diff options
188 files changed, 10654 insertions, 5401 deletions
diff --git a/package-lock.json b/package-lock.json index 384e82ce2..0347984a4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -401,17 +401,17 @@ "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==" }, "@emotion/is-prop-valid": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.0.tgz", - "integrity": "sha512-3aDpDprjM0AwaxGE09bOPkNxHpBd+kA6jty3RnaEXdweX1DF1U3VQpPYb0g1IStAuK7SVQ1cy+bNBBKp4W3Fjg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-1.2.1.tgz", + "integrity": "sha512-61Mf7Ufx4aDxx1xlDeOm8aFFigGHE4z+0sKCa+IHCeZKiyP9RLD0Mmx7m8b9/Cf37f7NAvQOOJAbQQGVr5uERw==", "requires": { - "@emotion/memoize": "^0.8.0" + "@emotion/memoize": "^0.8.1" }, "dependencies": { "@emotion/memoize": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", - "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==" + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" } } }, @@ -421,78 +421,128 @@ "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==" }, "@emotion/react": { - "version": "11.10.5", - "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.10.5.tgz", - "integrity": "sha512-TZs6235tCJ/7iF6/rvTaOH4oxQg2gMAcdHemjwLKIjKz4rRuYe1HJ2TQJKnAcRAfOUDdU8XoDadCe1rl72iv8A==", + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/react/-/react-11.11.0.tgz", + "integrity": "sha512-ZSK3ZJsNkwfjT3JpDAWJZlrGD81Z3ytNDsxw1LKq1o+xkmO5pnWfr6gmCC8gHEFf3nSSX/09YrG67jybNPxSUw==", "requires": { "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.10.5", - "@emotion/cache": "^11.10.5", - "@emotion/serialize": "^1.1.1", - "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0", - "@emotion/utils": "^1.2.0", - "@emotion/weak-memoize": "^0.3.0", + "@emotion/babel-plugin": "^11.11.0", + "@emotion/cache": "^11.11.0", + "@emotion/serialize": "^1.1.2", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", "hoist-non-react-statics": "^3.3.1" }, "dependencies": { + "@emotion/babel-plugin": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", + "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", + "requires": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/serialize": "^1.1.2", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, "@emotion/cache": { - "version": "11.10.5", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.10.5.tgz", - "integrity": "sha512-dGYHWyzTdmK+f2+EnIGBpkz1lKc4Zbj2KHd4cX3Wi8/OWr5pKslNjc3yABKH4adRGCvSX4VDC0i04mrrq0aiRA==", + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", + "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", "requires": { - "@emotion/memoize": "^0.8.0", - "@emotion/sheet": "^1.2.1", - "@emotion/utils": "^1.2.0", - "@emotion/weak-memoize": "^0.3.0", - "stylis": "4.1.3" + "@emotion/memoize": "^0.8.1", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "stylis": "4.2.0" } }, "@emotion/hash": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz", - "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==" + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" }, "@emotion/memoize": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", - "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==" + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" }, "@emotion/serialize": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.1.tgz", - "integrity": "sha512-Zl/0LFggN7+L1liljxXdsVSVlg6E/Z/olVWpfxUTxOAmi8NU7YoeWeLfi1RmnB2TATHoaWwIBRoL+FvAJiTUQA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.2.tgz", + "integrity": "sha512-zR6a/fkFP4EAcCMQtLOhIgpprZOwNmCldtpaISpvz348+DP4Mz8ZoKaGGCQpbzepNIUWbq4w6hNZkwDyKoS+HA==", "requires": { - "@emotion/hash": "^0.9.0", - "@emotion/memoize": "^0.8.0", - "@emotion/unitless": "^0.8.0", - "@emotion/utils": "^1.2.0", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/unitless": "^0.8.1", + "@emotion/utils": "^1.2.1", "csstype": "^3.0.2" } }, "@emotion/sheet": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.1.tgz", - "integrity": "sha512-zxRBwl93sHMsOj4zs+OslQKg/uhF38MB+OMKoCrVuS0nyTkqnau+BM3WGEoOptg9Oz45T/aIGs1qbVAsEFo3nA==" + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", + "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" }, "@emotion/unitless": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", - "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==" + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" }, "@emotion/utils": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.0.tgz", - "integrity": "sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" }, "@emotion/weak-memoize": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.0.tgz", - "integrity": "sha512-AHPmaAx+RYfZz0eYu6Gviiagpmiyw98ySSlQvCUhVGDRtDFe4DBS0x1bSjdF3gqUDYOczB+yYvBTtEylYSdRhg==" + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", + "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" + }, + "babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "requires": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + } + }, + "cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + } }, "csstype": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", - "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + }, + "stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" } } }, @@ -514,54 +564,104 @@ "integrity": "sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA==" }, "@emotion/styled": { - "version": "11.10.5", - "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.10.5.tgz", - "integrity": "sha512-8EP6dD7dMkdku2foLoruPCNkRevzdcBaY6q0l0OsbyJK+x8D9HWjX27ARiSIKNF634hY9Zdoedh8bJCiva8yZw==", + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-11.11.0.tgz", + "integrity": "sha512-hM5Nnvu9P3midq5aaXj4I+lnSfNi7Pmd4EWk1fOZ3pxookaQTNew6bp4JaCBYM4HVFZF9g7UjJmsUmC2JlxOng==", "requires": { "@babel/runtime": "^7.18.3", - "@emotion/babel-plugin": "^11.10.5", - "@emotion/is-prop-valid": "^1.2.0", - "@emotion/serialize": "^1.1.1", - "@emotion/use-insertion-effect-with-fallbacks": "^1.0.0", - "@emotion/utils": "^1.2.0" - }, - "dependencies": { + "@emotion/babel-plugin": "^11.11.0", + "@emotion/is-prop-valid": "^1.2.1", + "@emotion/serialize": "^1.1.2", + "@emotion/use-insertion-effect-with-fallbacks": "^1.0.1", + "@emotion/utils": "^1.2.1" + }, + "dependencies": { + "@emotion/babel-plugin": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.11.0.tgz", + "integrity": "sha512-m4HEDZleaaCH+XgDDsPF15Ht6wTLsgDTeR3WYj9Q/k76JtWhrJjcP4+/XlG8LGT/Rol9qUfOIztXeA84ATpqPQ==", + "requires": { + "@babel/helper-module-imports": "^7.16.7", + "@babel/runtime": "^7.18.3", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/serialize": "^1.1.2", + "babel-plugin-macros": "^3.1.0", + "convert-source-map": "^1.5.0", + "escape-string-regexp": "^4.0.0", + "find-root": "^1.1.0", + "source-map": "^0.5.7", + "stylis": "4.2.0" + } + }, "@emotion/hash": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.0.tgz", - "integrity": "sha512-14FtKiHhy2QoPIzdTcvh//8OyBlknNs2nXRwIhG904opCby3l+9Xaf/wuPvICBF0rc1ZCNBd3nKe9cd2mecVkQ==" + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.9.1.tgz", + "integrity": "sha512-gJB6HLm5rYwSLI6PQa+X1t5CFGrv1J1TWG+sOyMCeKz2ojaj6Fnl/rZEspogG+cvqbt4AE/2eIyD2QfLKTBNlQ==" }, "@emotion/memoize": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.0.tgz", - "integrity": "sha512-G/YwXTkv7Den9mXDO7AhLWkE3q+I92B+VqAE+dYG4NGPaHZGvt3G8Q0p9vmE+sq7rTGphUbAvmQ9YpbfMQGGlA==" + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" }, "@emotion/serialize": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.1.tgz", - "integrity": "sha512-Zl/0LFggN7+L1liljxXdsVSVlg6E/Z/olVWpfxUTxOAmi8NU7YoeWeLfi1RmnB2TATHoaWwIBRoL+FvAJiTUQA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-1.1.2.tgz", + "integrity": "sha512-zR6a/fkFP4EAcCMQtLOhIgpprZOwNmCldtpaISpvz348+DP4Mz8ZoKaGGCQpbzepNIUWbq4w6hNZkwDyKoS+HA==", "requires": { - "@emotion/hash": "^0.9.0", - "@emotion/memoize": "^0.8.0", - "@emotion/unitless": "^0.8.0", - "@emotion/utils": "^1.2.0", + "@emotion/hash": "^0.9.1", + "@emotion/memoize": "^0.8.1", + "@emotion/unitless": "^0.8.1", + "@emotion/utils": "^1.2.1", "csstype": "^3.0.2" } }, "@emotion/unitless": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.0.tgz", - "integrity": "sha512-VINS5vEYAscRl2ZUDiT3uMPlrFQupiKgHz5AA4bCH1miKBg4qtwkim1qPmJj/4WG6TreYMY111rEFsjupcOKHw==" + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.8.1.tgz", + "integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==" }, "@emotion/utils": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.0.tgz", - "integrity": "sha512-sn3WH53Kzpw8oQ5mgMmIzzyAaH2ZqFEbozVVBSYp538E06OSE6ytOp7pRAjNQR+Q/orwqdQYJSe2m3hCOeznkw==" + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" + }, + "babel-plugin-macros": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz", + "integrity": "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==", + "requires": { + "@babel/runtime": "^7.12.5", + "cosmiconfig": "^7.0.0", + "resolve": "^1.19.0" + } + }, + "cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", + "requires": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + } }, "csstype": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.1.tgz", - "integrity": "sha512-DJR/VvkAvSZW9bTouZue2sSxDwdTN92uHjqeKVm+0dAqdfNykRzQ95tay8aXMBAAPpUiq4Qcug2L7neoRh2Egw==" + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + }, + "escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==" + }, + "stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" } } }, @@ -576,9 +676,9 @@ "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" }, "@emotion/use-insertion-effect-with-fallbacks": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.0.tgz", - "integrity": "sha512-1eEgUGmkaljiBnRMTdksDV1W4kUnmwgp7X9G8B++9GYwl1lUdqSndSriIrTJ0N7LQaoauY9JJ2yhiOYK5+NI4A==" + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@emotion/use-insertion-effect-with-fallbacks/-/use-insertion-effect-with-fallbacks-1.0.1.tgz", + "integrity": "sha512-jT/qyKZ9rzLErtrjGgdkMBn2OP8wl0G3sQlBb3YPryvKHsjvINUhVaPFfP+fpBcOkmrVOVEEHQFJ7nbj2TH2gw==" }, "@emotion/utils": { "version": "0.11.3", @@ -1015,6 +1115,259 @@ "react-is": "^16.8.0 || ^17.0.0" } }, + "@mui/base": { + "version": "5.0.0-beta.1", + "resolved": "https://registry.npmjs.org/@mui/base/-/base-5.0.0-beta.1.tgz", + "integrity": "sha512-xrkDCeu3JQE+JjJUnJnOrdQJMXwKhbV4AW+FRjMIj5i9cHK3BAuatG/iqbf1M+jklVWLk0KdbgioKwK+03aYbA==", + "requires": { + "@babel/runtime": "^7.21.0", + "@emotion/is-prop-valid": "^1.2.0", + "@mui/types": "^7.2.4", + "@mui/utils": "^5.13.1", + "@popperjs/core": "^2.11.7", + "clsx": "^1.2.1", + "prop-types": "^15.8.1", + "react-is": "^18.2.0" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.5.tgz", + "integrity": "sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==", + "requires": { + "regenerator-runtime": "^0.13.11" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + } + } + }, + "@mui/core-downloads-tracker": { + "version": "5.13.1", + "resolved": "https://registry.npmjs.org/@mui/core-downloads-tracker/-/core-downloads-tracker-5.13.1.tgz", + "integrity": "sha512-qDHtNDO72NcBQMhaWBt9EZMvNiO+OXjPg5Sdk/6LgRDw6Zr3HdEZ5n2FJ/qtYsaT/okGyCuQavQkcZCOCEVf/g==" + }, + "@mui/icons-material": { + "version": "5.11.16", + "resolved": "https://registry.npmjs.org/@mui/icons-material/-/icons-material-5.11.16.tgz", + "integrity": "sha512-oKkx9z9Kwg40NtcIajF9uOXhxiyTZrrm9nmIJ4UjkU2IdHpd4QVLbCc/5hZN/y0C6qzi2Zlxyr9TGddQx2vx2A==", + "requires": { + "@babel/runtime": "^7.21.0" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.5.tgz", + "integrity": "sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==", + "requires": { + "regenerator-runtime": "^0.13.11" + } + } + } + }, + "@mui/material": { + "version": "5.13.1", + "resolved": "https://registry.npmjs.org/@mui/material/-/material-5.13.1.tgz", + "integrity": "sha512-qSnbJZer8lIuDYFDv19/t3s0AXYY9SxcOdhCnGvetRSfOG4gy3TkiFXNCdW5OLNveTieiMpOuv46eXUmE3ZA6A==", + "requires": { + "@babel/runtime": "^7.21.0", + "@mui/base": "5.0.0-beta.1", + "@mui/core-downloads-tracker": "^5.13.1", + "@mui/system": "^5.13.1", + "@mui/types": "^7.2.4", + "@mui/utils": "^5.13.1", + "@types/react-transition-group": "^4.4.6", + "clsx": "^1.2.1", + "csstype": "^3.1.2", + "prop-types": "^15.8.1", + "react-is": "^18.2.0", + "react-transition-group": "^4.4.5" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.5.tgz", + "integrity": "sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==", + "requires": { + "regenerator-runtime": "^0.13.11" + } + }, + "@types/react-transition-group": { + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/@types/react-transition-group/-/react-transition-group-4.4.6.tgz", + "integrity": "sha512-VnCdSxfcm08KjsJVQcfBmhEQAPnLB8G08hAxn39azX1qYBQ/5RVQuoHuKIcfKOdncuaUvEpFKFzEvbtIMsfVew==", + "requires": { + "@types/react": "*" + } + }, + "csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + } + } + }, + "@mui/private-theming": { + "version": "5.13.1", + "resolved": "https://registry.npmjs.org/@mui/private-theming/-/private-theming-5.13.1.tgz", + "integrity": "sha512-HW4npLUD9BAkVppOUZHeO1FOKUJWAwbpy0VQoGe3McUYTlck1HezGHQCfBQ5S/Nszi7EViqiimECVl9xi+/WjQ==", + "requires": { + "@babel/runtime": "^7.21.0", + "@mui/utils": "^5.13.1", + "prop-types": "^15.8.1" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.5.tgz", + "integrity": "sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==", + "requires": { + "regenerator-runtime": "^0.13.11" + } + } + } + }, + "@mui/styled-engine": { + "version": "5.12.3", + "resolved": "https://registry.npmjs.org/@mui/styled-engine/-/styled-engine-5.12.3.tgz", + "integrity": "sha512-AhZtiRyT8Bjr7fufxE/mLS+QJ3LxwX1kghIcM2B2dvJzSSg9rnIuXDXM959QfUVIM3C8U4x3mgVoPFMQJvc4/g==", + "requires": { + "@babel/runtime": "^7.21.0", + "@emotion/cache": "^11.10.8", + "csstype": "^3.1.2", + "prop-types": "^15.8.1" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.5.tgz", + "integrity": "sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==", + "requires": { + "regenerator-runtime": "^0.13.11" + } + }, + "@emotion/cache": { + "version": "11.11.0", + "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-11.11.0.tgz", + "integrity": "sha512-P34z9ssTCBi3e9EI1ZsWpNHcfY1r09ZO0rZbRO2ob3ZQMnFI35jB536qoXbkdesr5EUhYi22anuEJuyxifaqAQ==", + "requires": { + "@emotion/memoize": "^0.8.1", + "@emotion/sheet": "^1.2.2", + "@emotion/utils": "^1.2.1", + "@emotion/weak-memoize": "^0.3.1", + "stylis": "4.2.0" + } + }, + "@emotion/memoize": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.8.1.tgz", + "integrity": "sha512-W2P2c/VRW1/1tLox0mVUalvnWXxavmv/Oum2aPsRcoDJuob75FC3Y8FbpfLwUegRcxINtGUMPq0tFCvYNTBXNA==" + }, + "@emotion/sheet": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-1.2.2.tgz", + "integrity": "sha512-0QBtGvaqtWi+nx6doRwDdBIzhNdZrXUppvTM4dtZZWEGTXL/XE/yJxLMGlDT1Gt+UHH5IX1n+jkXyytE/av7OA==" + }, + "@emotion/utils": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-1.2.1.tgz", + "integrity": "sha512-Y2tGf3I+XVnajdItskUCn6LX+VUDmP6lTL4fcqsXAv43dnlbZiuW4MWQW38rW/BVWSE7Q/7+XQocmpnRYILUmg==" + }, + "@emotion/weak-memoize": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.3.1.tgz", + "integrity": "sha512-EsBwpc7hBUJWAsNPBmJy4hxWx12v6bshQsldrVmjxJoc3isbxhOrF2IcCpaXxfvq03NwkI7sbsOLXbYuqF/8Ww==" + }, + "csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + }, + "stylis": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/stylis/-/stylis-4.2.0.tgz", + "integrity": "sha512-Orov6g6BB1sDfYgzWfTHDOxamtX1bE/zo104Dh9e6fqJ3PooipYyfJ0pUmrZO2wAvO8YbEyeFrkV91XTsGMSrw==" + } + } + }, + "@mui/system": { + "version": "5.13.1", + "resolved": "https://registry.npmjs.org/@mui/system/-/system-5.13.1.tgz", + "integrity": "sha512-BsDUjhiO6ZVAvzKhnWBHLZ5AtPJcdT+62VjnRLyA4isboqDKLg4fmYIZXq51yndg/soDK9RkY5lYZwEDku13Ow==", + "requires": { + "@babel/runtime": "^7.21.0", + "@mui/private-theming": "^5.13.1", + "@mui/styled-engine": "^5.12.3", + "@mui/types": "^7.2.4", + "@mui/utils": "^5.13.1", + "clsx": "^1.2.1", + "csstype": "^3.1.2", + "prop-types": "^15.8.1" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.5.tgz", + "integrity": "sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==", + "requires": { + "regenerator-runtime": "^0.13.11" + } + }, + "csstype": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.2.tgz", + "integrity": "sha512-I7K1Uu0MBPzaFKg4nI5Q7Vs2t+3gWWW648spaF+Rg7pI9ds18Ugn+lvg4SHczUdKlHI5LWBXyqfS8+DufyBsgQ==" + } + } + }, + "@mui/types": { + "version": "7.2.4", + "resolved": "https://registry.npmjs.org/@mui/types/-/types-7.2.4.tgz", + "integrity": "sha512-LBcwa8rN84bKF+f5sDyku42w1NTxaPgPyYKODsh01U1fVstTClbUoSA96oyRBnSNyEiAVjKm6Gwx9vjR+xyqHA==" + }, + "@mui/utils": { + "version": "5.13.1", + "resolved": "https://registry.npmjs.org/@mui/utils/-/utils-5.13.1.tgz", + "integrity": "sha512-6lXdWwmlUbEU2jUI8blw38Kt+3ly7xkmV9ljzY4Q20WhsJMWiNry9CX8M+TaP/HbtuyR8XKsdMgQW7h7MM3n3A==", + "requires": { + "@babel/runtime": "^7.21.0", + "@types/prop-types": "^15.7.5", + "@types/react-is": "^18.2.0", + "prop-types": "^15.8.1", + "react-is": "^18.2.0" + }, + "dependencies": { + "@babel/runtime": { + "version": "7.21.5", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.21.5.tgz", + "integrity": "sha512-8jI69toZqqcsnqGGqwGS4Qb1VwLOEp4hz+CXPywcvjs60u3B4Pom/U/7rm4W8tMOYEB+E9wgD0mW1l3r8qlI9Q==", + "requires": { + "regenerator-runtime": "^0.13.11" + } + }, + "@types/react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/@types/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-1vz2yObaQkLL7YFe/pme2cpvDsCwI1WXIfL+5eLz0MI9gFG24Re16RzUsI8t9XZn9ZWvgLNDrJBmrqXJO7GNQQ==", + "requires": { + "@types/react": "*" + } + }, + "react-is": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.2.0.tgz", + "integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==" + } + } + }, "@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -1119,6 +1472,11 @@ "@octokit/openapi-types": "^16.0.0" } }, + "@popperjs/core": { + "version": "2.11.7", + "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.11.7.tgz", + "integrity": "sha512-Cr4OjIkipTtcXKjAsm8agyleBuDHvxzeBoa1v543lbv1YaIwQjESsVcmjiWiPEbC1FIeHOG/Op9kdCmAmiS3Kw==" + }, "@react-google-maps/api": { "version": "2.18.1", "resolved": "https://registry.npmjs.org/@react-google-maps/api/-/api-2.18.1.tgz", @@ -2812,7 +3170,7 @@ "textarea-caret": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/textarea-caret/-/textarea-caret-3.0.2.tgz", - "integrity": "sha512-gRzeti2YS4did7UJnPQ47wrjD+vp+CJIe9zbsu0bJ987d8QVLvLNG9757rqiQTIy4hGIeFauTTJt5Xkn51UkXg==" + "integrity": "sha1-82DEhpmqGr9xhoCkOjGoUGZcLK8=" } } }, @@ -2911,7 +3269,7 @@ "after": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/after/-/after-0.8.2.tgz", - "integrity": "sha512-QbJ0NTQ/I9DI3uSJA4cbexiwQeRAfjPScqIbSjUDd9TOrcg6pTkdgziesOqxBMBzit8vFCTwrP27t13vFOORRA==" + "integrity": "sha1-/ts5T58OAqqXaOcCvaI7UF+ufh8=" }, "agent-base": { "version": "6.0.2", @@ -3808,7 +4166,7 @@ "backo2": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/backo2/-/backo2-1.0.2.tgz", - "integrity": "sha512-zj6Z6M7Eq+PBZ7PQxl5NT665MvJdAkzp0f60nAJ+sLaSCBPMwVak5ZegFbgVCzFcCJTKFoMizvM5Ld7+JrRJHA==" + "integrity": "sha1-MasayLEpNjRj41s+u2n038+6eUc=" }, "bail": { "version": "2.0.2", @@ -3878,7 +4236,7 @@ "base64-arraybuffer": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz", - "integrity": "sha512-a1eIFi4R9ySrbiMuyTGx5e92uRH5tQY6kArNcFaKBUleIoLjdjBg7Zxm3Mqm3Kmkf27HLR/1fnxX9q8GQ7Iavg==" + "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=" }, "base64-js": { "version": "1.5.1", @@ -4859,7 +5217,7 @@ "component-bind": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz", - "integrity": "sha512-WZveuKPeKAG9qY+FkYDeADzdHyTYdIboXS59ixDeRJL5ZhxpqUnxSOwop4FQjMsiYm3/Or8cegVbpAHNA7pHxw==" + "integrity": "sha1-AMYIq33Nk4l8AAllGx06jh5zu9E=" }, "component-emitter": { "version": "1.3.0", @@ -4869,7 +5227,7 @@ "component-inherit": { "version": "0.0.3", "resolved": "https://registry.npmjs.org/component-inherit/-/component-inherit-0.0.3.tgz", - "integrity": "sha512-w+LhYREhatpVqTESyGFg3NlP6Iu0kEKUHETY9GoZP/pQyW4mHFZuFWRUCIqVPZ36ueVLtoOEZaAqbCF2RDndaA==" + "integrity": "sha1-ZF/ErfWLcrZJ1crmUTVhnbJv8UM=" }, "compress-commons": { "version": "2.1.1", @@ -5589,7 +5947,7 @@ "custom-event": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", - "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==" + "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=" }, "cyclist": { "version": "1.0.1", @@ -5597,6 +5955,16 @@ "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=", "dev": true }, + "d": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/d/-/d-1.0.1.tgz", + "integrity": "sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA==", + "dev": true, + "requires": { + "es5-ext": "^0.10.50", + "type": "^1.0.1" + } + }, "d3": { "version": "7.8.4", "resolved": "https://registry.npmjs.org/d3/-/d3-7.8.4.tgz", @@ -6793,7 +7161,7 @@ "ms": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" }, "ws": { "version": "7.4.6", @@ -6835,8 +7203,8 @@ "integrity": "sha512-/o+BXHmB7ocbHEAs6F2EnG0ogybVVUdkRunTT2glZU9XAaGmhqskrvKwqXuDfNjEO0LZKWdejEEpnq8aM0tOaw==" }, "equation-editor-react": { - "version": "github:bobzel/equation-editor-react#75915e852b4b36a6a4cd3e1cbc80598da6b65227", - "from": "github:bobzel/equation-editor-react#useLocally", + "version": "git+ssh://git@github.com/bobzel/equation-editor-react.git#75915e852b4b36a6a4cd3e1cbc80598da6b65227", + "from": "equation-editor-react@github:bobzel/equation-editor-react#useLocally", "requires": { "jquery": "^3.4.1", "mathquill": "^0.10.1-a" @@ -6977,6 +7345,28 @@ "is-symbol": "^1.0.2" } }, + "es5-ext": { + "version": "0.10.62", + "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.62.tgz", + "integrity": "sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA==", + "dev": true, + "requires": { + "es6-iterator": "^2.0.3", + "es6-symbol": "^3.1.3", + "next-tick": "^1.1.0" + } + }, + "es6-iterator": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.3.tgz", + "integrity": "sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g==", + "dev": true, + "requires": { + "d": "1", + "es5-ext": "^0.10.35", + "es6-symbol": "^3.1.1" + } + }, "es6-promise": { "version": "3.2.1", "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-3.2.1.tgz", @@ -6988,6 +7378,7 @@ "integrity": "sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA==", "dev": true, "requires": { + "d": "^1.0.1", "ext": "^1.1.2" } }, @@ -9960,14 +10351,14 @@ "isarray": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.1.tgz", - "integrity": "sha512-c2cu3UxbI+b6kR3fy0nRnAhodsvR9dx7U5+znCOzdj6IfP3upFURTr0Xl5BlQZNKZjEtxrmVyfSdeE3O57smoQ==" + "integrity": "sha1-o32U7ZzaLVmGXJ92/llu4fM4dB4=" } } }, "has-cors": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-cors/-/has-cors-1.1.0.tgz", - "integrity": "sha512-g5VNKdkFuUuVCP9gYfDJHjK2nqdQJ7aDLTnycnc2+RvsOQbuLdF5pm7vuE5J76SEBIQjs4kQY/BWq74JUmjbXA==" + "integrity": "sha1-XkdHk/fqmEPRu5nCPu9J/xJv/zk=" }, "has-flag": { "version": "3.0.0", @@ -10543,7 +10934,7 @@ "indexof": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/indexof/-/indexof-0.0.1.tgz", - "integrity": "sha512-i0G7hLJ1z0DE8dsqJa2rycj9dBmNKgXBvotXtZYXakU9oivfB9Uj2ZBC27qqef2U58/ZLwalxa1X/RDCdkHtVg==" + "integrity": "sha1-gtwzbSMrkGIXnQWrMpOmYFn9Q10=" }, "inflight": { "version": "1.0.6", @@ -12077,7 +12468,7 @@ "lodash.isequal": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==" + "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=" }, "lodash.isplainobject": { "version": "4.0.6", @@ -12338,7 +12729,7 @@ "mathquill": { "version": "0.10.1-a", "resolved": "https://registry.npmjs.org/mathquill/-/mathquill-0.10.1-a.tgz", - "integrity": "sha512-snSAEwAtwdwBFSor+nVBnWWQtTw67kgAgKMyAIxuz4ZPboy0qkWZmd7BL3lfOXp/INihhRlU1PcfaAtDaRhmzA==", + "integrity": "sha1-vyylaQEAY6w0vNXVKa3Ag3zVPD8=", "requires": { "jquery": "^1.12.3" }, @@ -12346,7 +12737,7 @@ "jquery": { "version": "1.12.4", "resolved": "https://registry.npmjs.org/jquery/-/jquery-1.12.4.tgz", - "integrity": "sha512-UEVp7PPK9xXYSk8xqXCJrkXnKZtlgWkd2GsAQbMRFK6S/ePU2JN5G2Zum8hIVjzR3CpdfSqdqAzId/xd4TJHeg==" + "integrity": "sha1-AeHfuikP5z3rp3zurLD5ui/sngw=" } } }, @@ -14032,7 +14423,7 @@ "dependencies": { "@iarna/cli": { "version": "2.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/@iarna/cli/-/cli-2.1.0.tgz", "integrity": "sha512-rvVVqDa2g860niRbqs3D5RhL4la3dc1vwk+NlpKPZxKaMSHtE2se6C2x8NeveN+rcjp3/686X+u+09CZ+7lmAQ==", "requires": { "glob": "^7.1.2", @@ -14135,7 +14526,7 @@ "dependencies": { "safe-buffer": { "version": "5.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" } } @@ -14150,7 +14541,7 @@ "dependencies": { "safe-buffer": { "version": "5.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" } } @@ -14159,12 +14550,12 @@ }, "asap": { "version": "2.0.6", - "resolved": false, + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==" }, "asn1": { "version": "0.2.6", - "resolved": false, + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", "requires": { "safer-buffer": "~2.1.0" @@ -14172,7 +14563,7 @@ }, "assert-plus": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==" }, "asynckit": { @@ -14182,22 +14573,22 @@ }, "aws-sign2": { "version": "0.7.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==" }, "aws4": { "version": "1.11.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==" }, "balanced-match": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, "bcrypt-pbkdf": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", "requires": { "tweetnacl": "^0.14.3" @@ -14218,7 +14609,7 @@ }, "bluebird": { "version": "3.7.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==" }, "boxen": { @@ -14266,7 +14657,7 @@ }, "cacache": { "version": "12.0.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz", "integrity": "sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==", "requires": { "bluebird": "^3.5.5", @@ -14303,7 +14694,7 @@ }, "caseless": { "version": "0.12.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" }, "chalk": { @@ -14447,7 +14838,7 @@ }, "combined-stream": { "version": "1.0.8", - "resolved": false, + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", "requires": { "delayed-stream": "~1.0.0" @@ -14455,7 +14846,7 @@ }, "concat-map": { "version": "0.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==" }, "concat-stream": { @@ -14485,7 +14876,7 @@ "dependencies": { "safe-buffer": { "version": "5.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" } } @@ -14500,7 +14891,7 @@ "dependencies": { "safe-buffer": { "version": "5.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" } } @@ -14509,7 +14900,7 @@ }, "config-chain": { "version": "1.1.13", - "resolved": false, + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", "requires": { "ini": "^1.3.4", @@ -14610,7 +15001,7 @@ }, "dashdash": { "version": "1.14.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", "requires": { "assert-plus": "^1.0.0" @@ -14643,7 +15034,7 @@ }, "decode-uri-component": { "version": "0.2.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==" }, "deep-extend": { @@ -14689,7 +15080,7 @@ }, "dezalgo": { "version": "1.0.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz", "integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==", "requires": { "asap": "^2.0.0", @@ -14741,7 +15132,7 @@ "dependencies": { "safe-buffer": { "version": "5.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" } } @@ -14756,7 +15147,7 @@ "dependencies": { "safe-buffer": { "version": "5.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" } } @@ -14765,7 +15156,7 @@ }, "ecc-jsbn": { "version": "0.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", "requires": { "jsbn": "~0.1.0", @@ -14800,7 +15191,7 @@ }, "env-paths": { "version": "2.2.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==" }, "err-code": { @@ -14884,7 +15275,7 @@ }, "extsprintf": { "version": "1.3.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==" }, "fast-json-stable-stringify": { @@ -14894,12 +15285,12 @@ }, "figgy-pudding": { "version": "3.5.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==" }, "filter-obj": { "version": "1.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz", "integrity": "sha512-8rXg1ZnX7xzy2NGDVkBVaAy+lSlPNwad13BtgSlLuxfIslyt5Vg64U7tFcCt4WS1R0hvtnQybT/IyCkGZ3DpXQ==" }, "find-npm-prefix": { @@ -14932,7 +15323,7 @@ "dependencies": { "safe-buffer": { "version": "5.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" } } @@ -14947,7 +15338,7 @@ "dependencies": { "safe-buffer": { "version": "5.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" } } @@ -14956,12 +15347,12 @@ }, "forever-agent": { "version": "0.6.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==" }, "form-data": { "version": "2.3.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", "requires": { "asynckit": "^0.4.0", @@ -14994,7 +15385,7 @@ "dependencies": { "safe-buffer": { "version": "5.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" } } @@ -15009,7 +15400,7 @@ "dependencies": { "safe-buffer": { "version": "5.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" } } @@ -15077,7 +15468,7 @@ "dependencies": { "safe-buffer": { "version": "5.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" } } @@ -15092,7 +15483,7 @@ "dependencies": { "safe-buffer": { "version": "5.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" } } @@ -15101,7 +15492,7 @@ }, "fs.realpath": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, "function-bind": { @@ -15191,7 +15582,7 @@ }, "getpass": { "version": "0.1.7", - "resolved": false, + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", "requires": { "assert-plus": "^1.0.0" @@ -15199,7 +15590,7 @@ }, "glob": { "version": "7.2.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "requires": { "fs.realpath": "^1.0.0", @@ -15212,7 +15603,7 @@ "dependencies": { "minimatch": { "version": "3.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "requires": { "brace-expansion": "^1.1.7" @@ -15255,12 +15646,12 @@ }, "graceful-fs": { "version": "4.2.10", - "resolved": false, + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==" }, "har-schema": { "version": "2.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==" }, "har-validator": { @@ -15339,7 +15730,7 @@ }, "http-signature": { "version": "1.2.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", "requires": { "assert-plus": "^1.0.0", @@ -15466,7 +15857,7 @@ }, "is-cidr": { "version": "3.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/is-cidr/-/is-cidr-3.1.1.tgz", "integrity": "sha512-Gx+oErgq1j2jAKCR2Kbq0b3wbH0vQKqZ0wOlHxm0o56nq51Cs/DZA8oz9dMDhbHyHEGgJ86eTeVudtgMMOx3Mw==", "requires": { "cidr-regex": "^2.0.10" @@ -15545,7 +15936,7 @@ }, "is-typedarray": { "version": "1.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" }, "isarray": { @@ -15560,12 +15951,12 @@ }, "isstream": { "version": "0.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" }, "jsbn": { "version": "0.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" }, "json-parse-better-errors": { @@ -15575,7 +15966,7 @@ }, "json-parse-even-better-errors": { "version": "2.3.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, "json-schema": { @@ -15585,7 +15976,7 @@ }, "json-stringify-safe": { "version": "5.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" }, "jsonparse": { @@ -15803,7 +16194,7 @@ }, "lock-verify": { "version": "2.2.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/lock-verify/-/lock-verify-2.2.2.tgz", "integrity": "sha512-2CUNtr1ZSVKJHcYP8uEzafmmuyauCB5zZimj8TvQd/Lflt9kXVZs+8S+EbAzZLaVUDn8CYGmeC3DFGdYfnCzeQ==", "requires": { "@iarna/cli": "^2.1.0", @@ -15932,7 +16323,7 @@ }, "meant": { "version": "1.0.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/meant/-/meant-1.0.3.tgz", "integrity": "sha512-88ZRGcNxAq4EH38cQ4D85PM57pikCwS8Z99EWHODxN7KBY+UuPiqzRTtZzS8KTXO/ywSWbdjjJST2Hly/EQxLw==" }, "mime-db": { @@ -15950,7 +16341,7 @@ }, "minimatch": { "version": "3.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", "requires": { "brace-expansion": "^1.1.7" @@ -15999,7 +16390,7 @@ }, "mkdirp": { "version": "0.5.6", - "resolved": false, + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", "requires": { "minimist": "^1.2.6" @@ -16047,7 +16438,7 @@ }, "node-gyp": { "version": "5.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-5.1.1.tgz", "integrity": "sha512-WH0WKGi+a4i4DUt2mHnvocex/xPLp9pYt5R6M2JdFB7pJ7Z34hveZ4nDTGTiLXCkitA9T8HFZjhinBCiVHYcWw==", "requires": { "env-paths": "^2.2.0", @@ -16385,7 +16776,7 @@ "dependencies": { "safe-buffer": { "version": "5.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" } } @@ -16400,7 +16791,7 @@ "dependencies": { "safe-buffer": { "version": "5.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" } } @@ -16414,7 +16805,7 @@ }, "path-is-absolute": { "version": "1.0.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==" }, "path-is-inside": { @@ -16434,7 +16825,7 @@ }, "performance-now": { "version": "2.1.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" }, "pify": { @@ -16483,7 +16874,7 @@ }, "proto-list": { "version": "1.2.4", - "resolved": false, + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==" }, "protoduck": { @@ -16506,7 +16897,7 @@ }, "psl": { "version": "1.9.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" }, "pump": { @@ -16546,12 +16937,12 @@ }, "qs": { "version": "6.5.3", - "resolved": false, + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==" }, "query-string": { "version": "6.14.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/query-string/-/query-string-6.14.1.tgz", "integrity": "sha512-XDxAeVmpfu1/6IjyT/gXHOl+S0vQ9owggJ30hhWKdHAsNPOcasn5o9BW0eejZqL2e4vMjhAxoW3jVHcD6mbcYw==", "requires": { "decode-uri-component": "^0.2.0", @@ -16562,7 +16953,7 @@ }, "qw": { "version": "1.0.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/qw/-/qw-1.0.2.tgz", "integrity": "sha512-1PhZ/iLKwlVNq45dnerTMKFjMof49uqli7/0QsvPNbX5OJ3IZ8msa9lUpvPheVdP+IYYPrf6cOaVil7S35joVA==" }, "rc": { @@ -16608,7 +16999,7 @@ }, "read-package-json": { "version": "2.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/read-package-json/-/read-package-json-2.1.2.tgz", "integrity": "sha512-D1KmuLQr6ZSJS0tW8hf3WGpRlwszJOXZ3E8Yd/DNRaM5d+1wVRZdHlpGBLAuovjr28LbWvjpWkBHMxpRGGjzNA==", "requires": { "glob": "^7.1.1", @@ -16667,7 +17058,7 @@ }, "request": { "version": "2.88.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", "requires": { "aws-sign2": "~0.7.0", @@ -16737,7 +17128,7 @@ }, "safe-buffer": { "version": "5.2.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" }, "safer-buffer": { @@ -16908,7 +17299,7 @@ }, "sshpk": { "version": "1.17.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz", "integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==", "requires": { "asn1": "~0.2.3", @@ -16964,7 +17355,7 @@ "dependencies": { "safe-buffer": { "version": "5.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" } } @@ -16979,7 +17370,7 @@ "dependencies": { "safe-buffer": { "version": "5.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" } } @@ -16993,7 +17384,7 @@ }, "strict-uri-encode": { "version": "2.0.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz", "integrity": "sha512-QwiXZgpRcKkhTj2Scnn++4PKtWsH0kpzZ62L2R6c/LUVYv7hVnZqcg2+sMuT6R7Jusu1vviK/MFsu6kNJfWlEQ==" }, "string-width": { @@ -17149,7 +17540,7 @@ "dependencies": { "safe-buffer": { "version": "5.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" } } @@ -17164,7 +17555,7 @@ "dependencies": { "safe-buffer": { "version": "5.1.2", - "resolved": false, + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" } } @@ -17183,7 +17574,7 @@ }, "tough-cookie": { "version": "2.5.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", "requires": { "psl": "^1.1.28", @@ -17192,7 +17583,7 @@ "dependencies": { "punycode": { "version": "2.1.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" } } @@ -17207,7 +17598,7 @@ }, "tweetnacl": { "version": "0.14.5", - "resolved": false, + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" }, "typedarray": { @@ -17278,7 +17669,7 @@ }, "uri-js": { "version": "4.4.1", - "resolved": false, + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "requires": { "punycode": "^2.1.0" @@ -17319,7 +17710,7 @@ }, "uuid": { "version": "3.4.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" }, "validate-npm-package-license": { @@ -17341,7 +17732,7 @@ }, "verror": { "version": "1.10.0", - "resolved": false, + "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", "requires": { "assert-plus": "^1.0.0", @@ -22152,7 +22543,7 @@ "to-array": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/to-array/-/to-array-0.1.4.tgz", - "integrity": "sha512-LhVdShQD/4Mk4zXNroIQZJC+Ap3zgLcDuwEdcmLv9CCO73NWockQDwyUnW/m8VX/EElfL6FcYx7EeutN4HJA6A==" + "integrity": "sha1-F+bBH3PdTz10zaek/zI46a2b+JA=" }, "to-fast-properties": { "version": "2.0.0", @@ -22660,6 +23051,12 @@ "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" }, + "type": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/type/-/type-1.2.0.tgz", + "integrity": "sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg==", + "dev": true + }, "type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -23775,7 +24172,7 @@ "resolve-cwd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", - "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", + "integrity": "sha512-ccu8zQTrzVr954472aUVPLEcB3YpKSYR3cg/3lo1okzobPBM+1INXBbBZlDbnI/hbEocnf8j0QVo43hQKrbchg==", "dev": true, "requires": { "resolve-from": "^3.0.0" @@ -23784,7 +24181,7 @@ "resolve-from": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", + "integrity": "sha512-GnlH6vxLymXJNMBo7XP1fJIzBFbdYt49CuTwmB/6N53t+kMPRMFKz783LlQ4tv28XoQfMWinAJX6WCGf2IlaIw==", "dev": true }, "semver": { @@ -24336,7 +24733,7 @@ "yeast": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/yeast/-/yeast-0.1.2.tgz", - "integrity": "sha512-8HFIh676uyGYP6wP13R/j6OJ/1HwJ46snpvzE7aHAN3Ryqh2yX6Xox2B4CUmTwwOIzlG3Bs7ocsP5dZH/R1Qbg==" + "integrity": "sha1-AI4G2AlDIMNy28L47XagymyKxBk=" }, "yn": { "version": "3.1.1", diff --git a/package.json b/package.json index a812829ff..6cd271c96 100644 --- a/package.json +++ b/package.json @@ -130,6 +130,8 @@ "webpack-hot-middleware": "^2.25.1" }, "dependencies": { + "@emotion/react": "^11.11.0", + "@emotion/styled": "^11.11.0", "@ffmpeg/core": "0.10.0", "@ffmpeg/ffmpeg": "0.10.0", "@fortawesome/fontawesome-svg-core": "^6.3.0", @@ -141,6 +143,8 @@ "@hig/theme-context": "^2.1.3", "@hig/theme-data": "^2.23.1", "@material-ui/core": "^4.12.3", + "@mui/icons-material": "^5.11.16", + "@mui/material": "^5.13.1", "@octokit/core": "^4.0.4", "@react-google-maps/api": "^2.7.0", "@react-three/fiber": "^6.2.3", diff --git a/src/.DS_Store b/src/.DS_Store Binary files differindex 8f0e384aa..be99aa5af 100644 --- a/src/.DS_Store +++ b/src/.DS_Store diff --git a/src/Utils.ts b/src/Utils.ts index 73de6d754..8c56896c5 100644 --- a/src/Utils.ts +++ b/src/Utils.ts @@ -527,6 +527,10 @@ export function returnDefault(): 'default' { return 'default'; } +export function return18() { + return 18; +} + export function returnFalse() { return false; } @@ -540,7 +544,7 @@ export function returnNone() { } export function returnVal(val1?: number, val2?: number) { - return val1 !== undefined ? val1 : val2 !== undefined ? val2 : 0; + return val1 ? val1 : val2 !== undefined ? val2 : 0; } export function returnOne() { diff --git a/src/client/DocServer.ts b/src/client/DocServer.ts index ba64f993c..02b047a95 100644 --- a/src/client/DocServer.ts +++ b/src/client/DocServer.ts @@ -1,7 +1,8 @@ import { runInAction } from 'mobx'; import * as rp from 'request-promise'; import * as io from 'socket.io-client'; -import { Doc, Opt, UpdatingFromServer } from '../fields/Doc'; +import { Doc, Opt } from '../fields/Doc'; +import { UpdatingFromServer } from '../fields/DocSymbols'; import { FieldLoader } from '../fields/FieldLoader'; import { HandleUpdate, Id, Parent } from '../fields/FieldSymbols'; import { ObjectField } from '../fields/ObjectField'; diff --git a/src/client/Network.ts b/src/client/Network.ts index 19eff3b3b..d606b9854 100644 --- a/src/client/Network.ts +++ b/src/client/Network.ts @@ -2,6 +2,11 @@ import { Utils } from '../Utils'; import requestPromise = require('request-promise'); import { Upload } from '../server/SharedMediaTypes'; +/** + * Networking is repsonsible for connecting the client to the server. Networking + * mainly provides methods that the client can use to begin the process of + * interacting with the server, such as fetching or uploading files. + */ export namespace Networking { export async function FetchFromServer(relativeRoute: string) { return (await fetch(relativeRoute)).text(); @@ -18,20 +23,31 @@ export namespace Networking { } /** + * FileGuidPair attaches a guid to a file that is being uploaded, + * allowing the client to track the upload progress. + * + * When files are dragged to the canvas, the overWriteDoc's ID is + * used as the guid. Otherwise, a new guid is generated. + */ + export interface FileGuidPair { + file: File; + guid?: string; + } + /** * Handles uploading basic file types to server and makes the API call to "/uploadFormData" endpoint - * with the mapping of GUID to filem as parameters. + * with the mapping of guid to files as parameters. * - * @param files the files to be uploaded to the server + * @param fileguidpairs the files and corresponding guids to be uploaded to the server * @returns the response as a json from the server */ - export async function UploadFilesToServer<T extends Upload.FileInformation = Upload.FileInformation>(files: File | File[]): Promise<Upload.FileResponse<T>[]> { + export async function UploadFilesToServer<T extends Upload.FileInformation = Upload.FileInformation>(fileguidpairs: FileGuidPair | FileGuidPair[]): Promise<Upload.FileResponse<T>[]> { const formData = new FormData(); - if (Array.isArray(files)) { - if (!files.length) { + if (Array.isArray(fileguidpairs)) { + if (!fileguidpairs.length) { return []; } const maxFileSize = 50000000; - if (files.some(f => f.size > maxFileSize)) { + if (fileguidpairs.some(f => f.file.size > maxFileSize)) { return new Promise<any>(res => res([ { @@ -41,9 +57,11 @@ export namespace Networking { ]) ); } - files.forEach(file => formData.append(Utils.GenerateGuid(), file)); + // If the fileguidpair has a guid to use (From the overwriteDoc) use that guid. Otherwise, generate a new guid. + fileguidpairs.forEach(fileguidpair => formData.append(fileguidpair.guid ?? Utils.GenerateGuid(), fileguidpair.file)); } else { - formData.append(Utils.GenerateGuid(), files); + // Handle the case where fileguidpairs is a single file. + formData.append(fileguidpairs.guid ?? Utils.GenerateGuid(), fileguidpairs.file); } const parameters = { method: 'POST', diff --git a/src/client/apis/google_docs/GooglePhotosClientUtils.ts b/src/client/apis/google_docs/GooglePhotosClientUtils.ts index ff9460b62..e8fd8fb8a 100644 --- a/src/client/apis/google_docs/GooglePhotosClientUtils.ts +++ b/src/client/apis/google_docs/GooglePhotosClientUtils.ts @@ -1,27 +1,26 @@ -import { AssertionError } from "assert"; -import { EditorState } from "prosemirror-state"; -import { Doc, DocListCastAsync, Opt } from "../../../fields/Doc"; -import { Id } from "../../../fields/FieldSymbols"; -import { RichTextField } from "../../../fields/RichTextField"; -import { RichTextUtils } from "../../../fields/RichTextUtils"; -import { Cast, StrCast } from "../../../fields/Types"; -import { ImageField } from "../../../fields/URLField"; -import { MediaItem, NewMediaItemResult } from "../../../server/apis/google/SharedTypes"; -import { Utils } from "../../../Utils"; -import { Docs, DocumentOptions, DocUtils } from "../../documents/Documents"; -import { Networking } from "../../Network"; -import { FormattedTextBox } from "../../views/nodes/formattedText/FormattedTextBox"; -import { GoogleAuthenticationManager } from "../GoogleAuthenticationManager"; +import { AssertionError } from 'assert'; +import { EditorState } from 'prosemirror-state'; +import { Doc, DocListCastAsync, Opt } from '../../../fields/Doc'; +import { Id } from '../../../fields/FieldSymbols'; +import { RichTextField } from '../../../fields/RichTextField'; +import { RichTextUtils } from '../../../fields/RichTextUtils'; +import { Cast, StrCast } from '../../../fields/Types'; +import { ImageField } from '../../../fields/URLField'; +import { MediaItem, NewMediaItemResult } from '../../../server/apis/google/SharedTypes'; +import { Utils } from '../../../Utils'; +import { Docs, DocumentOptions, DocUtils } from '../../documents/Documents'; +import { Networking } from '../../Network'; +import { FormattedTextBox } from '../../views/nodes/formattedText/FormattedTextBox'; +import { GoogleAuthenticationManager } from '../GoogleAuthenticationManager'; import Photos = require('googlephotos'); export namespace GooglePhotos { - const endpoint = async () => new Photos(await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken()); export enum MediaType { ALL_MEDIA = 'ALL_MEDIA', PHOTO = 'PHOTO', - VIDEO = 'VIDEO' + VIDEO = 'VIDEO', } export type AlbumReference = { id: string } | { title: string }; @@ -58,11 +57,10 @@ export namespace GooglePhotos { HOUSES: 'HOUSES', GARDENS: 'GARDENS', FLOWERS: 'FLOWERS', - HOLIDAYS: 'HOLIDAYS' + HOLIDAYS: 'HOLIDAYS', }; export namespace Export { - export interface AlbumCreationResult { albumId: string; mediaItems: MediaItem[]; @@ -82,20 +80,20 @@ export namespace GooglePhotos { if (!images || !images.length) { return undefined; } - const resolved = title ? title : (StrCast(collection.title) || `Dash Collection (${collection[Id]}`); + const resolved = title ? title : StrCast(collection.title) || `Dash Collection (${collection[Id]}`; const { id, productUrl } = await Create.Album(resolved); const response = await Transactions.UploadImages(images, { id }, descriptionKey); if (response) { const { results, failed } = response; let index: Opt<number>; while ((index = failed.pop()) !== undefined) { - Doc.RemoveDocFromList(dataDocument, "data", images.splice(index, 1)[0]); + Doc.RemoveDocFromList(dataDocument, 'data', images.splice(index, 1)[0]); } const mediaItems: MediaItem[] = results.map(item => item.mediaItem); if (mediaItems.length !== images.length) { throw new AssertionError({ actual: mediaItems.length, expected: images.length }); } - const idMapping = new Doc; + const idMapping = new Doc(); for (let i = 0; i < images.length; i++) { const image = Doc.GetProto(images[i]); const mediaItem = mediaItems[i]; @@ -117,38 +115,30 @@ export namespace GooglePhotos { return { albumId: id, mediaItems }; } }; - } export namespace Import { - export type CollectionConstructor = (data: Array<Doc>, options: DocumentOptions, ...args: any) => Doc; export const CollectionFromSearch = async (constructor: CollectionConstructor, requested: Opt<Partial<Query.SearchOptions>>): Promise<Doc> => { await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(); const response = await Query.ContentSearch(requested); const uploads = await Transactions.WriteMediaItemsToServer(response); - const children = uploads.map((upload: Transactions.UploadInformation) => { - const document = Docs.Create.ImageDocument(Utils.fileUrl(upload.fileNames.clean)); - document.contentSize = upload.contentSize; - return document; - }); + const children = uploads.map((upload: Transactions.UploadInformation) => Docs.Create.ImageDocument(Utils.fileUrl(upload.fileNames.clean) /*, {"data_contentSize":upload.contentSize}*/)); const options = { _width: 500, _height: 500 }; return constructor(children, options); }; - } export namespace Query { - - const delimiter = ", "; - const comparator = (a: string, b: string) => (a < b) ? -1 : (a > b ? 1 : 0); + const delimiter = ', '; + const comparator = (a: string, b: string) => (a < b ? -1 : a > b ? 1 : 0); export const TagChildImages = async (collection: Doc) => { await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(); const idMapping = await Cast(collection.googlePhotosIdMapping, Doc); if (!idMapping) { - throw new Error("Appending image metadata requires that the targeted collection have already been mapped to an album!"); + throw new Error('Appending image metadata requires that the targeted collection have already been mapped to an album!'); } const tagMapping = new Map<string, string>(); const images = (await DocListCastAsync(collection.data))!.map(Doc.GetProto); @@ -169,7 +159,7 @@ export namespace GooglePhotos { const concatenated = tagMapping.get(image[Id])!; const tags = concatenated.split(delimiter); if (tags.length > 1) { - const cleaned = concatenated.replace(ContentCategories.NONE + delimiter, ""); + const cleaned = concatenated.replace(ContentCategories.NONE + delimiter, ''); image.googlePhotosTags = cleaned.split(delimiter).sort(comparator).join(delimiter); } else { image.googlePhotosTags = ContentCategories.NONE; @@ -253,19 +243,15 @@ export namespace GooglePhotos { export const GetImage = async (mediaItemId: string): Promise<Transactions.MediaItem> => { return (await endpoint()).mediaItems.get(mediaItemId); }; - } namespace Create { - export const Album = async (title: string) => { return (await endpoint()).albums.create(title); }; - } export namespace Transactions { - export interface UploadInformation { mediaPaths: string[]; fileNames: { [key: string]: string }; @@ -295,18 +281,20 @@ export namespace GooglePhotos { }; export const WriteMediaItemsToServer = async (body: { mediaItems: any[] }): Promise<UploadInformation[]> => { - const uploads = await Networking.PostToServer("/googlePhotosMediaGet", body); + const uploads = await Networking.PostToServer('/googlePhotosMediaGet', body); return uploads; }; - export const UploadThenFetch = async (sources: Doc[], album?: AlbumReference, descriptionKey = "caption") => { + export const UploadThenFetch = async (sources: Doc[], album?: AlbumReference, descriptionKey = 'caption') => { const response = await UploadImages(sources, album, descriptionKey); if (!response) { return undefined; } - const baseUrls: string[] = await Promise.all(response.results.map(item => { - return new Promise<string>(resolve => Query.GetImage(item.mediaItem.id).then(item => resolve(item.baseUrl))); - })); + const baseUrls: string[] = await Promise.all( + response.results.map(item => { + return new Promise<string>(resolve => Query.GetImage(item.mediaItem.id).then(item => resolve(item.baseUrl))); + }) + ); return baseUrls; }; @@ -315,9 +303,9 @@ export namespace GooglePhotos { failed: number[]; } - export const UploadImages = async (sources: Doc[], album?: AlbumReference, descriptionKey = "caption"): Promise<Opt<ImageUploadResults>> => { + export const UploadImages = async (sources: Doc[], album?: AlbumReference, descriptionKey = 'caption'): Promise<Opt<ImageUploadResults>> => { await GoogleAuthenticationManager.Instance.fetchOrGenerateAccessToken(); - if (album && "title" in album) { + if (album && 'title' in album) { album = await Create.Album(album.title); } const media: MediaInput[] = []; @@ -327,13 +315,13 @@ export namespace GooglePhotos { return; } const url = data.url.href; - const target = Doc.MakeAlias(source); + const target = Doc.MakeEmbedding(source); const description = parseDescription(target, descriptionKey); await DocUtils.makeCustomViewClicked(target, Docs.Create.FreeformDocument); media.push({ url, description }); } if (media.length) { - const results = await Networking.PostToServer("/googlePhotosMediaPost", { media, album }); + const results = await Networking.PostToServer('/googlePhotosMediaPost', { media, album }); return results; } }; @@ -341,14 +329,12 @@ export namespace GooglePhotos { const parseDescription = (document: Doc, descriptionKey: string) => { let description: string = Utils.prepend(`/doc/${document[Id]}?sharing=true`); const target = document[descriptionKey]; - if (typeof target === "string") { + if (typeof target === 'string') { description = target; } else if (target instanceof RichTextField) { description = RichTextUtils.ToPlainText(EditorState.fromJSON(FormattedTextBox.Instance.config, JSON.parse(target.Data))); } return description; }; - } - -}
\ No newline at end of file +} diff --git a/src/client/documents/DocumentTypes.ts b/src/client/documents/DocumentTypes.ts index d99cd2dac..cdc8c275c 100644 --- a/src/client/documents/DocumentTypes.ts +++ b/src/client/documents/DocumentTypes.ts @@ -11,7 +11,7 @@ export enum DocumentType { AUDIO = 'audio', REC = 'recording', PDF = 'pdf', - INK = 'inks', + INK = 'ink', SCREENSHOT = 'screenshot', FONTICON = 'fonticonbox', FILTER = 'filter', @@ -19,7 +19,7 @@ export enum DocumentType { LABEL = 'label', // simple text label BUTTON = 'button', // onClick button WEBCAM = 'webcam', // webcam - MARKER = 'marker', // generic marker document not intended to be viewed independently of its context (e.g., for text selections in PDF/Web/RTF) + CONFIG = 'config', // configuration document intended to specify a view layout configuration, but not be directly rendered (e.g., for saving the page# of a PDF, or view transform of a collection) DATE = 'date', // calendar view of a date SCRIPTING = 'script', // script editor EQUATION = 'equation', // equation editor @@ -27,6 +27,7 @@ export enum DocumentType { MAP = 'map', DATAVIZ = 'dataviz', LOADING = 'loading', + SIMULATION = 'simulation', //physics simulation // special purpose wrappers that either take no data or are compositions of lower level types LINK = 'link', diff --git a/src/client/documents/Documents.ts b/src/client/documents/Documents.ts index 5a7894c08..a91d5806c 100644 --- a/src/client/documents/Documents.ts +++ b/src/client/documents/Documents.ts @@ -2,7 +2,8 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { action, runInAction } from 'mobx'; import { basename } from 'path'; import { DateField } from '../../fields/DateField'; -import { Doc, DocListCast, Field, Initializing, Opt, updateCachedAcls } from '../../fields/Doc'; +import { Doc, DocListCast, Field, Opt, updateCachedAcls } from '../../fields/Doc'; +import { Initializing } from '../../fields/DocSymbols'; import { Id } from '../../fields/FieldSymbols'; import { HtmlField } from '../../fields/HtmlField'; import { InkField, PointData } from '../../fields/InkField'; @@ -24,7 +25,7 @@ import { DirectoryImportBox } from '../util/Import & Export/DirectoryImportBox'; import { FollowLinkScript } from '../util/LinkFollower'; import { LinkManager } from '../util/LinkManager'; import { ScriptingGlobals } from '../util/ScriptingGlobals'; -import { undoBatch, UndoManager } from '../util/UndoManager'; +import { undoable, undoBatch, UndoManager } from '../util/UndoManager'; import { CollectionDockingView } from '../views/collections/CollectionDockingView'; import { DimUnit } from '../views/collections/collectionMulticolumn/CollectionMulticolumnView'; import { CollectionView } from '../views/collections/CollectionView'; @@ -37,7 +38,6 @@ import { FontIconBox } from '../views/nodes/button/FontIconBox'; import { ColorBox } from '../views/nodes/ColorBox'; import { ComparisonBox } from '../views/nodes/ComparisonBox'; import { DataVizBox } from '../views/nodes/DataVizBox/DataVizBox'; -import { OpenWhereMod } from '../views/nodes/DocumentView'; import { EquationBox } from '../views/nodes/EquationBox'; import { FieldViewProps } from '../views/nodes/FieldView'; import { FormattedTextBox } from '../views/nodes/formattedText/FormattedTextBox'; @@ -50,6 +50,7 @@ import { LinkDescriptionPopup } from '../views/nodes/LinkDescriptionPopup'; import { LoadingBox } from '../views/nodes/LoadingBox'; import { MapBox } from '../views/nodes/MapBox/MapBox'; import { PDFBox } from '../views/nodes/PDFBox'; +import { PhysicsSimulationBox } from '../views/nodes/PhysicsBox/PhysicsSimulationBox'; import { RecordingBox } from '../views/nodes/RecordingBox/RecordingBox'; import { ScreenshotBox } from '../views/nodes/ScreenshotBox'; import { ScriptingBox } from '../views/nodes/ScriptingBox'; @@ -109,18 +110,28 @@ class DocInfo extends FInfo { } } class DimInfo extends FInfo { - fieldType? = 'DimUnit'; + fieldType? = 'enumeration'; values? = [DimUnit.Pixel, DimUnit.Ratio]; - readOnly = true; + readOnly = false; } class PEInfo extends FInfo { - fieldType? = 'pointerEvents'; + fieldType? = 'enumeration'; values? = ['all', 'none']; - readOnly = true; + readOnly = false; } class DAInfo extends FInfo { - fieldType? = 'dropActionType'; - values? = ['alias', 'copy', 'move', 'same', 'proto', 'none']; + fieldType? = 'enumeration'; + values? = ['embed', 'copy', 'move', 'same', 'proto', 'none']; + readOnly = false; +} +class CTypeInfo extends FInfo { + fieldType? = 'enumeration'; + values? = Array.from(Object.keys(CollectionViewType)); + readOnly = false; +} +class DTypeInfo extends FInfo { + fieldType? = 'enumeration'; + values? = Array.from(Object.keys(DocumentType)); readOnly = true; } class DateInfo extends FInfo { @@ -133,271 +144,267 @@ type STRt = StrInfo | string; type DOCt = DocInfo | Doc; type DIMt = DimInfo | typeof DimUnit.Pixel | typeof DimUnit.Ratio; type PEVt = PEInfo | 'none' | 'all'; +type COLLt = CTypeInfo | CollectionViewType; type DROPt = DAInfo | dropActionType; type DATEt = DateInfo | number; +type DTYPEt = DTypeInfo | string; export class DocumentOptions { + // coordinate and dimensions depending on view x?: NUMt = new NumInfo('x coordinate of document in a freeform view'); y?: NUMt = new NumInfo('y coordinage of document in a freeform view'); z?: NUMt = new NumInfo('whether document is in overlay (1) or not (0)', false, [1, 0]); - system?: BOOLt = new BoolInfo('is this a system created/owned doc', true); - type?: STRt = new StrInfo('type of document', true, Array.from(Object.keys(DocumentType))); - title?: string; - creationDate?: DATEt = new DateInfo('date the document was created', true); - _dropAction?: DROPt = new DAInfo("what should happen to this document when it's dropped somewhere else"); - allowOverlayDrop?: BOOLt = new BoolInfo('can documents be dropped onto this document without using dragging title bar or holding down embed key (ctrl)?', true); - childDropAction?: DROPt = new DAInfo("what should happen to the source document when it's dropped onto a child of a collection "); - targetDropAction?: DROPt = new DAInfo('what should happen to the source document when ??? '); - userColor?: STRt = new StrInfo('color associated with a Dash user (seen in header fields of shared documents)'); - color?: STRt = new StrInfo('foreground color data doc'); - backgroundColor?: STRt = new StrInfo('background color for data doc'); - _autoHeight?: BOOLt = new BoolInfo('whether document automatically resizes vertically to display contents'); - _headerHeight?: NUMt = new NumInfo('height of document header used for displaying title'); - _headerFontSize?: NUMt = new NumInfo('font size of header of custom notes'); - _headerPointerEvents?: PEVt = new PEInfo('types of events the header of a custom text document can consume'); - _panX?: NUMt = new NumInfo('horizontal pan location of a freeform view'); - _panY?: NUMt = new NumInfo('vertical pan location of a freeform view'); + _dimMagnitude?: NUMt = new NumInfo("magnitude of collectionMulti{row,col} element's width or height"); + _dimUnit?: DIMt = new DimInfo("units of collectionMulti{row,col} element's width or height - 'px' or '*' for pixels or relative units"); + lat?: NUMt = new NumInfo('latitude coordinate for map views'); + lng?: NUMt = new NumInfo('longitude coordinate for map views'); + _timecodeToShow?: NUMt = new NumInfo('the time that a document should be displayed (e.g., when an annotation shows up as a video plays)'); + _timecodeToHide?: NUMt = new NumInfo('the time that a document should be hidden'); _width?: NUMt = new NumInfo('displayed width of a document'); _height?: NUMt = new NumInfo('displayed height of document'); - 'data-nativeWidth'?: NUMt = new NumInfo('native width of data field contents (e.g., the pixel width of an image)'); - 'data-nativeHeight'?: NUMt = new NumInfo('native height of data field contents (e.g., the pixel height of an image)'); + data_nativeWidth?: NUMt = new NumInfo('native width of data field contents (e.g., the pixel width of an image)'); + data_nativeHeight?: NUMt = new NumInfo('native height of data field contents (e.g., the pixel height of an image)'); + linearBtnWidth?: NUMt = new NumInfo('unexpanded width of a linear menu button (button "width" changes when it expands)'); _nativeWidth?: NUMt = new NumInfo('native width of document contents (e.g., the pixel width of an image)'); _nativeHeight?: NUMt = new NumInfo('native height of document contents (e.g., the pixel height of an image)'); _nativeDimModifiable?: BOOLt = new BoolInfo('native dimensions can be modified using document decoration reizers'); _nativeHeightUnfrozen?: BOOLt = new BoolInfo('native height can be changed independent of width by dragging decoration resizers'); - _dimMagnitude?: NUMt = new NumInfo("magnitude of collectionMulti{row,col} element's width or height"); - _dimUnit?: DIMt = new DimInfo("units of collectionMulti{row,col} element's width or height - 'px' or '*' for pixels or relative units"); - _fitWidth?: BOOLt = new BoolInfo('whether document should scale its contents to fit its rendered width or not (e.g., for PDFviews)'); - _fitContentsToBox?: BOOLt = new BoolInfo('whether a freeformview should zoom/scale to create a shrinkwrapped view of its content'); - _contentBounds?: List<number>; // the (forced) bounds of the document to display. format is: [left, top, right, bottom] - _lockedPosition?: boolean; // lock the x,y coordinates of the document so that it can't be dragged - _lockedTransform?: boolean; // lock the panx,pany and scale parameters of the document so that it be panned/zoomed - _followLinkToggle?: boolean; // whether document, when clicked, toggles display of its link target - _showTitle?: string; // field name to display in header (:hover is an optional suffix) - _showAltContentUI?: boolean; // whether to show alternate content button - _isLightbox?: boolean; // whether a collection acts as a lightbox by opening lightbox links by hiding all other documents in collection besides link target - _showCaption?: string; // which field to display in the caption area. leave empty to have no caption - _scrollTop?: number; // scroll location for pdfs - _noAutoscroll?: boolean; // whether collections autoscroll when this item is dragged - _chromeHidden?: boolean; // whether the editing chrome for a document is hidden - _searchDoc?: boolean; // is this a search document (used to change UI for search results in schema view) - _forceActive?: boolean; // flag to handle pointer events when not selected (or otherwise active) - enableDragWhenActive?: boolean; // allow dragging even if document contentts are active (e.g., tree, groups) - _stayInCollection?: boolean; // whether the document should remain in its collection when someone tries to drag and drop it elsewhere - _raiseWhenDragged?: boolean; // whether a document is brought to front when dragged. - _hideContextMenu?: boolean; // whether the context menu can be shown - _viewType?: string; // sub type of a collection - viewType?: string; // sub type of a collection - _gridGap?: number; // gap between items in masonry view - _viewScale?: number; // how much a freeform view has been scaled (zoomed) - _overflow?: string; // set overflow behavior - _xMargin?: number; // gap between left edge of document and start of masonry/stacking layouts - _yMargin?: number; // gap between top edge of dcoument and start of masonry/stacking layouts - _xPadding?: number; - _yPadding?: number; - _itemIndex?: number; // which item index the carousel viewer is showing - _showSidebar?: boolean; //whether an annotationsidebar should be displayed for text docuemnts - _singleLine?: boolean; // whether text document is restricted to a single line (carriage returns make new document) - _minFontSize?: number; // minimum font size for labelBoxes - _maxFontSize?: number; // maximum font size for labelBoxes - _columnWidth?: number; - _columnsHideIfEmpty?: boolean; // whether stacking view column headings should be hidden - _fontSize?: string; - _fontFamily?: string; - _fontWeight?: string; - _pivotField?: string; // field key used to determine headings for sections in stacking, masonry, pivot views - _curPage?: number; // current page of a PDF or other? paginated document - _currentTimecode?: number; // the current timecode of a time-based document (e.g., current time of a video) value is in seconds - _currentFrame?: number; // the current frame of a frame-based collection (e.g., progressive slide) - _timecodeToShow?: number; // the time that a document should be displayed (e.g., when an annotation shows up as a video plays) - _timecodeToHide?: number; // the time that a document should be hidden - _timelineLabel?: boolean; // whether the document exists on a timeline - '_carousel-caption-xMargin'?: NUMt = new NumInfo('x margin of caption inside of a carouself collection', true); - '_carousel-caption-yMargin'?: NUMt = new NumInfo('y margin of caption inside of a carouself collection', true); - 'icon-nativeWidth'?: NUMt = new NumInfo('native width of icon view', true); - 'icon-nativeHeight'?: NUMt = new NumInfo('native height of icon view', true); - 'dragFactory-count'?: NUMt = new NumInfo('number of items created from a drag button (used for setting title with incrementing index)', true); - openFactoryLocation?: string; // an OpenWhere value to place the factory created document - openFactoryAsDelegate?: boolean; // - lat?: number; - lng?: number; - infoWindowOpen?: boolean; - author?: string; - _layoutKey?: string; - fieldValues?: List<any>; // possible field values used by fieldInfos - fieldType?: string; // type of afield used by fieldInfos - unrendered?: boolean; // denotes an annotation that is not rendered with a DocumentView (e.g, rtf/pdf text selections and links to scroll locations in web/pdf) + 'acl-Public'?: string; // public permissions '_acl-Public'?: string; // public permissions - version?: string; // version identifier for a document - label?: string; - hidden?: boolean; - _hidden?: boolean; - pointerEvents?: string; // pointer events that the documentview should have - mediaState?: string; // status of audio/video media document: "pendingRecording", "recording", "paused", "playing" - recording?: boolean; // whether WebCam is recording or not - autoPlayAnchors?: boolean; // whether to play audio/video when an anchor is clicked in a stackedTimeline. - dontPlayLinkOnSelect?: boolean; // whether an audio/video should start playing when a link is followed to it. - linkSource?: Doc; // the source document for a collection of backlinks + type?: DTYPEt = new DTypeInfo('type of document', true); + _type_collection?: COLLt = new CTypeInfo('how collection is rendered'); // sub type of a collection + title?: STRt = new StrInfo('title of document'); + caption?: RichTextField; + author?: string; // STRt = new StrInfo('creator of document'); // bcz: don't change this. Otherwise, the userDoc's field Infos will have a FieldInfo assigned to its author field which will render it unreadable + author_date?: DATEt = new DateInfo('date the document was created', true); + annotationOn?: DOCt = new DocInfo('document annotated by this document'); + color?: STRt = new StrInfo('foreground color data doc'); + hidden?: BOOLt = new BoolInfo('whether the document is not rendered by its collection'); + backgroundColor?: STRt = new StrInfo('background color for data doc'); + opacity?: NUMt = new NumInfo('document opacity'); + viewTransitionTime?: NUMt = new NumInfo('transition duration for view parameters'); + dontRegisterView?: BOOLt = new BoolInfo('are views of this document registered so that they can be found when following links, etc'); + dontUndo?: BOOLt = new BoolInfo('whether button clicks should be undoable (true for Undo/Redo/and sidebar) AND whether modifications to document are undoable (true for linearview menu buttons to prevent open/close from entering undo stack)'); + _headerHeight?: NUMt = new NumInfo('height of document header used for displaying title'); + _headerFontSize?: NUMt = new NumInfo('font size of header of custom notes'); + _headerPointerEvents?: PEVt = new PEInfo('types of events the header of a custom text document can consume'); + _lockedPosition?: BOOLt = new BoolInfo("lock the x,y coordinates of the document so that it can't be dragged"); + _lockedTransform?: BOOLt = new BoolInfo('lock the freeform_panx,freeform_pany and scale parameters of the document so that it be panned/zoomed'); + + layout?: string | Doc; // default layout string or template document + layout_keyValue?: STRt = new StrInfo('layout definition for showing keyValue view of document'); + layout_explainer?: STRt = new StrInfo('explanation displayed at top of a collection to describe its purpose'); + layout_headerButton?: DOCt = new DocInfo('the (button) Doc to display at the top of a collection.'); + layout_disableBrushing?: BOOLt = new BoolInfo('whether to suppress border highlighting'); + layout_unrendered?: BOOLt = new BoolInfo('denotes an annotation that is not rendered with a DocumentView (e.g, rtf/pdf text selections and links to scroll locations in web/pdf)'); + layout_hideOpenButton?: BOOLt = new BoolInfo('whether to hide the open full screen button when selected'); + layout_hideDocumentButtonBar?: BOOLt = new BoolInfo('whether to hide the document decorations lower button bar when selected'); + layout_hideLinkAnchors?: BOOLt = new BoolInfo('suppresses link anchor dots from being displayed'); + layout_hideAllLinks?: BOOLt = new BoolInfo('whether all individual blue anchor dots should be hidden'); + layout_hideResizeHandles?: BOOLt = new BoolInfo('whether to hide the resize handles when selected'); + layout_hideLinkButton?: BOOLt = new BoolInfo('whether the blue link counter button should be hidden'); + layout_hideDecorationTitle?: BOOLt = new BoolInfo('whether to suppress the document decortations title when selected'); + layout_borderRounding?: string; + layout_boxShadow?: string; // box-shadow css string OR "standard" to use dash standard box shadow + layout_maxAutoHeight?: NUMt = new NumInfo('maximum height for newly created (eg, from pasting) text documents'); + _layout_autoHeight?: BOOLt = new BoolInfo('whether document automatically resizes vertically to display contents'); + _layout_curPage?: NUMt = new NumInfo('current page of a PDF or other? paginated document'); + _layout_currentTimecode?: NUMt = new NumInfo('the current timecode of a time-based document (e.g., current time of a video) value is in seconds'); + _layout_hideContextMenu?: BOOLt = new BoolInfo('whether the context menu can be shown'); + _layout_fitWidth?: BOOLt = new BoolInfo('whether document should scale its contents to fit its rendered width or not (e.g., for PDFviews)'); + _layout_fitContentsToBox?: BOOLt = new BoolInfo('whether a freeformview should zoom/scale to create a shrinkwrapped view of its content'); + _layout_fieldKey?: STRt = new StrInfo('the field key containing the current layout definition'); + _layout_enableAltContentUI?: BOOLt = new BoolInfo('whether to show alternate content button'); + _layout_showTitle?: string; // field name to display in header (:hover is an optional suffix) + _layout_showSidebar?: BOOLt = new BoolInfo('whether an annotationsidebar should be displayed for text docuemnts'); + _layout_showCaption?: string; // which field to display in the caption area. leave empty to have no caption + + _chromeHidden?: BOOLt = new BoolInfo('whether the editing chrome for a document is hidden'); + _gridGap?: NUMt = new NumInfo('gap between items in masonry view'); + _xMargin?: NUMt = new NumInfo('gap between left edge of document and start of masonry/stacking layouts'); + _yMargin?: NUMt = new NumInfo('gap between top edge of dcoument and start of masonry/stacking layouts'); + _xPadding?: NUMt = new NumInfo('x padding'); + _yPadding?: NUMt = new NumInfo('y padding'); + _singleLine?: boolean; // whether label box is restricted to one line of text + _createDocOnCR?: boolean; // whether carriage returns and tabs create new text documents + _columnWidth?: NUMt = new NumInfo('width of table column'); + _columnsHideIfEmpty?: BOOLt = new BoolInfo('whether stacking view column headings should be hidden'); + _caption_xMargin?: NUMt = new NumInfo('x margin of caption inside of a carousel collection', true); + _caption_yMargin?: NUMt = new NumInfo('y margin of caption inside of a carousel collection', true); + icon_nativeWidth?: NUMt = new NumInfo('native width of icon view', true); + icon_nativeHeight?: NUMt = new NumInfo('native height of icon view', true); + _text_fontSize?: string; + _text_fontFamily?: string; + _text_fontWeight?: string; + _pivotField?: string; // field key used to determine headings for sections in stacking, masonry, pivot views + + infoWindowOpen?: BOOLt = new BoolInfo('whether info window corresponding to pin is open (on MapDocuments)'); + _carousel_index?: NUMt = new NumInfo('which item index the carousel viewer is showing'); + _label_minFontSize?: NUMt = new NumInfo('minimum font size for labelBoxes'); + _label_maxFontSize?: NUMt = new NumInfo('maximum font size for labelBoxes'); + stroke_width?: NUMt = new NumInfo('width of an ink stroke'); + icon_label?: STRt = new StrInfo('label to use for a fontIcon doc (otherwise, the title is used)'); + mediaState?: STRt = new StrInfo('status of audio/video media document: "pendingRecording", "recording", "paused", "playing"'); + recording?: BOOLt = new BoolInfo('whether WebCam is recording or not'); + autoPlayAnchors?: BOOLt = new BoolInfo('whether to play audio/video when an anchor is clicked in a stackedTimeline.'); + dontPlayLinkOnSelect?: BOOLt = new BoolInfo('whether an audio/video should start playing when a link is followed to it.'); updateContentsScript?: ScriptField; // reactive script invoked when viewing a document that can update contents of a collection (or do anything) toolTip?: string; // tooltip to display on hover toolType?: string; // type of pen tool - expertMode?: boolean; // something available only in expert (not novice) mode + expertMode?: BOOLt = new BoolInfo('something available only in expert (not novice) mode'); + + contentPointerEvents?: string; // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents contextMenuFilters?: List<ScriptField>; contextMenuScripts?: List<ScriptField>; contextMenuLabels?: List<string>; contextMenuIcons?: List<string>; - defaultDoubleClick?: 'ignore' | 'default'; // ignore double clicks, or deafult (undefined) means open document full screen - waitForDoubleClickToClick?: 'always' | 'never' | 'default'; // whether a click function wait for double click to expire. 'default' undefined = wait only if there's a click handler, "never" = never wait, "always" = alway wait - dontUndo?: boolean; // whether button clicks should be undoable (this is set to true for Undo/Redo/and sidebar buttons that open the siebar panel) - description?: string; // added for links - layout?: string | Doc; // default layout string for a document - contentPointerEvents?: string; // pointer events allowed for content of a document view. eg. set to "none" in menuSidebar for sharedDocs so that you can select a document, but not interact with its contents - childLimitHeight?: number; // whether to limit the height of collection children. 0 - means height can be no bigger than width + childFilters_boolean?: string; + childFilters?: List<string>; + childLimitHeight?: NUMt = new NumInfo('whether to limit the height of collection children. 0 - means height can be no bigger than width'); childLayoutTemplate?: Doc; // template for collection to use to render its children (see PresBox layout in tree view) childLayoutString?: string; // template string for collection to use to render its children - childDocumentsActive?: boolean; // whether child documents are active when parent is document active - childDontRegisterViews?: boolean; - childHideLinkButton?: boolean; // hide link buttons on all children + childDocumentsActive?: BOOLt = new BoolInfo('whether child documents are active when parent is document active'); + childDontRegisterViews?: BOOLt = new BoolInfo('whether child document views should be registered so that they can be found when following links, etc'); + childHideLinkButton?: BOOLt = new BoolInfo('hide link buttons on all children'); childContextMenuFilters?: List<ScriptField>; childContextMenuScripts?: List<ScriptField>; childContextMenuLabels?: List<string>; childContextMenuIcons?: List<string>; - followLinkZoom?: boolean; // whether to zoom to the target of a link - hideLinkButton?: boolean; // whether the blue link counter button should be hidden - disableDocBrushing?: boolean; // whether to suppress border highlighting - hideDecorationTitle?: boolean; - hideOpenButton?: boolean; - hideResizeHandles?: boolean; - hideDocumentButtonBar?: boolean; - hideAllLinks?: boolean; // whether all individual blue anchor dots should be hidden - isTemplateForField?: string; // the field key for which the containing document is a rendering template - isTemplateDoc?: boolean; targetScriptKey?: string; // where to write a template script (used by collections with click templates which need to target onClick, onDoubleClick, etc) - templates?: List<string>; - hero?: ImageField; // primary image that best represents a compound document (e.g., for a buxton device document that has multiple images) - caption?: RichTextField; - opacity?: number; - defaultBackgroundColor?: string; - _linkAutoMove?: boolean; // whether link endpoint should move around the edges of a document to make shortest path to other link endpoint - hideLinkAnchors?: boolean; // suppresses link anchor dots from being displayed - isFolder?: boolean; - lastFrame?: number; // the last frame of a frame-based collection (e.g., progressive slide) - activeFrame?: number; // the active frame of a document in a frame base collection - appearFrame?: number; // the frame in which the document appears - viewTransitionTime?: number; // transition duration for view parameters - presPanX?: number; // panX saved as a view spec - presPanY?: number; // panY saved as a view spec - presViewScale?: number; // viewScale saved as a view Spec - presTransition?: number; //the time taken for the transition TO a document - presDuration?: number; //the duration of the slide in presentation view - presZoomText?: boolean; // whether text anchors should shown in a larger box when following links to make them stand out - borderRounding?: string; - boxShadow?: string; // box-shadow css string OR "standard" to use dash standard box shadow + + lastFrame?: NUMt = new NumInfo('the last frame of a frame-based collection (e.g., progressive slide)'); + activeFrame?: NUMt = new NumInfo('the active frame of a document in a frame base collection'); + appearFrame?: NUMt = new NumInfo('the frame in which the document appears'); + _currentFrame?: NUMt = new NumInfo('the current frame of a frame-based collection (e.g., progressive slide)'); + + isSystem?: BOOLt = new BoolInfo('is this a system created/owned doc', true); + isBaseProto?: BOOLt = new BoolInfo('is doc a base level prototype for data documents as opposed to data documents which are prototypes for layout documents. base protos are not cloned during a deep'); + isTemplateForField?: string; // the field key for which the containing document is a rendering template + isTemplateDoc?: BOOLt = new BoolInfo('is the document a template for creating other documents'); + isGroup?: BOOLt = new BoolInfo('should collection use a grouping UI behavior'); + isFolder?: BOOLt = new BoolInfo('is document a tree view folder'); + _isTimelineLabel?: BOOLt = new BoolInfo('is document a timeline label'); + _isLightbox?: BOOLt = new BoolInfo('whether a collection acts as a lightbox by opening lightbox links by hiding all other documents in collection besides link target'); + + presPanX?: NUMt = new NumInfo('panX saved as a view spec'); + presPanY?: NUMt = new NumInfo('panY saved as a view spec'); + presViewScale?: NUMt = new NumInfo('viewScale saved as a view Spec'); + presTransition?: NUMt = new NumInfo('the time taken for the transition TO a document'); + presDuration?: NUMt = new NumInfo('the duration of the slide in presentation view'); + presZoomText?: BOOLt = new BoolInfo('whether text anchors should shown in a larger box when following links to make them stand out'); + data?: any; - baseProto?: boolean; // is this a base prototoype - dontRegisterView?: boolean; - lookupField?: ScriptField; // script that returns the value of a field. This script is passed the rootDoc, layoutDoc, field, and container of the document. see PresBox. + data_useCors?: BOOLt = new BoolInfo('whether CORS protocol should be used for web page'); columnHeaders?: List<SchemaHeaderField>; // headers for stacking views schemaHeaders?: List<SchemaHeaderField>; // headers for schema view - clipWidth?: number; // percent transition from before to after in comparisonBox - dockingConfig?: string; - annotationOn?: Doc; - followLinkToggle?: boolean; - isGroup?: boolean; // whether a collection should use a grouping UI behavior - _removeDropProperties?: List<string>; // list of properties that should be removed from a document when it is dropped. e.g., a creator button may be forceActive to allow it be dragged, but the forceActive property can be removed from the dropped document + dockingConfig?: STRt = new StrInfo('configuration of golden layout windows (applies only if doc is rendered as a CollectionDockingView)'); + icon?: string; // icon used by fonticonbox to render button noteType?: string; - // BACKGROUND GRID - _backgroundGridShow?: boolean; + + // freeform properties + _freeform_backgroundGrid?: boolean; + _freeform_scale?: NUMt = new NumInfo('how much a freeform view has been scaled (zoomed)'); + _freeform_panX?: NUMt = new NumInfo('horizontal pan location of a freeform view'); + _freeform_panY?: NUMt = new NumInfo('vertical pan location of a freeform view'); + _freeform_noAutoPan?: BOOLt = new BoolInfo('disables autopanning when this item is dragged'); + _freeform_noZoom?: BOOLt = new BoolInfo('disables zooming'); //BUTTONS buttonText?: string; - iconShape?: string; // shapes of the fonticon border btnType?: string; btnList?: List<string>; docColorBtn?: string; userColorBtn?: string; - canClick?: string; script?: ScriptField; - numBtnType?: string; - numBtnMax?: number; - numBtnMin?: number; + numBtnMax?: NUMt = new NumInfo('maximum value of a number button'); + numBtnMin?: NUMt = new NumInfo('minimum value of a number button'); switchToggle?: boolean; badgeValue?: ScriptField; //LINEAR VIEW - linearViewIsExpanded?: boolean; // is linear view expanded - linearViewExpandable?: boolean; // can linear view be expanded - linearViewToggleButton?: string; // button to open close linear view group - linearViewSubMenu?: boolean; - linearBtnWidth?: number; - flexGap?: number; // Linear view flex gap + linearView_IsExpanded?: BOOLt = new BoolInfo('is linear view expanded'); + linearView_Expandable?: BOOLt = new BoolInfo('can linear view be expanded'); + linearView_SubMenu?: BOOLt = new BoolInfo('is doc a sub menu of more linear views'); + flexGap?: NUMt = new NumInfo('Linear view flex gap'); flexDirection?: 'unset' | 'row' | 'column' | 'row-reverse' | 'column-reverse'; - layout_linkView?: Doc; // view template for a link document - layout_keyValue?: string; // view tempalte for key value docs - linkRelationship?: string; // type of relatinoship a link represents - linkDisplay?: boolean; // whether a link line should be dipslayed between the two link anchors - anchor1?: Doc; - anchor2?: Doc; - 'anchor1-useLinkSmallAnchor'?: boolean; // whether anchor1 of a link should use a miniature anchor dot (as when the anchor is a text selection) - 'anchor2-useLinkSmallAnchor'?: boolean; // whether anchor1 of a link should use a miniature anchor dot (as when the anchor is a text selection) - ignoreClick?: boolean; + link_description?: string; // added for links + link_relationship?: string; // type of relatinoship a link represents + link_displayLine?: BOOLt = new BoolInfo('whether a link line should be dipslayed between the two link anchors'); + link_displayArrow?: BOOLt = new BoolInfo("whether to display link's directional arrowhead"); + link_anchor_1?: Doc; + link_anchor_2?: Doc; + link_autoMoveAnchors?: BOOLt = new BoolInfo('whether link endpoint should move around the edges of a document to make shortest path to other link endpoint'); + link_anchor_1_useSmallAnchor?: BOOLt = new BoolInfo('whether link_anchor_1 of a link should use a miniature anchor dot (as when the anchor is a text selection)'); + link_anchor_2_useSmallAnchor?: BOOLt = new BoolInfo('whether link_anchor_1 of a link should use a miniature anchor dot (as when the anchor is a text selection)'); + link_relationshipList?: List<string>; // for storing different link relationships (when set by user in the link editor) + link_relationshipSizes?: List<number>; //stores number of links contained in each relationship + link_colorList?: List<string>; // colors of links corresponding to specific link relationships + followLinkZoom?: BOOLt = new BoolInfo('whether to zoom to the target of a link'); + followLinkToggle?: BOOLt = new BoolInfo('whether target of link should be toggled on and off when following a link to it'); + followLinkLocation?: STRt = new StrInfo('where to open link target when following link'); + followLinkAnimEffect?: STRt = new StrInfo('animation effect triggered on target of link'); + followLinkAnimDirection?: STRt = new StrInfo('direction modifier for animation effect'); + + ignoreClick?: BOOLt = new BoolInfo('whether clicks on document should be ignored'); onClick?: ScriptField; onDoubleClick?: ScriptField; onChildClick?: ScriptField; // script given to children of a collection to execute when they are clicked onChildDoubleClick?: ScriptField; // script given to children of a collection to execute when they are double clicked + defaultDoubleClick?: 'ignore' | 'default'; // ignore double clicks, or deafult (undefined) means open document full screen + waitForDoubleClickToClick?: 'always' | 'never' | 'default'; // whether a click function wait for double click to expire. 'default' undefined = wait only if there's a click handler, "never" = never wait, "always" = alway wait onPointerDown?: ScriptField; onPointerUp?: ScriptField; + dragFactory_count?: NUMt = new NumInfo('number of items created from a drag button (used for setting title with incrementing index)', true); + openFactoryLocation?: string; // an OpenWhere value to place the factory created document + openFactoryAsDelegate?: BOOLt = new BoolInfo('create a delegate of the factory'); + _forceActive?: BOOLt = new BoolInfo('flag to handle pointer events when not selected (or otherwise active)'); + enableDragWhenActive?: BOOLt = new BoolInfo('allow dragging even if document contentts are active (e.g., tree, groups)'); + _stayInCollection?: BOOLt = new BoolInfo('whether the document should remain in its collection when someone tries to drag and drop it elsewhere'); + _raiseWhenDragged?: BOOLt = new BoolInfo('whether a document is brought to front when dragged.'); + allowOverlayDrop?: BOOLt = new BoolInfo('can documents be dropped onto this document without using dragging title bar or holding down embed key (ctrl)?', true); + childDropAction?: DROPt = new DAInfo("what should happen to the source document when it's dropped onto a child of a collection "); + targetDropAction?: DROPt = new DAInfo('what should happen to the source document when ??? '); + _dropAction?: DROPt = new DAInfo("what should happen to this document when it's dropped somewhere else"); + _removeDropProperties?: List<string>; // list of properties that should be removed from a document when it is dropped. e.g., a creator button may be forceActive to allow it be dragged, but the forceActive property can be removed from the dropped document + cloneFieldFilter?: List<string>; // fields not to copy when the document is clonedclipboard?: Doc; dropConverter?: ScriptField; // script to run when documents are dropped on this Document. dragFactory?: Doc; // document to create when dragging with a suitable onDragStart script clickFactory?: Doc; // document to create when clicking on a button with a suitable onClick script onDragStart?: ScriptField; //script to execute at start of drag operation -- e.g., when a "creator" button is dragged this script generates a different document to drop - cloneFieldFilter?: List<string>; // fields not to copy when the document is clonedclipboard?: Doc; - filterBoolean?: string; - useCors?: boolean; - icon?: string; - target?: Doc; // available for use in scripts as the primary target document - sourcePanel?: Doc; // panel to display in 'targetContainer' as the result of a button onClick script - targetContainer?: Doc; // document whose proto will be set to 'panel' as the result of a onClick click script - searchFileTypes?: List<string>; // file types allowed in a search query - strokeWidth?: number; - freezeChildren?: string; // whether children are now allowed to be added and or removed from a collection - treeViewHideTitle?: boolean; // whether to hide the top document title of a tree view - treeViewHideUnrendered?: boolean; // tells tree view not to display documents that have an 'unrendered' tag unless they also have a treeViewFieldKey tag (presBox) - treeViewHideHeaderIfTemplate?: boolean; // whether to hide the header for a document in a tree view only if a childLayoutTemplate is provided (presBox) - treeViewHideHeader?: boolean; // whether to hide the header for a document in a tree view - treeViewHideHeaderFields?: boolean; // whether to hide the drop down options for tree view items. + target?: Doc; // available for use in scripts. used to provide a document parameter to the script (Note, this is a convenience entry since any field could be used for parameterizing a script) + + treeViewHideTitle?: BOOLt = new BoolInfo('whether to hide the top document title of a tree view'); + treeViewHideUnrendered?: BOOLt = new BoolInfo("tells tree view not to display documents that have an 'layout_unrendered' tag unless they also have a treeViewFieldKey tag (presBox)"); + treeViewHideHeaderIfTemplate?: BOOLt = new BoolInfo('whether to hide the header for a document in a tree view only if a childLayoutTemplate is provided (presBox)'); + treeViewHideHeader?: BOOLt = new BoolInfo('whether to hide the header for a document in a tree view'); + treeViewHideHeaderFields?: BOOLt = new BoolInfo('whether to hide the drop down options for tree view items.'); treeViewChildDoubleClick?: ScriptField; // - // Action Button - buttonMenu?: boolean; // whether a action button should be displayed - buttonMenuDoc?: Doc; - explainer?: string; - - treeViewOpenIsTransient?: boolean; // ignores the treeViewOpen Doc flag, allowing a treeViewItem's expand/collapse state to be independent of other views of the same document in the same or any other tree view - _treeViewOpen?: boolean; // whether this document is expanded in a tree view (note: need _ and regular versions since this can be specified for both proto and layout docs) - treeViewOpen?: boolean; // whether this document is expanded in a tree view + treeViewOpenIsTransient?: BOOLt = new BoolInfo("ignores the treeViewOpen Doc flag, allowing a treeViewItem's expand/collapse state to be independent of other views of the same document in the same or any other tree view"); + treeViewOpen?: BOOLt = new BoolInfo('whether this document is expanded in a tree view'); treeViewExpandedView?: string; // which field/thing is displayed when this item is opened in tree view - treeViewExpandedViewLock?: boolean; // whether the expanded view can be changed + treeViewExpandedViewLock?: BOOLt = new BoolInfo('whether the expanded view can be changed'); treeViewChecked?: ScriptField; // script to call when a tree view checkbox is checked - treeViewTruncateTitleWidth?: number; - treeViewHasOverlay?: boolean; // whether the treeview has an overlay for freeform annotations + treeViewTruncateTitleWidth?: NUMt = new NumInfo('maximum width of a treew view title before truncation'); + treeViewHasOverlay?: BOOLt = new BoolInfo('whether the treeview has an overlay for freeform annotations'); treeViewType?: string; // whether treeview is a Slide, file system, or (default) collection hierarchy - sidebarColor?: string; // background color of text sidebar - sidebarViewType?: string; // collection type of text sidebar - docMaxAutoHeight?: number; // maximum height for newly created (eg, from pasting) text documents + treeViewFreezeChildren?: STRt = new StrInfo('set (add, remove, add|remove) to disable adding, removing or both from collection'); + + sidebar_color?: string; // background color of text sidebar + sidebar_collectionType?: string; // collection type of text sidebar + text?: string; - textTransform?: string; // is linear view expanded - letterSpacing?: string; // is linear view expanded + textTransform?: string; + letterSpacing?: string; iconTemplate?: string; // name of icon template style - selectedIndex?: number; // which item in a linear view has been selected using the "thumb doc" ui + selectedIndex?: NUMt = new NumInfo("which item in a linear view has been selected using the 'thumb doc' ui"); + + fieldValues?: List<any>; // possible values a field can have (used by FieldInfo's only) + fieldType?: string; // display type of a field, e.g. string, number, enumeration (used by FieldInfo's only) + clipboard?: Doc; - searchQuery?: string; // for quersyBox - useLinkSmallAnchor?: boolean; // whether links to this document should use a miniature linkAnchorBox - border?: string; //for searchbox hoverBackgroundColor?: string; // background color of a label when hovered - linkRelationshipList?: List<string>; // for storing different link relationships (when set by user in the link editor) - linkRelationshipSizes?: List<number>; //stores number of links contained in each relationship - linkColorList?: List<string>; // colors of links corresponding to specific link relationships + userColor?: STRt = new StrInfo('color associated with a Dash user (seen in header fields of shared documents)'); } export namespace Docs { export let newAccount: boolean = false; @@ -427,7 +434,7 @@ export namespace Docs { _yMargin: 10, nativeDimModifiable: true, nativeHeightUnfrozen: true, - forceReflow: true, + layout_forceReflow: true, defaultDoubleClick: 'ignore', }, }, @@ -450,42 +457,42 @@ export namespace Docs { DocumentType.IMG, { layout: { view: ImageBox, dataField: defaultDataKey }, - options: {}, + options: { freeform: '' }, }, ], [ DocumentType.WEB, { layout: { view: WebBox, dataField: defaultDataKey }, - options: { _height: 300, _fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true, waitForDoubleClickToClick: 'always' }, + options: { _height: 300, _layout_fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true, waitForDoubleClickToClick: 'always' }, }, ], [ DocumentType.COL, { layout: { view: CollectionView, dataField: defaultDataKey }, - options: { _fitWidth: true, _panX: 0, _panY: 0, _viewScale: 1 }, + options: { _layout_fitWidth: true, freeform: '', _freeform_panX: 0, _freeform_panY: 0, _freeform_scale: 1 }, }, ], [ DocumentType.KVP, { layout: { view: KeyValueBox, dataField: defaultDataKey }, - options: { _fitWidth: true, _height: 150 }, + options: { _layout_fitWidth: true, _height: 150 }, }, ], [ DocumentType.VID, { layout: { view: VideoBox, dataField: defaultDataKey }, - options: { _currentTimecode: 0 }, + options: { _layout_currentTimecode: 0 }, }, ], [ DocumentType.AUDIO, { layout: { view: AudioBox, dataField: defaultDataKey }, - options: { _height: 100, fitWidth: true, forceReflow: true, nativeDimModifiable: true }, + options: { _height: 100, layout_fitWidth: true, layout_forceReflow: true, nativeDimModifiable: true }, }, ], [ @@ -499,7 +506,7 @@ export namespace Docs { DocumentType.PDF, { layout: { view: PDFBox, dataField: defaultDataKey }, - options: { _curPage: 1, _fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true }, + options: { _layout_curPage: 1, _layout_fitWidth: true, nativeDimModifiable: true, nativeHeightUnfrozen: true }, }, ], [ @@ -519,14 +526,15 @@ export namespace Docs { [ DocumentType.LINK, { - layout: { view: LinkBox, dataField: defaultDataKey }, + layout: { view: LinkBox, dataField: 'link' }, options: { childDontRegisterViews: true, onClick: FollowLinkScript(), - hideLinkAnchors: true, + layout_hideLinkAnchors: true, _height: 150, - description: '', - showCaption: 'description', + link: '', + link_description: '', + layout_showCaption: 'link_description', backgroundColor: 'lightblue', // lightblue is default color for linking dot and link documents text comment area _removeDropProperties: new List(['onClick']), }, @@ -537,7 +545,7 @@ export namespace Docs { { data: new List<Doc>(), layout: { view: EmptyBox, dataField: defaultDataKey }, - options: { childDropAction: 'alias', title: 'Global Link Database' }, + options: { childDropAction: 'embed', title: 'Global Link Database' }, }, ], [ @@ -545,7 +553,7 @@ export namespace Docs { { data: new List<Doc>(), layout: { view: EmptyBox, dataField: defaultDataKey }, - options: { childDropAction: 'alias', title: 'Global Script Database' }, + options: { childDropAction: 'embed', title: 'Global Script Database' }, }, ], [ @@ -571,8 +579,8 @@ export namespace Docs { [ DocumentType.EQUATION, { - layout: { view: EquationBox, dataField: defaultDataKey }, - options: { nativeDimModifiable: true, fontSize: '14px', hideResizeHandles: true, hideDecorationTitle: true }, + layout: { view: EquationBox, dataField: 'text' }, + options: { nativeDimModifiable: true, fontSize: '14px', layout_hideResizeHandles: true, layout_hideDecorationTitle: true }, }, ], [ @@ -600,14 +608,14 @@ export namespace Docs { DocumentType.PRES, { layout: { view: PresBox, dataField: defaultDataKey }, - options: { defaultDoubleClick: 'ignore', hideLinkAnchors: true }, + options: { defaultDoubleClick: 'ignore', layout_hideLinkAnchors: true }, }, ], [ DocumentType.FONTICON, { layout: { view: FontIconBox, dataField: 'icon' }, - options: { defaultDoubleClick: 'ignore', waitForDoubleClickToClick: 'never', enableDragWhenActive: true, hideLinkButton: true, _width: 40, _height: 40 }, + options: { defaultDoubleClick: 'ignore', waitForDoubleClickToClick: 'never', enableDragWhenActive: true, layout_hideLinkButton: true, _width: 40, _height: 40 }, }, ], [ @@ -621,21 +629,21 @@ export namespace Docs { DocumentType.PRESELEMENT, { layout: { view: PresElementBox, dataField: defaultDataKey }, - options: { title: 'pres element template', _fitWidth: true, _xMargin: 0, isTemplateDoc: true, isTemplateForField: 'data' }, + options: { title: 'pres element template', _layout_fitWidth: true, _xMargin: 0, isTemplateDoc: true, isTemplateForField: 'data' }, }, ], [ - DocumentType.MARKER, + DocumentType.CONFIG, { layout: { view: CollectionView, dataField: defaultDataKey }, - options: { hideLinkButton: true, pointerEvents: 'none' }, + options: { layout_hideLinkButton: true, pointerEvents: 'none', layout_unrendered: true }, }, ], [ DocumentType.INK, { // NOTE: this is unused!! ink fields are filled in directly within the InkDocument() method - layout: { view: InkingStroke, dataField: defaultDataKey }, + layout: { view: InkingStroke, dataField: 'stroke' }, options: {}, }, ], @@ -643,14 +651,15 @@ export namespace Docs { DocumentType.SCREENSHOT, { layout: { view: ScreenshotBox, dataField: defaultDataKey }, - options: {}, + options: { nativeDimModifiable: true, nativeHeightUnfrozen: true }, }, ], [ DocumentType.COMPARISON, { + data: '', layout: { view: ComparisonBox, dataField: defaultDataKey }, - options: { clipWidth: 50, nativeDimModifiable: true, backgroundColor: 'gray', targetDropAction: 'alias' }, + options: { backgroundColor: 'gray', targetDropAction: 'embed' }, }, ], [ @@ -658,7 +667,7 @@ export namespace Docs { { data: new List<Doc>(), layout: { view: EmptyBox, dataField: defaultDataKey }, - options: { childDropAction: 'alias', title: 'Global Group Database' }, + options: { childDropAction: 'embed', title: 'Global Group Database' }, }, ], [ @@ -672,14 +681,22 @@ export namespace Docs { DocumentType.DATAVIZ, { layout: { view: DataVizBox, dataField: defaultDataKey }, - options: { _fitWidth: true, nativeDimModifiable: true }, + options: { _layout_fitWidth: true, nativeDimModifiable: true }, }, ], [ DocumentType.LOADING, { layout: { view: LoadingBox, dataField: '' }, - options: { _fitWidth: true, _fitHeight: true, nativeDimModifiable: true }, + options: { _layout_fitWidth: true, _fitHeight: true, nativeDimModifiable: true }, + }, + ], + [ + DocumentType.SIMULATION, + { + data: '', + layout: { view: PhysicsSimulationBox, dataField: defaultDataKey, _width: 1000, _height: 800 }, + options: { _height: 100, layout_forceReflow: true, nativeHeightUnfrozen: true, mass1: '', mass2: '', nativeDimModifiable: true, position: '', acceleration: '', pendulum: '', spring: '', wedge: '', simulation: '', review: '' }, }, ], ]); @@ -687,7 +704,7 @@ export namespace Docs { const suffix = 'Proto'; /** - * This function loads or initializes the prototype for each docment type. + * This function loads or initializes the prototype for each document type. * * This is an asynchronous function because it has to attempt * to fetch the prototype documents from the server. @@ -773,11 +790,11 @@ export namespace Docs { // synthesize the default options, the type and title from computed values and // whatever options pertain to this specific prototype const options: DocumentOptions = { - system: true, - _layoutKey: 'layout', + isSystem: true, + _layout_fieldKey: 'layout', title, type, - baseProto: true, + isBaseProto: true, x: 0, y: 0, _width: 300, @@ -818,24 +835,24 @@ export namespace Docs { * main document. */ function InstanceFromProto(proto: Doc, data: Field | undefined, options: DocumentOptions, delegId?: string, fieldKey: string = 'data', protoId?: string, placeholderDoc?: Doc) { - const viewKeys = ['x', 'y', 'system']; // keys that should be addded to the view document even though they don't begin with an "_" + const viewKeys = ['x', 'y', 'isSystem']; // keys that should be addded to the view document even though they don't begin with an "_" const { omit: dataProps, extract: viewProps } = OmitKeys(options, viewKeys, '^_'); // dataProps['acl-Override'] = SharingPermissions.Unset; dataProps['acl-Public'] = options['acl-Public'] ? options['acl-Public'] : Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment; - dataProps.system = viewProps.system; - dataProps.isPrototype = true; + dataProps.isSystem = viewProps.isSystem; + dataProps.isDataDoc = true; dataProps.author = Doc.CurrentUserEmail; - dataProps.creationDate = new DateField(); + dataProps.author_date = new DateField(); if (fieldKey) { - dataProps[`${fieldKey}-lastModified`] = new DateField(); + dataProps[`${fieldKey}_modificationDate`] = new DateField(); dataProps[fieldKey] = data; // so that the list of annotations is already initialised, prevents issues in addonly. // without this, if a doc has no annotations but the user has AddOnly privileges, they won't be able to add an annotation because they would have needed to create the field's list which they don't have permissions to do. - dataProps[fieldKey + '-annotations'] = new List<Doc>(); - dataProps[fieldKey + '-sidebar'] = new List<Doc>(); + dataProps[fieldKey + '_annotations'] = new List<Doc>(); + dataProps[fieldKey + '_sidebar'] = new List<Doc>(); } // users placeholderDoc as proto if it exists @@ -859,7 +876,7 @@ export namespace Docs { viewDoc = Doc.assign(Doc.MakeDelegate(dataDoc, delegId), viewFirstProps, true, true); } Doc.assign(viewDoc, viewProps, true, true); - if (![DocumentType.LINK, DocumentType.MARKER, DocumentType.LABEL].includes(viewDoc.type as any)) { + if (![DocumentType.LINK, DocumentType.CONFIG, DocumentType.LABEL].includes(viewDoc.type as any)) { DocUtils.MakeLinkToActiveAudio(() => viewDoc); } @@ -901,7 +918,7 @@ export namespace Docs { } export function ComparisonDocument(options: DocumentOptions = { title: 'Comparison Box' }) { - return InstanceFromProto(Prototypes.get(DocumentType.COMPARISON), '', options); + return InstanceFromProto(Prototypes.get(DocumentType.COMPARISON), undefined, options); } export function AudioDocument(url: string, options: DocumentOptions = {}, overwriteDoc?: Doc) { @@ -954,11 +971,12 @@ export namespace Docs { Prototypes.get(DocumentType.LINK), undefined, { - anchor1: source, - anchor2: target, + link_anchor_1: source, + link_anchor_2: target, ...options, }, - id + id, + 'link' ); LinkManager.Instance.addLink(linkDoc); @@ -966,47 +984,33 @@ export namespace Docs { return linkDoc; } - export function InkDocument( - color: string, - tool: string, - strokeWidth: number, - strokeBezier: string, - fillColor: string, - arrowStart: string, - arrowEnd: string, - dash: string, - points: PointData[], - isInkMask: boolean, - options: DocumentOptions = {} - ) { + export function InkDocument(color: string, strokeWidth: number, stroke_bezier: string, fillColor: string, arrowStart: string, arrowEnd: string, dash: string, points: PointData[], isInkMask: boolean, options: DocumentOptions = {}) { const I = new Doc(); I[Initializing] = true; - I.isInkMask = isInkMask; I.type = DocumentType.INK; - I.layout = InkingStroke.LayoutString('data'); + I.layout = InkingStroke.LayoutString('stroke'); + I.layout_fitWidth = true; + I.layout_hideDecorationTitle = true; // don't show title when selected + // I.layout_hideOpenButton = true; // don't show open full screen button when selected I.color = color; - I.hideDecorationTitle = true; // don't show title when selected - // I.hideOpenButton = true; // don't show open full screen button when selected I.fillColor = fillColor; - I.strokeWidth = strokeWidth; - I.strokeBezier = strokeBezier; - I.strokeStartMarker = arrowStart; - I.strokeEndMarker = arrowEnd; - I.strokeDash = dash; - I.tool = tool; - I.fitWidth = true; - I['text-align'] = 'center'; + I.stroke = new InkField(points); + I.stroke_width = strokeWidth; + I.stroke_bezier = stroke_bezier; + I.stroke_startMarker = arrowStart; + I.stroke_endMarker = arrowEnd; + I.stroke_dash = dash; + I.stroke_isInkMask = isInkMask; + I.text_align = 'center'; I.title = 'ink'; I.x = options.x as number; I.y = options.y as number; I._width = options._width as number; I._height = options._height as number; - I._fontFamily = 'cursive'; I.author = Doc.CurrentUserEmail; I.rotation = 0; - I.data = new InkField(points); I.defaultDoubleClick = 'click'; - I.creationDate = new DateField(); + I.author_date = new DateField(); I['acl-Public'] = Doc.defaultAclPrivate ? SharingPermissions.None : SharingPermissions.Augment; //I['acl-Override'] = SharingPermissions.Unset; I[Initializing] = false; @@ -1039,81 +1043,97 @@ export namespace Docs { return InstanceFromProto(Prototypes.get(DocumentType.MAP), new List(documents), options); } - export function MapMarkerDocument(lat: number, lng: number, infoWindowOpen: boolean, documents: Array<Doc>, options: DocumentOptions, id?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.MARKER), new List(documents), { lat, lng, infoWindowOpen, ...options }, id); - } - // shouldn't ever need to create a KVP document-- instead set the LayoutTemplateString to be a KeyValueBox for the DocumentView (see addDocTab in TabDocView) // export function KVPDocument(document: Doc, options: DocumentOptions = {}) { // return InstanceFromProto(Prototypes.get(DocumentType.KVP), document, { title: document.title + '.kvp', ...options }); // } export function FreeformDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) { - const inst = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _xPadding: 20, _yPadding: 20, ...options, _viewType: CollectionViewType.Freeform }, id); - documents.map(d => (d.context = inst)); + const inst = InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _xPadding: 20, _yPadding: 20, ...options, _type_collection: CollectionViewType.Freeform }, id); + documents.forEach(d => (d.embedContainer = inst)); return inst; } - export function WebanchorDocument(url?: string, options: DocumentOptions = {}, id?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.MARKER), url, options, id); + export function WebConfigDocument(options: DocumentOptions = {}, id?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.CONFIG), undefined, options, id); } - - export function CollectionAnchorDocument(options: DocumentOptions = {}, id?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.MARKER), options?.data, options, id); + export function CollectionConfigDocument(options: DocumentOptions = {}, id?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.CONFIG), options?.data, options, id); } - export function TextanchorDocument(options: DocumentOptions = {}, id?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.MARKER), options?.data, options, id); + export function PdfConfigDocument(options: DocumentOptions = {}, id?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.CONFIG), options?.data, options, id); } - - export function ImageanchorDocument(options: DocumentOptions = {}, id?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.MARKER), options?.data, options, id); + export function TextConfigDocument(options: DocumentOptions = {}, id?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.CONFIG), options?.data, options, id); } - - export function InkAnchorDocument(options: DocumentOptions, id?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.MARKER), options?.data, options, id); + export function FunctionPlotConfigDocument(options: DocumentOptions = {}, id?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.CONFIG), options?.data, options, id); + } + export function DataVizConfigDocument(options: DocumentOptions = {}, id?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.CONFIG), options?.data, options, id); + } + export function LineChartConfigDocument(options: DocumentOptions = {}, id?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.CONFIG), options?.data, options, id); + } + export function ComparisonConfigDocument(options: DocumentOptions = {}, id?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.CONFIG), options?.data, options, id); + } + export function ImageConfigDocument(options: DocumentOptions = {}, id?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.CONFIG), options?.data, options, id); + } + export function InkConfigDocument(options: DocumentOptions, id?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.CONFIG), options?.data, options, id); } - export function HTMLAnchorDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.MARKER), new List(documents), options, id); + export function HTMLMarkerDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Freeform }, id); + } + export function MapMarkerDocument(lat: number, lng: number, infoWindowOpen: boolean, documents: Array<Doc>, options: DocumentOptions, id?: string) { + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { lat, lng, infoWindowOpen, ...options, _type_collection: CollectionViewType.Freeform }, id); } export function PileDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _overflow: 'visible', enableDragWhenActive: true, _forceActive: true, _noAutoscroll: true, ...options, _viewType: CollectionViewType.Pile }, id); + return InstanceFromProto( + Prototypes.get(DocumentType.COL), + new List(documents), + { enableDragWhenActive: true, _forceActive: true, _freeform_noZoom: true, _freeform_noAutoPan: true, ...options, _type_collection: CollectionViewType.Pile }, + id + ); } export function LinearDocument(documents: Array<Doc>, options: DocumentOptions, id?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Linear }, id); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Linear }, id); } export function MapCollectionDocument(documents: Array<Doc>, options: DocumentOptions = {}) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Map }); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Map }); } export function CarouselDocument(documents: Array<Doc>, options: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Carousel }); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Carousel }); } export function Carousel3DDocument(documents: Array<Doc>, options: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Carousel3D }); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Carousel3D }); } export function SchemaDocument(schemaHeaders: SchemaHeaderField[], documents: Array<Doc>, options: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { schemaHeaders: new List(schemaHeaders), ...options, _viewType: CollectionViewType.Schema }); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { schemaHeaders: new List(schemaHeaders), ...options, _type_collection: CollectionViewType.Schema }); } export function TreeDocument(documents: Array<Doc>, options: DocumentOptions, id?: string, protoId?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _xMargin: 5, _yMargin: 5, ...options, _viewType: CollectionViewType.Tree }, id, undefined, protoId); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { _xMargin: 5, _yMargin: 5, ...options, _type_collection: CollectionViewType.Tree }, id, undefined, protoId); } export function StackingDocument(documents: Array<Doc>, options: DocumentOptions, id?: string, protoId?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Stacking }, id, undefined, protoId); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Stacking }, id, undefined, protoId); } export function NoteTakingDocument(documents: Array<Doc>, options: DocumentOptions, id?: string, protoId?: string) { return InstanceFromProto( Prototypes.get(DocumentType.COL), new List(documents), - { columnHeaders: new List<SchemaHeaderField>([new SchemaHeaderField('Untitled')]), ...options, _viewType: CollectionViewType.NoteTaking }, + { columnHeaders: new List<SchemaHeaderField>([new SchemaHeaderField('Untitled')]), ...options, _type_collection: CollectionViewType.NoteTaking }, id, undefined, protoId @@ -1121,14 +1141,14 @@ export namespace Docs { } export function MulticolumnDocument(documents: Array<Doc>, options: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Multicolumn }); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Multicolumn }); } export function MultirowDocument(documents: Array<Doc>, options: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Multirow }); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Multirow }); } export function MasonryDocument(documents: Array<Doc>, options: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _viewType: CollectionViewType.Masonry }); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { ...options, _type_collection: CollectionViewType.Masonry }); } export function LabelDocument(options?: DocumentOptions) { @@ -1136,7 +1156,7 @@ export namespace Docs { } export function EquationDocument(options?: DocumentOptions) { - return InstanceFromProto(Prototypes.get(DocumentType.EQUATION), undefined, { ...(options || {}) }); + return InstanceFromProto(Prototypes.get(DocumentType.EQUATION), undefined, { ...(options || {}) }, undefined, 'text'); } export function FunctionPlotDocument(documents: Array<Doc>, options?: DocumentOptions) { @@ -1167,7 +1187,7 @@ export namespace Docs { } export function DockDocument(documents: Array<Doc>, config: string, options: DocumentOptions, id?: string) { - return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { freezeChildren: 'remove|add', ...options, viewType: CollectionViewType.Docking, _viewType: CollectionViewType.Docking, dockingConfig: config }, id); + return InstanceFromProto(Prototypes.get(DocumentType.COL), new List(documents), { treeViewFreezeChildren: 'remove|add', ...options, _type_collection: CollectionViewType.Docking, dockingConfig: config }, id); } export function DirectoryImportDocument(options: DocumentOptions = {}) { @@ -1189,35 +1209,41 @@ export namespace Docs { }, ], }; - return DockDocument( + const doc = DockDocument( configs.map(c => c.doc), JSON.stringify(layoutConfig), options, id ); + configs.map(c => (c.doc.embedContainer = doc)); + return doc; } export function DelegateDocument(proto: Doc, options: DocumentOptions = {}) { return InstanceFromProto(proto, undefined, options); } + + export function SimulationDocument(options?: DocumentOptions) { + return InstanceFromProto(Prototypes.get(DocumentType.SIMULATION), undefined, { ...(options || {}) }); + } } } export namespace DocUtils { /** * @param docs - * @param docFilters - * @param docRangeFilters + * @param childFilters + * @param childFiltersByRanges * @param parentCollection - * Given a list of docs and docFilters, @returns the list of Docs that match those filters + * Given a list of docs and childFilters, @returns the list of Docs that match those filters */ - export function FilterDocs(childDocs: Doc[], docFilters: string[], docRangeFilters: string[], parentCollection?: Doc) { - if (!docFilters?.length && !docRangeFilters?.length) { + export function FilterDocs(childDocs: Doc[], childFilters: string[], childFiltersByRanges: string[], parentCollection?: Doc) { + if (!childFilters?.length && !childFiltersByRanges?.length) { return childDocs.filter(d => !d.cookies); // remove documents that need a cookie if there are no filters to provide one } const filterFacets: { [key: string]: { [value: string]: string } } = {}; // maps each filter key to an object with value=>modifier fields - docFilters.forEach(filter => { + childFilters.forEach(filter => { const fields = filter.split(':'); const key = fields[0]; const value = fields[1]; @@ -1228,7 +1254,7 @@ export namespace DocUtils { filterFacets[key][value] = modifiers; }); - const filteredDocs = docFilters.length + const filteredDocs = childFilters.length ? childDocs.filter(d => { if (d.z) return true; // if the document needs a cookie but no filter provides the cookie, then the document does not pass the filter @@ -1263,7 +1289,7 @@ export namespace DocUtils { ? true : matches.some(value => { if (facetKey.startsWith('*')) { - // fields starting with a '*' are used to match families of related fields. ie, *lastModified will match text-lastModified, data-lastModified, etc + // fields starting with a '*' are used to match families of related fields. ie, *modificationDate will match text_modificationDate, data_modificationDate, etc const allKeys = Array.from(Object.keys(d)); allKeys.push(...Object.keys(Doc.GetProto(d))); const keys = allKeys.filter(key => key.includes(facetKey.substring(1))); @@ -1272,7 +1298,7 @@ export namespace DocUtils { return Field.toString(d[facetKey] as Field).includes(value); }); // if we're ORing them together, the default return is false, and we return true for a doc if it satisfies any one set of criteria - if (parentCollection?.filterBoolean === 'OR') { + if (parentCollection?.childFilters_boolean === 'OR') { if (satisfiesUnsetsFacets && satisfiesExistsFacets && satisfiesCheckFacets && !failsNotEqualFacets && satisfiesMatchFacets) return true; } // if we're ANDing them together, the default return is true, and we return false for a doc if it doesn't satisfy any set of criteria @@ -1280,14 +1306,14 @@ export namespace DocUtils { if (!satisfiesUnsetsFacets || !satisfiesExistsFacets || !satisfiesCheckFacets || failsNotEqualFacets || (matches.length && !satisfiesMatchFacets)) return false; } } - return (parentCollection?.currentFilter as Doc)?.filterBoolean === 'OR' ? false : true; + return (parentCollection?.currentFilter as Doc)?.childFilters_boolean === 'OR' ? false : true; }) : childDocs; const rangeFilteredDocs = filteredDocs.filter(d => { - for (let i = 0; i < docRangeFilters.length; i += 3) { - const key = docRangeFilters[i]; - const min = Number(docRangeFilters[i + 1]); - const max = Number(docRangeFilters[i + 2]); + for (let i = 0; i < childFiltersByRanges.length; i += 3) { + const key = childFiltersByRanges[i]; + const min = Number(childFiltersByRanges[i + 1]); + const max = Number(childFiltersByRanges[i + 2]); const val = typeof d[key] === 'string' ? (Number(StrCast(d[key])).toString() === StrCast(d[key]) ? Number(StrCast(d[key])) : undefined) : Cast(d[key], 'number', null); if (val === undefined) { //console.log("Should 'undefined' pass range filter or not?") @@ -1304,12 +1330,12 @@ export namespace DocUtils { broadcastEvent && runInAction(() => (DocumentManager.Instance.RecordingEvent = DocumentManager.Instance.RecordingEvent + 1)); return DocUtils.ActiveRecordings.map(audio => { const sourceDoc = getSourceDoc(); - return sourceDoc && DocUtils.MakeLink(sourceDoc, audio.getAnchor(true) || audio.props.Document, { linkDisplay: false, linkRelationship: 'recording annotation:linked recording', description: 'recording timeline' }); + return sourceDoc && DocUtils.MakeLink(sourceDoc, audio.getAnchor(true) || audio.props.Document, { link_displayLine: false, link_relationship: 'recording annotation:linked recording', link_description: 'recording timeline' }); }); } - export function MakeLink(source: Doc, target: Doc, linkSettings: { linkRelationship?: string; description?: string; linkDisplay?: boolean }, id?: string, showPopup?: number[]) { - if (!linkSettings.linkRelationship) linkSettings.linkRelationship = target.type === DocumentType.RTF ? 'Commentary:Comments On' : 'link'; + export function MakeLink(source: Doc, target: Doc, linkSettings: { link_relationship?: string; link_description?: string; link_displayLine?: boolean }, id?: string, showPopup?: number[]) { + if (!linkSettings.link_relationship) linkSettings.link_relationship = target.type === DocumentType.RTF ? 'Commentary:Comments On' : 'link'; const sv = DocumentManager.Instance.getDocumentView(source); if (target.doc === Doc.UserDoc()) return undefined; @@ -1349,17 +1375,17 @@ export namespace DocUtils { source, target, { - title: ComputedField.MakeFunction('generateLinkTitle(self)') as any, - 'anchor1-useLinkSmallAnchor': source.useLinkSmallAnchor ? true : undefined, - 'anchor2-useLinkSmallAnchor': target.useLinkSmallAnchor ? true : undefined, 'acl-Public': SharingPermissions.Augment, '_acl-Public': SharingPermissions.Augment, - linkDisplay: linkSettings.linkDisplay, - _linkAutoMove: true, - linkRelationship: linkSettings.linkRelationship, - _showCaption: 'description', - _showTitle: 'linkRelationship', - description: linkSettings.description, + title: ComputedField.MakeFunction('generateLinkTitle(self)') as any, + link_anchor_1_useSmallAnchor: source.useSmallAnchor ? true : undefined, + link_anchor_2_useSmallAnchor: target.useSmallAnchor ? true : undefined, + link_displayLine: linkSettings.link_displayLine, + link_relationship: linkSettings.link_relationship, + link_description: linkSettings.link_description, + link_autoMoveAnchors: true, + _layout_showCaption: 'link_description', + _layout_showTitle: 'link_relationship', }, id ), @@ -1443,13 +1469,13 @@ export namespace DocUtils { created = Docs.Create.AudioDocument(field.url.href, resolved); created.layout = AudioBox.LayoutString(fieldKey); } else if (field instanceof InkField) { - created = Docs.Create.InkDocument(ActiveInkColor(), Doc.ActiveTool, ActiveInkWidth(), ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), field.inkData, ActiveIsInkMask(), resolved); + created = Docs.Create.InkDocument(ActiveInkColor(), ActiveInkWidth(), ActiveInkBezierApprox(), ActiveFillColor(), ActiveArrowStart(), ActiveArrowEnd(), ActiveDash(), field.inkData, ActiveIsInkMask(), resolved); created.layout = InkingStroke.LayoutString(fieldKey); } else if (field instanceof List && field[0] instanceof Doc) { created = Docs.Create.StackingDocument(DocListCast(field), resolved); created.layout = CollectionView.LayoutString(fieldKey); } else { - created = Docs.Create.TextDocument('', { ...{ _width: 200, _height: 25, _autoHeight: true }, ...resolved }); + created = Docs.Create.TextDocument('', { ...{ _width: 200, _height: 25, _layout_autoHeight: true }, ...resolved }); created.layout = FormattedTextBox.LayoutString(fieldKey); } if (created) { @@ -1503,12 +1529,12 @@ export namespace DocUtils { const id = s[s.length - 1]; return DocServer.GetRefField(id).then(field => { if (field instanceof Doc) { - const alias = Doc.MakeAlias(field); - alias.x = (options.x as number) || 0; - alias.y = (options.y as number) || 0; - alias._width = (options._width as number) || 300; - alias._height = (options._height as number) || (options._width as number) || 300; - return alias; + const embedding = Doc.MakeEmbedding(field); + embedding.x = (options.x as number) || 0; + embedding.y = (options.y as number) || 0; + embedding._width = (options._width as number) || 300; + embedding._height = (options._height as number) || (options._width as number) || 300; + return embedding; } return undefined; }); @@ -1526,21 +1552,21 @@ export namespace DocUtils { description: 'Quick Notes', subitems: DocListCast((Doc.UserDoc()['template-notes'] as Doc).data).map((note, i) => ({ description: ':' + StrCast(note.title), - event: undoBatch((args: { x: number; y: number }) => { + event: undoable((args: { x: number; y: number }) => { const textDoc = Docs.Create.TextDocument('', { _width: 200, x, y, - _autoHeight: note._autoHeight !== false, - title: StrCast(note.title) + '#' + (note.aliasCount = NumCast(note.aliasCount) + 1), + _layout_autoHeight: note._layout_autoHeight !== false, + title: StrCast(note.title) + '#' + (note.embeddingCount = NumCast(note.embeddingCount) + 1), }); - textDoc.layoutKey = 'layout_' + note.title; - textDoc[textDoc.layoutKey] = note; + textDoc.layout_fieldKey = 'layout_' + note.title; + textDoc[textDoc.layout_fieldKey] = note; if (pivotField) { textDoc[pivotField] = pivotValue; } docTextAdder(textDoc); - }), + }, 'create quick note'), icon: StrCast(note.icon) as IconProp, })) as ContextMenuProps[], icon: 'sticky-note', @@ -1551,7 +1577,7 @@ export namespace DocUtils { .filter(doc => doc && doc !== Doc.UserDoc().emptyTrail && doc !== Doc.UserDoc().emptyDataViz) .map((dragDoc, i) => ({ description: ':' + StrCast(dragDoc.title).replace('Untitled ', ''), - event: undoBatch((args: { x: number; y: number }) => { + event: undoable((args: { x: number; y: number }) => { const newDoc = DocUtils.copyDragFactory(dragDoc); if (newDoc) { newDoc.author = Doc.CurrentUserEmail; @@ -1564,7 +1590,7 @@ export namespace DocUtils { } docAdder?.(newDoc); } - }), + }, StrCast(dragDoc.title)), icon: Doc.toIcon(dragDoc), })) as ContextMenuProps[]; ContextMenu.Instance.addItem({ @@ -1576,7 +1602,7 @@ export namespace DocUtils { export function makeCustomViewClicked(doc: Doc, creator: Opt<(documents: Array<Doc>, options: DocumentOptions, id?: string) => Doc>, templateSignature: string = 'custom', docLayoutTemplate?: Doc) { const batch = UndoManager.StartBatch('makeCustomViewClicked'); runInAction(() => { - doc.layoutKey = 'layout_' + templateSignature; + doc.layout_fieldKey = 'layout_' + templateSignature; createCustomView(doc, creator, templateSignature, docLayoutTemplate); }); batch.end(); @@ -1607,7 +1633,7 @@ export namespace DocUtils { const customName = 'layout_' + templateSignature; const _width = NumCast(doc._width); const _height = NumCast(doc._height); - const options = { title: 'data', backgroundColor: StrCast(doc.backgroundColor), _autoHeight: true, _width, x: -_width / 2, y: -_height / 2, _showSidebar: false }; + const options = { title: 'data', backgroundColor: StrCast(doc.backgroundColor), _layout_autoHeight: true, _width, x: -_width / 2, y: -_height / 2, _layout_showSidebar: false }; if (docLayoutTemplate) { if (docLayoutTemplate !== doc[customName]) { @@ -1638,9 +1664,9 @@ export namespace DocUtils { } } export function iconify(doc: Doc) { - const layoutKey = Cast(doc.layoutKey, 'string', null); + const layout_fieldKey = Cast(doc.layout_fieldKey, 'string', null); DocUtils.makeCustomViewClicked(doc, Docs.Create.StackingDocument, 'icon', undefined); - if (layoutKey && layoutKey !== 'layout' && layoutKey !== 'layout_icon') doc.deiconifyLayout = layoutKey.replace('layout_', ''); + if (layout_fieldKey && layout_fieldKey !== 'layout' && layout_fieldKey !== 'layout_icon') doc.deiconifyLayout = layout_fieldKey.replace('layout_', ''); } export function pileup(docList: Doc[], x?: number, y?: number, size: number = 55, create: boolean = true) { @@ -1680,19 +1706,19 @@ export namespace DocUtils { export function LeavePushpin(doc: Doc, annotationField: string) { if (doc.followLinkToggle) return undefined; - const context = Cast(doc.context, Doc, null) ?? Cast(doc.annotationOn, Doc, null); - const hasContextAnchor = LinkManager.Links(doc).some(l => (l.anchor2 === doc && Cast(l.anchor1, Doc, null)?.annotationOn === context) || (l.anchor1 === doc && Cast(l.anchor2, Doc, null)?.annotationOn === context)); + const context = Cast(doc.embedContainer, Doc, null) ?? Cast(doc.annotationOn, Doc, null); + const hasContextAnchor = LinkManager.Links(doc).some(l => (l.link_anchor_2 === doc && Cast(l.link_anchor_1, Doc, null)?.annotationOn === context) || (l.link_anchor_1 === doc && Cast(l.link_anchor_2, Doc, null)?.annotationOn === context)); if (context && !hasContextAnchor && (context.type === DocumentType.VID || context.type === DocumentType.WEB || context.type === DocumentType.PDF || context.type === DocumentType.IMG)) { const pushpin = Docs.Create.FontIconDocument({ title: 'pushpin', - label: '', + icon_label: '', annotationOn: Cast(doc.annotationOn, Doc, null), followLinkToggle: true, icon: 'map-pin', x: Cast(doc.x, 'number', null), y: Cast(doc.y, 'number', null), backgroundColor: '#ACCEF7', - hideAllLinks: true, + layout_hideAllLinks: true, _width: 15, _height: 15, _xPadding: 0, @@ -1700,7 +1726,7 @@ export namespace DocUtils { _timecodeToShow: Cast(doc._timecodeToShow, 'number', null), }); Doc.AddDocToList(context, annotationField, pushpin); - const pushpinLink = DocUtils.MakeLink(pushpin, doc, { linkRelationship: 'pushpin' }, ''); + const pushpinLink = DocUtils.MakeLink(pushpin, doc, { link_relationship: 'pushpin' }, ''); doc._timecodeToShow = undefined; return pushpin; } @@ -1745,21 +1771,17 @@ export namespace DocUtils { if (doc) { const proto = Doc.GetProto(doc); proto.text = result.rawText; - proto.fileUpload = pathname - .replace(/.*\//, '') - .replace('upload_', '') - .replace(/\.[a-z0-9]*$/, ''); if (Upload.isImageInformation(result)) { const maxNativeDim = Math.min(Math.max(result.nativeHeight, result.nativeWidth), defaultNativeImageDim); const exifRotation = StrCast((result.exifData?.data as any)?.Orientation).toLowerCase(); proto['data-nativeOrientation'] = result.exifData?.data?.image?.Orientation ?? (exifRotation.includes('rotate 90') || exifRotation.includes('rotate 270') ? 5 : undefined); - proto['data-nativeWidth'] = result.nativeWidth < result.nativeHeight ? (maxNativeDim * result.nativeWidth) / result.nativeHeight : maxNativeDim; - proto['data-nativeHeight'] = result.nativeWidth < result.nativeHeight ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight); + proto['data_nativeWidth'] = result.nativeWidth < result.nativeHeight ? (maxNativeDim * result.nativeWidth) / result.nativeHeight : maxNativeDim; + proto['data_nativeHeight'] = result.nativeWidth < result.nativeHeight ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight); if (NumCast(proto['data-nativeOrientation']) >= 5) { - proto['data-nativeHeight'] = result.nativeWidth < result.nativeHeight ? (maxNativeDim * result.nativeWidth) / result.nativeHeight : maxNativeDim; - proto['data-nativeWidth'] = result.nativeWidth < result.nativeHeight ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight); + proto['data_nativeHeight'] = result.nativeWidth < result.nativeHeight ? (maxNativeDim * result.nativeWidth) / result.nativeHeight : maxNativeDim; + proto['data_nativeWidth'] = result.nativeWidth < result.nativeHeight ? maxNativeDim : maxNativeDim / (result.nativeWidth / result.nativeHeight); } - proto.contentSize = result.contentSize; + proto.data_contentSize = result.contentSize; // exif gps data coordinates are stored in DMS (Degrees Minutes Seconds), the following operation converts that to decimal coordinates const latitude = result.exifData?.data?.GPSLatitude; const latitudeDirection = result.exifData?.data?.GPSLatitudeRef; @@ -1771,12 +1793,12 @@ export namespace DocUtils { } } if (Upload.isVideoInformation(result)) { - proto['data-duration'] = result.duration; + proto.data_duration = result.duration; } if (overwriteDoc) { Doc.removeCurrentlyLoading(overwriteDoc); // loading doc icons are just labels. so any icon views of loading docs need to be replaced with the proper icon view. - DocumentManager.Instance.getAllDocumentViews(overwriteDoc).forEach(dv => StrCast(dv.rootDoc.layoutKey) === 'layout_icon' && dv.iconify(() => dv.iconify())); + DocumentManager.Instance.getAllDocumentViews(overwriteDoc).forEach(dv => StrCast(dv.rootDoc.layout_fieldKey) === 'layout_icon' && dv.iconify(() => dv.iconify())); } generatedDocuments.push(doc); } @@ -1787,22 +1809,22 @@ export namespace DocUtils { _xMargin: noMargins ? 0 : undefined, _yMargin: noMargins ? 0 : undefined, annotationOn, - docMaxAutoHeight: maxHeight, + layout_maxAutoHeight: maxHeight, backgroundColor, _width: width || 200, _height: 35, x: x, y: y, - _fitWidth: true, - _autoHeight: true, - _showAltContentUI: BoolCast(Doc.UserDoc().defaultToFlashcards), + _layout_fitWidth: true, + _layout_autoHeight: true, + _layout_enableAltContentUI: BoolCast(Doc.UserDoc().defaultToFlashcards), title, }); const template = Doc.UserDoc().defaultTextLayout; if (template instanceof Doc) { tbox._width = NumCast(template._width); - tbox.layoutKey = 'layout_' + StrCast(template.title); - Doc.GetProto(tbox)[StrCast(tbox.layoutKey)] = template; + tbox.layout_fieldKey = 'layout_' + StrCast(template.title); + Doc.GetProto(tbox)[StrCast(tbox.layout_fieldKey)] = template; } return tbox; } @@ -1824,9 +1846,21 @@ export namespace DocUtils { }); } + /** + * uploadFilesToDocs will take in an array of Files, and creates documents for the + * new files. + * + * @param files an array of files that will be uploaded + * @param options options to use while uploading + * @returns + */ export async function uploadFilesToDocs(files: File[], options: DocumentOptions) { const generatedDocuments: Doc[] = []; - const upfiles = await Networking.UploadFilesToServer(files); + + // These files do not have overwriteDocs, so we do not set the guid and let the client generate one. + const fileNoGuidPairs: Networking.FileGuidPair[] = files.map(file => ({ file })); + + const upfiles = await Networking.UploadFilesToServer(fileNoGuidPairs); for (const { source: { name, type }, result, @@ -1838,7 +1872,8 @@ export namespace DocUtils { export function uploadFileToDoc(file: File, options: DocumentOptions, overwriteDoc: Doc) { const generatedDocuments: Doc[] = []; - Networking.UploadFilesToServer([file]).then(upfiles => { + // Since this file has an overwriteDoc, we can set the client tracking guid to the overwriteDoc's guid. + Networking.UploadFilesToServer([{ file, guid: overwriteDoc[Id] }]).then(upfiles => { const { source: { name, type }, result, @@ -1857,9 +1892,9 @@ export namespace DocUtils { if (!dragFactory) return undefined; const ndoc = dragFactory.isTemplateDoc ? Doc.ApplyTemplate(dragFactory) : Doc.MakeCopy(dragFactory, true); ndoc && Doc.AddFileOrphan(Doc.GetProto(ndoc)); - if (ndoc && dragFactory['dragFactory-count'] !== undefined) { - dragFactory['dragFactory-count'] = NumCast(dragFactory['dragFactory-count']) + 1; - Doc.SetInPlace(ndoc, 'title', ndoc.title + ' ' + NumCast(dragFactory['dragFactory-count']).toString(), true); + if (ndoc && dragFactory['dragFactory_count'] !== undefined) { + dragFactory['dragFactory_count'] = NumCast(dragFactory['dragFactory_count']) + 1; + Doc.SetInPlace(ndoc, 'title', ndoc.title + ' ' + NumCast(dragFactory['dragFactory_count']).toString(), true); } if (ndoc && Doc.ActiveDashboard) inheritParentAcls(Doc.ActiveDashboard, ndoc); @@ -1868,9 +1903,9 @@ export namespace DocUtils { } export function delegateDragFactory(dragFactory: Doc) { const ndoc = Doc.MakeDelegateWithProto(dragFactory); - if (ndoc && dragFactory['dragFactory-count'] !== undefined) { - dragFactory['dragFactory-count'] = NumCast(dragFactory['dragFactory-count']) + 1; - Doc.GetProto(ndoc).title = ndoc.title + ' ' + NumCast(dragFactory['dragFactory-count']).toString(); + if (ndoc && dragFactory['dragFactory_count'] !== undefined) { + dragFactory['dragFactory_count'] = NumCast(dragFactory['dragFactory_count']) + 1; + Doc.GetProto(ndoc).title = ndoc.title + ' ' + NumCast(dragFactory['dragFactory_count']).toString(); } return ndoc; } @@ -1885,11 +1920,8 @@ ScriptingGlobals.add(function makeDelegate(proto: any) { return d; }); ScriptingGlobals.add(function generateLinkTitle(self: Doc) { - const anchor1title = self.anchor1 && self.anchor1 !== self ? Cast(self.anchor1, Doc, null)?.title : '<?>'; - const anchor2title = self.anchor2 && self.anchor2 !== self ? Cast(self.anchor2, Doc, null)?.title : '<?>'; - const relation = self.linkRelationship || 'to'; - return `${anchor1title} (${relation}) ${anchor2title}`; -}); -ScriptingGlobals.add(function openTabAlias(tab: Doc) { - CollectionDockingView.AddSplit(Doc.MakeAlias(tab), OpenWhereMod.right); + const link_anchor_1title = self.link_anchor_1 && self.link_anchor_1 !== self ? Cast(self.link_anchor_1, Doc, null)?.title : '<?>'; + const link_anchor_2title = self.link_anchor_2 && self.link_anchor_2 !== self ? Cast(self.link_anchor_2, Doc, null)?.title : '<?>'; + const relation = self.link_relationship || 'to'; + return `${link_anchor_1title} (${relation}) ${link_anchor_2title}`; }); diff --git a/src/client/documents/Gitlike.ts b/src/client/documents/Gitlike.ts index 5e2baf924..f35f33f65 100644 --- a/src/client/documents/Gitlike.ts +++ b/src/client/documents/Gitlike.ts @@ -9,24 +9,24 @@ // // we don't have individual timestamps for all fields -- this is a problematic design issue. // function GitlikeSynchDocs(bd: Doc, md: Doc) { // const fieldKey = Doc.LayoutFieldKey(md); -// const bdate = DateCast(bd[`${fieldKey}-lastModified`])?.date; -// const mdate = DateCast(md[`${fieldKey}-lastModified`])?.date; +// const bdate = DateCast(bd[`${fieldKey}_modificationDate`])?.date; +// const mdate = DateCast(md[`${fieldKey}_modificationDate`])?.date; // const bdproto = bd && Doc.GetProto(bd); // if (bdate !== mdate && bdate <= mdate) { // if (bdproto && md) { // bdproto[fieldKey] = Field.Copy(md[fieldKey]); -// bdproto[`${fieldKey}-lastModified`] = new DateField(); +// bdproto[`${fieldKey}_modificationDate`] = new DateField(); // } // } -// const bldate = DateCast(bd._lastModified)?.date; -// const mldate = DateCast(md._lastModified)?.date; +// const bldate = DateCast(bd._layout_modificationDate)?.date; +// const mldate = DateCast(md._layout_modificationDate)?.date; // if (bldate === mldate || bldate > mldate) return; // if (bdproto && md) { // bd.x = Field.Copy(md.x); // bd.y = Field.Copy(md.y); // bd.width = Field.Copy(md.width); // bd.height = Field.Copy(md.height); -// bdproto._lastModified = new DateField(); +// bdproto._layout_modificationDate = new DateField(); // } // } @@ -53,10 +53,10 @@ // const newlyBranchedDocs = await Promise.all(newDocsFromMaster?.map(async md => (await Doc.MakeClone(md, false, true, cloneMap)).clone) || []); // newlyBranchedDocs.forEach(nd => { // Doc.AddDocToList(branch, Doc.LayoutFieldKey(branch) + suffix, nd); -// nd.context = branch; +// nd.embedContainer = branch; // }); -// // if a branch doc's corresponding main branch doc doesn't have a context, then it was deleted. -// const remDocsFromMaster = branchMainDocs?.filter(bd => Cast(bd.branchOf, Doc, null) && !Cast(bd.branchOf, Doc, null)?.context); +// // if a branch doc's corresponding main branch doc doesn't have a embedContainer, then it was deleted. +// const remDocsFromMaster = branchMainDocs?.filter(bd => Cast(bd.branchOf, Doc, null) && !Cast(bd.branchOf, Doc, null)?.embedContainer); // // so then remove all the deleted main docs from this branch. // remDocsFromMaster?.forEach(rd => Doc.RemoveDocFromList(branch, Doc.LayoutFieldKey(branch) + suffix, rd)); // } @@ -87,14 +87,14 @@ // branchDocProto.branchOf = masterChild; // the branch child is now a branch of the master child // } // Doc.AddDocToList(master, Doc.LayoutFieldKey(master) + suffix, masterChild); // add the masterChild to master (if it's already there, this is a no-op) -// masterChild.context = master; +// masterChild.embedContainer = master; // GitlikeSynchDocs(masterChild, bd);//Doc.GetProto(masterChild), bd); // })); // const masterChildren = await DocListCastAsync(master[Doc.LayoutFieldKey(master) + suffix]); // masterChildren?.forEach(mc => { // see if any master children // if (!branchChildren?.find(bc => bc.branchOf === mc)) { // are not in the list of children for this branch. // Doc.RemoveDocFromList(master, Doc.LayoutFieldKey(master) + suffix, mc); // if so, delete the master child since the branch has deleted it. -// mc.context = undefined; // NOTE if we merge a branch that didn't do a pull, it will look like the branch deleted documents -- need edit timestamps that prevent merging if branch isn't up-to-date with last edit timestamp +// mc.embedContainer = undefined; // NOTE if we merge a branch that didn't do a pull, it will look like the branch deleted documents -- need edit timestamps that prevent merging if branch isn't up-to-date with last edit timestamp // } // }); // }); @@ -109,8 +109,8 @@ // export async function BranchTask(target: Doc, action: "pull" | "merge") { // const func = action === "pull" ? GitlikePullFromMaster : GitlikeMergeWithMaster; // await func(target, ""); -// await DocListCast(target[Doc.LayoutFieldKey(target)]).forEach(async targetChild => func(targetChild, "-annotations")); -// await DocListCast(target[Doc.LayoutFieldKey(target)]).forEach(async targetChild => func(targetChild, "-sidebar")); +// await DocListCast(target[Doc.LayoutFieldKey(target)]).forEach(async targetChild => func(targetChild, "_annotations")); +// await DocListCast(target[Doc.LayoutFieldKey(target)]).forEach(async targetChild => func(targetChild, "_sidebar")); // } // export async function BranchCreate(target: Doc) { diff --git a/src/client/goldenLayout.js b/src/client/goldenLayout.js index 9cb20d834..843b8bb5f 100644 --- a/src/client/goldenLayout.js +++ b/src/client/goldenLayout.js @@ -4740,7 +4740,7 @@ newstack._$init(); newstack.addChild(this.contentItems[0]); } - correctRowOrCol.addChild(newstack, insertBefore ? 0 : undefined, true); + correctRowOrCol.addChild(newstack, !insertBefore ? 0 : undefined, true); newstack.config[dimension] = 50; contentItem.config[dimension] = 50; correctRowOrCol.callDownwards('setSize'); diff --git a/src/client/util/CaptureManager.tsx b/src/client/util/CaptureManager.tsx index c9fcc84a3..d68761ba7 100644 --- a/src/client/util/CaptureManager.tsx +++ b/src/client/util/CaptureManager.tsx @@ -53,7 +53,7 @@ export class CaptureManager extends React.Component<{}> { order.push( <div className="list-item"> <div className="number">{i}</div> - {StrCast(DocCast(l.anchor1)?.title)} + {StrCast(DocCast(l.link_anchor_1)?.title)} </div> ) ); diff --git a/src/client/util/CurrentUserUtils.ts b/src/client/util/CurrentUserUtils.ts index 80a5f0993..59041862f 100644 --- a/src/client/util/CurrentUserUtils.ts +++ b/src/client/util/CurrentUserUtils.ts @@ -20,10 +20,10 @@ import { TreeViewType } from "../views/collections/CollectionTreeView"; import { DashboardView } from "../views/DashboardView"; import { Colors } from "../views/global/globalEnums"; import { MainView } from "../views/MainView"; -import { ButtonType, NumButtonType } from "../views/nodes/button/FontIconBox"; +import { ButtonType } from "../views/nodes/button/FontIconBox"; import { OpenWhere } from "../views/nodes/DocumentView"; import { OverlayView } from "../views/OverlayView"; -import { DragManager } from "./DragManager"; +import { DragManager, dropActionType } from "./DragManager"; import { MakeTemplate } from "./DropConverter"; import { FollowLinkScript } from "./LinkFollower"; import { LinkManager } from "./LinkManager"; @@ -37,7 +37,6 @@ interface Button { toolTip?: string; icon?: string; btnType?: ButtonType; - numBtnType?: NumButtonType; numBtnMin?: number; numBtnMax?: number; switchToggle?: boolean; @@ -65,11 +64,11 @@ export class CurrentUserUtils { const requiredTypeNameFields:{btnOpts:DocumentOptions, templateOpts:DocumentOptions, template:(opts:DocumentOptions) => Doc}[] = [ { btnOpts: { title: "slide", icon: "address-card" }, - templateOpts: { _width: 400, _height: 300, title: "slideView", _xMargin: 3, _yMargin: 3, system: true }, + templateOpts: { _width: 400, _height: 300, title: "slideView", _xMargin: 3, _yMargin: 3, isSystem: true }, template: (opts:DocumentOptions) => Docs.Create.MultirowDocument( [ - Docs.Create.MulticolumnDocument([], { title: "data", _height: 200, system: true }), - Docs.Create.TextDocument("", { title: "text", _fitWidth:true, _height: 100, system: true, _fontFamily: StrCast(Doc.UserDoc()._fontFamily), _fontSize: StrCast(Doc.UserDoc()._fontSize) }) + Docs.Create.MulticolumnDocument([], { title: "data", _height: 200, isSystem: true }), + Docs.Create.TextDocument("", { title: "text", _layout_fitWidth:true, _height: 100, isSystem: true, _text_fontFamily: StrCast(Doc.UserDoc().fontFamily), _text_fontSize: StrCast(Doc.UserDoc().fontSize) }) ], opts) }, { @@ -97,9 +96,9 @@ export class CurrentUserUtils { }); const reqdOpts:DocumentOptions = { - title: "Experimental Tools", _xMargin: 0, _showTitle: "title", _chromeHidden: true, - _stayInCollection: true, _hideContextMenu: true, _forceActive: true, system: true, - _autoHeight: true, _width: 500, _height: 300, _fitWidth: true, _columnWidth: 35, ignoreClick: true, _lockedPosition: true, + title: "Experimental Tools", _xMargin: 0, _layout_showTitle: "title", _chromeHidden: true, + _stayInCollection: true, _layout_hideContextMenu: true, _forceActive: true, isSystem: true, + _layout_autoHeight: true, _width: 500, _height: 300, _layout_fitWidth: true, _columnWidth: 35, ignoreClick: true, _lockedPosition: true, }; const reqdScripts = { dropConverter : "convertToButtons(dragData)" }; const reqdFuncs = { hidden: "IsNoviceMode()" }; @@ -109,7 +108,7 @@ export class CurrentUserUtils { /// Initializes templates for editing click funcs of a document static setupChildClickEditors(doc: Doc, field = "clickFuncs-child") { const tempClicks = DocCast(doc[field]); - const reqdClickOpts:DocumentOptions = {_width: 300, _height:200, system: true}; + const reqdClickOpts:DocumentOptions = {_width: 300, _height:200, isSystem: true}; const reqdTempOpts:{opts:DocumentOptions, script: string}[] = [ { opts: { title: "Open In Target", targetScriptKey: "onChildClick"}, script: "docCast(documentView?.props.docViewPath().lastElement()?.rootDoc.target).then((target) => target && (target.proto.data = new List([self])))"}, { opts: { title: "Open Detail On Right", targetScriptKey: "onChildDoubleClick"}, script: `openDoc(self.doubleClickView.${OpenWhere.addRight})`}]; @@ -119,14 +118,14 @@ export class CurrentUserUtils { return DocUtils.AssignOpts(clickDoc, allOpts) ?? Docs.Create.ScriptingDocument(ScriptField.MakeScript(opts.script, allOpts),allOpts); }); - const reqdOpts:DocumentOptions = { title: "child click editors", _height:75, system: true}; + const reqdOpts:DocumentOptions = { title: "child click editors", _height:75, isSystem: true}; return DocUtils.AssignOpts(tempClicks, reqdOpts, reqdClickList) ?? (doc[field] = Docs.Create.TreeDocument(reqdClickList, reqdOpts)); } /// Initializes templates for editing click funcs of a document static setupClickEditorTemplates(doc: Doc, field = "template-clickFuncs") { const tempClicks = DocCast(doc[field]); - const reqdClickOpts:DocumentOptions = { _width: 300, _height:200, system: true}; + const reqdClickOpts:DocumentOptions = { _width: 300, _height:200, isSystem: true}; const reqdTempOpts:{opts:DocumentOptions, script: string}[] = [ { opts: { title: "onClick"}, script: "console.log('click')"}, { opts: { title: "onDoubleClick"}, script: "console.log('click')"}, @@ -137,10 +136,10 @@ export class CurrentUserUtils { const reqdClickList = reqdTempOpts.map(opts => { const allOpts = {...reqdClickOpts, ...opts.opts}; const clickDoc = tempClicks ? DocListCast(tempClicks.data).find(doc => doc.title === opts.opts.title): undefined; - return DocUtils.AssignOpts(clickDoc, allOpts) ?? MakeTemplate(Docs.Create.ScriptingDocument(ScriptField.MakeScript(opts.script, {heading:Doc.name, checked:"boolean", containingTreeView:Doc.name}), allOpts), true, opts.opts.title); + return DocUtils.AssignOpts(clickDoc, allOpts) ?? MakeTemplate(Docs.Create.ScriptingDocument(ScriptField.MakeScript(opts.script, {heading:Doc.name, checked:"boolean", containingTreeView:Doc.name}), allOpts), true, opts.opts.title?.toString()); }); - const reqdOpts:DocumentOptions = {title: "click editor templates", _height:75, system: true}; + const reqdOpts:DocumentOptions = {title: "click editor templates", _height:75, isSystem: true}; return DocUtils.AssignOpts(tempClicks, reqdOpts, reqdClickList) ?? (doc[field] = Docs.Create.TreeDocument(reqdClickList, reqdOpts)); } @@ -152,12 +151,12 @@ export class CurrentUserUtils { { noteType: "Idea", backgroundColor: "pink", icon: "lightbulb" }, { noteType: "Topic", backgroundColor: "lightblue", icon: "book-open" }]; const reqdNoteList = reqdTempOpts.map(opts => { - const reqdOpts = {...opts, title: "text", width:200, autoHeight: true, fitWidth: true}; + const reqdOpts = {...opts, title: "text", width:200, layout_autoHeight: true, layout_fitWidth: true}; const noteType = tempNotes ? DocListCast(tempNotes.data).find(doc => doc.noteType === opts.noteType): undefined; return DocUtils.AssignOpts(noteType, reqdOpts) ?? MakeTemplate(Docs.Create.TextDocument("",reqdOpts), true, opts.noteType??"Note"); }); - const reqdOpts:DocumentOptions = { title: "Note Layouts", _height: 75, system: true }; + const reqdOpts:DocumentOptions = { title: "Note Layouts", _height: 75, isSystem: true }; return DocUtils.AssignOpts(tempNotes, reqdOpts, reqdNoteList) ?? (doc[field] = Docs.Create.TreeDocument(reqdNoteList, reqdOpts)); } @@ -169,14 +168,14 @@ export class CurrentUserUtils { CurrentUserUtils.setupClickEditorTemplates(doc) ]; CurrentUserUtils.setupChildClickEditors(doc) - const reqdOpts = { title: "template layouts", _xMargin: 0, system: true, }; + const reqdOpts = { title: "template layouts", _xMargin: 0, isSystem: true, }; const reqdScripts = { dropConverter: "convertToButtons(dragData)" }; return DocUtils.AssignDocField(doc, field, (opts,items) => Docs.Create.TreeDocument(items??[], opts), reqdOpts, templates, reqdScripts); } // setup templates for different document types when they are iconified from Document Decorations static setupDefaultIconTemplates(doc: Doc, field="template-icons") { - const reqdOpts = { title: "icon templates", _height: 75, system: true }; + const reqdOpts = { title: "icon templates", _height: 75, isSystem: true }; const templateIconsDoc = DocUtils.AssignOpts(DocCast(doc[field]), reqdOpts) ?? (doc[field] = Docs.Create.TreeDocument([], reqdOpts)); const makeIconTemplate = (type: DocumentType | undefined, templateField: string, opts:DocumentOptions) => { @@ -187,22 +186,22 @@ export class CurrentUserUtils { case DocumentType.IMG : creator = imageBox; break; case DocumentType.FONTICON: creator = fontBox; break; } - const allopts = {system: true, onClickScriptDisable: "never", ...opts}; + const allopts = {isSystem: true, onClickScriptDisable: "never", ...opts}; return DocUtils.AssignScripts( (curIcon?.iconTemplate === opts.iconTemplate ? DocUtils.AssignOpts(curIcon, allopts):undefined) ?? ((templateIconsDoc[iconFieldName] = MakeTemplate(creator(allopts), true, iconFieldName, templateField))), {onClick:"deiconifyView(documentView)", onDoubleClick: "deiconifyViewToLightbox(documentView)", }); }; const labelBox = (opts: DocumentOptions, data?:string) => Docs.Create.LabelDocument({ - textTransform: "unset", letterSpacing: "unset", _singleLine: false, _minFontSize: 14, _maxFontSize: 24, borderRounding: "5px", _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, ...opts + textTransform: "unset", letterSpacing: "unset", _singleLine: false, _label_minFontSize: 14, _label_maxFontSize: 24, layout_borderRounding: "5px", _width: 150, _height: 70, _xPadding: 10, _yPadding: 10, ...opts }); - const imageBox = (opts: DocumentOptions, url?:string) => Docs.Create.ImageDocument(url ?? "http://www.cs.brown.edu/~bcz/noImage.png", { "icon-nativeWidth": 360 / 4, "icon-nativeHeight": 270 / 4, iconTemplate:DocumentType.IMG, _width: 360 / 4, _height: 270 / 4, _showTitle: "title", ...opts }); + const imageBox = (opts: DocumentOptions, url?:string) => Docs.Create.ImageDocument(url ?? "http://www.cs.brown.edu/~bcz/noImage.png", { "icon_nativeWidth": 360 / 4, "icon_nativeHeight": 270 / 4, iconTemplate:DocumentType.IMG, _width: 360 / 4, _height: 270 / 4, _layout_showTitle: "title", ...opts }); const fontBox = (opts:DocumentOptions, data?:string) => Docs.Create.FontIconDocument({ _nativeHeight: 30, _nativeWidth: 30, _width: 30, _height: 30, ...opts }); const iconTemplates = [ makeIconTemplate(undefined, "title", { iconTemplate:DocumentType.LABEL, backgroundColor: "dimgray"}), makeIconTemplate(DocumentType.AUDIO, "title", { iconTemplate:DocumentType.LABEL, backgroundColor: "lightgreen"}), makeIconTemplate(DocumentType.PDF, "title", { iconTemplate:DocumentType.LABEL, backgroundColor: "pink"}), makeIconTemplate(DocumentType.WEB, "title", { iconTemplate:DocumentType.LABEL, backgroundColor: "brown"}), - makeIconTemplate(DocumentType.RTF, "text", { iconTemplate:DocumentType.LABEL, _showTitle: "creationDate"}), + makeIconTemplate(DocumentType.RTF, "text", { iconTemplate:DocumentType.LABEL, _layout_showTitle: "author_date"}), makeIconTemplate(DocumentType.IMG, "data", { iconTemplate:DocumentType.IMG, _height: undefined}), makeIconTemplate(DocumentType.COL, "icon", { iconTemplate:DocumentType.IMG}), makeIconTemplate(DocumentType.COL, "icon", { iconTemplate:DocumentType.IMG}), @@ -220,7 +219,7 @@ export class CurrentUserUtils { title: string, toolTip: string, icon: string, ignoreClick?: boolean, dragFactory?: Doc, backgroundColor?: string, openFactoryAsDelegate?:boolean, openFactoryLocation?:string, clickFactory?: Doc, scripts?: { onClick?: string, onDragStart?: string}, funcs?: {onDragStart?:string, hidden?: string}, }[] { - const standardOps = (key:string) => ({ title : "Untitled "+ key, _fitWidth: false, system: true, "dragFactory-count": 0, cloneFieldFilter: new List<string>(["system"]) }); + const standardOps = (key:string) => ({ title : "Untitled "+ key, _layout_fitWidth: false, isSystem: true, "dragFactory_count": 0, cloneFieldFilter: new List<string>(["isSystem"]) }); const json = { doc: { type: "doc", @@ -232,7 +231,7 @@ export class CurrentUserUtils { marks: [{ type: "strong" }] }, { type: "dashField", - attrs: { fieldKey: "creationDate", docId: "", hideKey: false }, + attrs: { fieldKey: "author_date", docId: "", hideKey: false }, marks: [{ type: "strong" }] }] }] @@ -247,7 +246,7 @@ export class CurrentUserUtils { "<HTMLdiv transformOrigin='top left' width='{100/scale}%' height='{100/scale}%' transform='scale({scale})'>" + ` <FormattedTextBox {...props} dontScale='true' fieldKey={'text'} height='calc(100% - ${headerBtnHgt}px - {this._headerHeight||0}px)'/>` + " <FormattedTextBox {...props} dontScale='true' fieldKey={'header'} dontSelectOnLoad='true' ignoreAutoHeight='true' fontSize='{this._headerFontSize||9}px' height='{(this._headerHeight||0)}px' backgroundColor='{this._headerColor || MySharedDocs().userColor||`lightGray`}' />" + - ` <HTMLdiv fontSize='${headerBtnHgt - 1}px' height='${headerBtnHgt}px' backgroundColor='yellow' onClick={‘(this._headerHeight=scale*Math.min(Math.max(0,this._height-30),this._headerHeight===0?50:0)) + (this._autoHeightMargins=this._headerHeight ? this._headerHeight+${headerBtnHgt}:0)’} >Metadata</HTMLdiv>` + + ` <HTMLdiv fontSize='${headerBtnHgt - 1}px' height='${headerBtnHgt}px' backgroundColor='yellow' onClick={‘(this._headerHeight=scale*Math.min(Math.max(0,this._height-30),this._headerHeight===0?50:0)) + (this._layout_autoHeightMargins=this._headerHeight ? this._headerHeight+${headerBtnHgt}:0)’} >Metadata</HTMLdiv>` + "</HTMLdiv>" }, "header"); @@ -261,52 +260,56 @@ export class CurrentUserUtils { const emptyThings:{key:string, // the field name where the empty thing will be stored opts:DocumentOptions, // the document options that are required for the empty thing funcs?:{[key:string]: any}, // computed fields that are rquired for the empth thing + scripts?:{[key:string]: any}, creator:(opts:DocumentOptions)=> any // how to create the empty thing if it doesn't exist }[] = [ - {key: "Note", creator: opts => Docs.Create.TextDocument("", opts), opts: { _width: 200, _autoHeight: true }}, - {key: "Flashcard", creator: opts => Docs.Create.TextDocument("", opts), opts: { _width: 200, _autoHeight: true, _showAltContentUI: true}}, - {key: "Equation", creator: opts => Docs.Create.EquationDocument(opts), opts: { _width: 300, _height: 35, _backgroundGridShow: true, }}, - {key: "Noteboard", creator: opts => Docs.Create.NoteTakingDocument([], opts), opts: { _width: 250, _height: 200, _fitWidth: true}}, - {key: "Collection", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 150, _height: 100, _fitWidth: true }}, - {key: "Webpage", creator: opts => Docs.Create.WebDocument("",opts), opts: { _width: 400, _height: 512, _nativeWidth: 850, useCors: true, }}, + {key: "Note", creator: opts => Docs.Create.TextDocument("", opts), opts: { _width: 200, _layout_autoHeight: true }}, + {key: "Flashcard", creator: opts => Docs.Create.TextDocument("", opts), opts: { _width: 200, _layout_autoHeight: true, _layout_enableAltContentUI: true}}, + {key: "Equation", creator: opts => Docs.Create.EquationDocument(opts), opts: { _width: 300, _height: 35, }}, + {key: "Noteboard", creator: opts => Docs.Create.NoteTakingDocument([], opts), opts: { _width: 250, _height: 200, _layout_fitWidth: true}}, + {key: "Simulation", creator: opts => Docs.Create.SimulationDocument(opts), opts: { _width: 300, _height: 300, }}, + {key: "Collection", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 150, _height: 100, _layout_fitWidth: true }}, + {key: "Webpage", creator: opts => Docs.Create.WebDocument("",opts), opts: { _width: 400, _height: 512, _nativeWidth: 850, data_useCors: true, }}, {key: "Comparison", creator: Docs.Create.ComparisonDocument, opts: { _width: 300, _height: 300 }}, {key: "Audio", creator: opts => Docs.Create.AudioDocument(nullAudio, opts),opts: { _width: 200, _height: 100, }}, - {key: "Map", creator: opts => Docs.Create.MapDocument([], opts), opts: { _width: 800, _height: 600, _fitWidth: true, _showSidebar: true, }}, + {key: "Map", creator: opts => Docs.Create.MapDocument([], opts), opts: { _width: 800, _height: 600, _layout_fitWidth: true, _layout_showSidebar: true, }}, {key: "Screengrab", creator: Docs.Create.ScreenshotDocument, opts: { _width: 400, _height: 200 }}, - {key: "WebCam", creator: opts => Docs.Create.WebCamDocument("", opts), opts: { _width: 400, _height: 200, recording:true, system: true, cloneFieldFilter: new List<string>(["system"]) }}, - {key: "Button", creator: Docs.Create.ButtonDocument, opts: { _width: 150, _height: 50, _xPadding: 10, _yPadding: 10, onClick: FollowLinkScript()}}, + {key: "WebCam", creator: opts => Docs.Create.WebCamDocument("", opts), opts: { _width: 400, _height: 200, recording:true, isSystem: true, cloneFieldFilter: new List<string>(["isSystem"]) }}, + {key: "Button", creator: Docs.Create.ButtonDocument, opts: { _width: 150, _height: 50, _xPadding: 10, _yPadding: 10}, scripts: {onClick: FollowLinkScript()?.script.originalScript ?? ""}}, {key: "Script", creator: opts => Docs.Create.ScriptingDocument(null, opts), opts: { _width: 200, _height: 250, }}, {key: "DataViz", creator: opts => Docs.Create.DataVizDocument("/users/rz/Downloads/addresses.csv", opts), opts: { _width: 300, _height: 300 }}, - {key: "Header", creator: headerTemplate, opts: { _width: 300, _height: 70, _headerPointerEvents: "all", _headerHeight: 12, _headerFontSize: 9, _autoHeight: true, treeViewHideUnrendered: true}}, - {key: "Trail", creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 30, _viewType: CollectionViewType.Stacking, targetDropAction: "alias" as any, treeViewHideTitle: true, _fitWidth:true, _chromeHidden: true, boxShadow: "0 0" }}, - {key: "Tab", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 500, _height: 800, _fitWidth: true, _backgroundGridShow: true, }}, - {key: "Slide", creator: opts => Docs.Create.TreeDocument([], opts), opts: { _width: 300, _height: 200, _viewType: CollectionViewType.Tree, - treeViewHasOverlay: true, _fontSize: "20px", _autoHeight: true, + {key: "Header", creator: headerTemplate, opts: { _width: 300, _height: 70, _headerPointerEvents: "all", _headerHeight: 12, _headerFontSize: 9, _layout_autoHeight: true, treeViewHideUnrendered: true}}, + {key: "Trail", creator: Docs.Create.PresDocument, opts: { _width: 400, _height: 30, _type_collection: CollectionViewType.Stacking, targetDropAction: "embed" as dropActionType, treeViewHideTitle: true, _layout_fitWidth:true, _chromeHidden: true, layout_boxShadow: "0 0" }}, + {key: "Tab", creator: opts => Docs.Create.FreeformDocument([], opts), opts: { _width: 500, _height: 800, _layout_fitWidth: true, _freeform_backgroundGrid: true, }}, + {key: "Slide", creator: opts => Docs.Create.TreeDocument([], opts), opts: { _width: 300, _height: 200, _type_collection: CollectionViewType.Tree, + treeViewHasOverlay: true, _text_fontSize: "20px", _layout_autoHeight: true, allowOverlayDrop: true, treeViewType: TreeViewType.outline, - backgroundColor: "white", _xMargin: 0, _yMargin: 0, _singleLine: true + backgroundColor: "white", _xMargin: 0, _yMargin: 0, _createDocOnCR: true }, funcs: {title: 'self.text?.Text'}}, ]; - emptyThings.forEach(thing => DocUtils.AssignDocField(doc, "empty"+thing.key, (opts) => thing.creator(opts), {...standardOps(thing.key), ...thing.opts}, undefined, undefined, thing.funcs)); + emptyThings.forEach(thing => DocUtils.AssignDocField(doc, "empty"+thing.key, (opts) => thing.creator(opts), {...standardOps(thing.key), ...thing.opts}, undefined, thing.scripts, thing.funcs)); return [ - { toolTip: "Tap or drag to create a note", title: "Note", icon: "sticky-note", dragFactory: doc.emptyNote as Doc, clickFactory: DocCast(doc.emptyNote)}, - { toolTip: "Tap or drag to create a flashcard", title: "Flashcard", icon: "id-card", dragFactory: doc.emptyFlashcard as Doc, clickFactory: DocCast(doc.emptyFlashcard)}, - { toolTip: "Tap or drag to create an equation", title: "Math", icon: "calculator", dragFactory: doc.emptyEquation as Doc, clickFactory: DocCast(doc.emptyEquation)}, - { toolTip: "Tap or drag to create a note board", title: "Notes", icon: "folder", dragFactory: doc.emptyNoteboard as Doc, clickFactory: DocCast(doc.emptyNoteboard)}, - { toolTip: "Tap or drag to create a collection", title: "Col", icon: "folder", dragFactory: doc.emptyCollection as Doc,clickFactory: DocCast(doc.emptyTab)}, - { toolTip: "Tap or drag to create a webpage", title: "Web", icon: "globe-asia", dragFactory: doc.emptyWebpage as Doc, clickFactory: DocCast(doc.emptyWebpage)}, - { toolTip: "Tap or drag to create a comparison box", title: "Compare", icon: "columns", dragFactory: doc.emptyComparison as Doc,clickFactory: DocCast(doc.emptyComparison)}, - { toolTip: "Tap or drag to create an audio recorder", title: "Audio", icon: "microphone", dragFactory: doc.emptyAudio as Doc, clickFactory: DocCast(doc.emptyAudio), openFactoryLocation: OpenWhere.overlay}, - { toolTip: "Tap or drag to create a map", title: "Map", icon: "map-marker-alt", dragFactory: doc.emptyMap as Doc, clickFactory: DocCast(doc.emptyMap)}, - { toolTip: "Tap or drag to create a screen grabber", title: "Grab", icon: "photo-video", dragFactory: doc.emptyScreengrab as Doc,clickFactory: DocCast(doc.emptyScreengrab), openFactoryLocation: OpenWhere.overlay}, - { toolTip: "Tap or drag to create a WebCam recorder", title: "WebCam", icon: "photo-video", dragFactory: doc.emptyWebCam as Doc, clickFactory: DocCast(doc.emptyWebCam), openFactoryLocation: OpenWhere.overlay}, - { toolTip: "Tap or drag to create a button", title: "Button", icon: "bolt", dragFactory: doc.emptyButton as Doc, clickFactory: DocCast(doc.emptyButton)}, - { toolTip: "Tap or drag to create a scripting box", title: "Script", icon: "terminal", dragFactory: doc.emptyScript as Doc, clickFactory: DocCast(doc.emptyScript)}, - { toolTip: "Tap or drag to create a data viz node", title: "DataViz", icon: "file", dragFactory: doc.emptyDataViz as Doc, clickFactory: DocCast(doc.emptyDataViz)}, - { toolTip: "Tap or drag to create a bullet slide", title: "PPT Slide", icon: "file", dragFactory: doc.emptySlide as Doc, clickFactory: DocCast(doc.emptySlide), openFactoryLocation: OpenWhere.overlay}, - { toolTip: "Tap or drag to create a data note", title: "DataNote", icon: "window-maximize", dragFactory: doc.emptyHeader as Doc, clickFactory: DocCast(doc.emptyHeader), openFactoryAsDelegate: true }, - { toolTip: "Toggle a Calculator REPL", title: "repl", icon: "calculator", clickFactory: 'repl' as any, openFactoryLocation: OpenWhere.overlay}, + { toolTip: "Tap or drag to create a note", title: "Note", icon: "sticky-note", dragFactory: doc.emptyNote as Doc, clickFactory: DocCast(doc.emptyNote)}, + { toolTip: "Tap or drag to create a flashcard", title: "Flashcard", icon: "id-card", dragFactory: doc.emptyFlashcard as Doc, clickFactory: DocCast(doc.emptyFlashcard)}, + { toolTip: "Tap or drag to create an equation", title: "Math", icon: "calculator", dragFactory: doc.emptyEquation as Doc, clickFactory: DocCast(doc.emptyEquation)}, + { toolTip: "Tap or drag to create a physics simulation", title: "Simulation", icon: "atom", dragFactory: doc.emptySimulation as Doc, }, + { toolTip: "Tap or drag to create a note board", title: "Notes", icon: "folder", dragFactory: doc.emptyNoteboard as Doc, clickFactory: DocCast(doc.emptyNoteboard)}, + { toolTip: "Tap or drag to create a collection", title: "Col", icon: "folder", dragFactory: doc.emptyCollection as Doc, clickFactory: DocCast(doc.emptyTab)}, + { toolTip: "Tap or drag to create a webpage", title: "Web", icon: "globe-asia", dragFactory: doc.emptyWebpage as Doc, clickFactory: DocCast(doc.emptyWebpage)}, + { toolTip: "Tap or drag to create a comparison box", title: "Compare", icon: "columns", dragFactory: doc.emptyComparison as Doc, clickFactory: DocCast(doc.emptyComparison)}, + { toolTip: "Tap or drag to create an audio recorder", title: "Audio", icon: "microphone", dragFactory: doc.emptyAudio as Doc, clickFactory: DocCast(doc.emptyAudio), openFactoryLocation: OpenWhere.overlay}, + { toolTip: "Tap or drag to create a map", title: "Map", icon: "map-marker-alt", dragFactory: doc.emptyMap as Doc, clickFactory: DocCast(doc.emptyMap)}, + { toolTip: "Tap or drag to create a screen grabber", title: "Grab", icon: "photo-video", dragFactory: doc.emptyScreengrab as Doc, clickFactory: DocCast(doc.emptyScreengrab), openFactoryLocation: OpenWhere.overlay}, + { toolTip: "Tap or drag to create a WebCam recorder", title: "WebCam", icon: "photo-video", dragFactory: doc.emptyWebCam as Doc, clickFactory: DocCast(doc.emptyWebCam), openFactoryLocation: OpenWhere.overlay}, + { toolTip: "Tap or drag to create a button", title: "Button", icon: "bolt", dragFactory: doc.emptyButton as Doc, clickFactory: DocCast(doc.emptyButton)}, + { toolTip: "Tap or drag to create a scripting box", title: "Script", icon: "terminal", dragFactory: doc.emptyScript as Doc, clickFactory: DocCast(doc.emptyScript)}, + { toolTip: "Tap or drag to create a data viz node", title: "DataViz", icon: "file", dragFactory: doc.emptyDataViz as Doc, clickFactory: DocCast(doc.emptyDataViz)}, + { toolTip: "Tap or drag to create a bullet slide", title: "PPT Slide", icon: "file", dragFactory: doc.emptySlide as Doc, clickFactory: DocCast(doc.emptySlide), openFactoryLocation: OpenWhere.overlay}, + { toolTip: "Tap or drag to create a data note", title: "DataNote", icon: "window-maximize",dragFactory: doc.emptyHeader as Doc,clickFactory: DocCast(doc.emptyHeader), openFactoryAsDelegate: true }, + { toolTip: "Toggle a Calculator REPL", title: "replviewer", icon: "calculator", clickFactory: '<ScriptingRepl />' as any, openFactoryLocation: OpenWhere.overlay}, // hack: clickFactory is not a Doc but will get interpreted as a custom UI by the openDoc() onClick script + { toolTip: "Toggle an UndoStack", title: "undostacker", icon: "calculator", clickFactory: "<UndoStack>" as any, openFactoryLocation: OpenWhere.overlay}, ].map(tuple => ( { openFactoryLocation: OpenWhere.addRight, scripts: { onClick: 'openDoc(copyDragFactory(this.clickFactory,this.openFactoryAsDelegate), this.openFactoryLocation)', @@ -319,17 +322,17 @@ export class CurrentUserUtils { const creatorBtns = CurrentUserUtils.creatorBtnDescriptors(doc).map((reqdOpts) => { const btn = dragCreatorDoc ? DocListCast(dragCreatorDoc.data).find(doc => doc.title === reqdOpts.title): undefined; const opts:DocumentOptions = {...OmitKeys(reqdOpts, ["funcs", "scripts", "backgroundColor"]).omit, - _width: 35, _height: 35, _hideContextMenu: true, _stayInCollection: true, - btnType: ButtonType.ToolButton, backgroundColor: reqdOpts.backgroundColor ?? Colors.DARK_GRAY, color: Colors.WHITE, system: true, + _width: 35, _height: 35, _layout_hideContextMenu: true, _stayInCollection: true, + btnType: ButtonType.ToolButton, backgroundColor: reqdOpts.backgroundColor ?? Colors.DARK_GRAY, color: Colors.WHITE, isSystem: true, _removeDropProperties: new List<string>(["_stayInCollection"]), }; return DocUtils.AssignScripts(DocUtils.AssignOpts(btn, opts) ?? Docs.Create.FontIconDocument(opts), reqdOpts.scripts, reqdOpts.funcs); }); const reqdOpts:DocumentOptions = { - title: "Basic Item Creators", _showTitle: "title", _xMargin: 0, _stayInCollection: true, _hideContextMenu: true, _chromeHidden: true, system: true, - _autoHeight: true, _width: 500, _height: 300, _fitWidth: true, _columnWidth: 40, ignoreClick: true, _lockedPosition: true, _forceActive: true, - childDropAction: 'alias' + title: "Basic Item Creators", _layout_showTitle: "title", _xMargin: 0, _stayInCollection: true, _layout_hideContextMenu: true, _chromeHidden: true, isSystem: true, + _layout_autoHeight: true, _width: 500, _height: 300, _layout_fitWidth: true, _columnWidth: 40, ignoreClick: true, _lockedPosition: true, _forceActive: true, + childDropAction: 'embed' }; const reqdScripts = { dropConverter: "convertToButtons(dragData)" }; return DocUtils.AssignScripts(DocUtils.AssignOpts(dragCreatorDoc, reqdOpts, creatorBtns) ?? Docs.Create.MasonryDocument(creatorBtns, reqdOpts), reqdScripts); @@ -354,7 +357,7 @@ export class CurrentUserUtils { /// the empty panel that is filled with whichever left menu button's panel has been selected static setupLeftSidebarPanel(doc: Doc, field="myLeftSidebarPanel") { - DocUtils.AssignDocField(doc, field, (opts) => ((doc:Doc) => {doc.system = true; return doc;})(new Doc()), {system:true}); + DocUtils.AssignDocField(doc, field, (opts) => ((doc:Doc) => {doc.isSystem = true; return doc;})(new Doc()), {isSystem:true}); } /// Initializes the left sidebar menu buttons and the panels they open up @@ -364,23 +367,23 @@ export class CurrentUserUtils { const menuBtns = CurrentUserUtils.leftSidebarMenuBtnDescriptions(doc).map(({ title, target, icon, scripts, funcs }) => { const btnDoc = myLeftSidebarMenu ? DocListCast(myLeftSidebarMenu.data).find(doc => doc.title === title) : undefined; const reqdBtnOpts:DocumentOptions = { - title, icon, target, btnType: ButtonType.MenuButton, system: true, dontUndo: true, dontRegisterView: true, - _width: 60, _height: 60, _stayInCollection: true, _hideContextMenu: true, _chromeHidden: true, + title, icon, target, btnType: ButtonType.MenuButton, isSystem: true, dontUndo: true, dontRegisterView: true, + _width: 60, _height: 60, _stayInCollection: true, _layout_hideContextMenu: true, _removeDropProperties: new List<string>(["_stayInCollection"]), }; return DocUtils.AssignScripts(DocUtils.AssignOpts(btnDoc, reqdBtnOpts) ?? Docs.Create.FontIconDocument(reqdBtnOpts), scripts, funcs); }); const reqdStackOpts:DocumentOptions ={ - title: "menuItemPanel", childDropAction: "same", backgroundColor: Colors.DARK_GRAY, boxShadow: "rgba(0,0,0,0)", dontRegisterView: true, ignoreClick: true, - _chromeHidden: true, _gridGap: 0, _yMargin: 0, _yPadding: 0, _xMargin: 0, _autoHeight: false, _width: 60, _columnWidth: 60, _lockedPosition: true, system: true, + title: "menuItemPanel", childDropAction: "same", backgroundColor: Colors.DARK_GRAY, layout_boxShadow: "rgba(0,0,0,0)", dontRegisterView: true, ignoreClick: true, + _chromeHidden: true, _gridGap: 0, _yMargin: 0, _yPadding: 0, _xMargin: 0, _layout_autoHeight: false, _width: 60, _columnWidth: 60, _lockedPosition: true, isSystem: true, }; return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(items??[], opts), reqdStackOpts, menuBtns, { dropConverter: "convertToButtons(dragData)" }); } // Sets up mobile menu if it is undefined creates a new one, otherwise returns existing menu static setupActiveMobileMenu(doc: Doc, field="activeMobileMenu") { - const reqdOpts = { _width: 980, ignoreClick: true, _lockedPosition: false, title: "home", _yMargin: 100, system: true, _chromeHidden: true,}; + const reqdOpts = { _width: 980, ignoreClick: true, _lockedPosition: false, title: "home", _yMargin: 100, isSystem: true, _chromeHidden: true,}; DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(this.setupMobileButtons(), opts), reqdOpts); } @@ -401,9 +404,9 @@ export class CurrentUserUtils { title: data.title, _lockedPosition: true, onClick: data.click ? ScriptField.MakeScript(data.click) : undefined, - backgroundColor: data.backgroundColor, system: true + backgroundColor: data.backgroundColor, isSystem: true }, - [this.createToolButton({ ignoreClick: true, icon: data.icon, backgroundColor: "rgba(0,0,0,0)", system: true, btnType: ButtonType.ClickButton, }), this.mobileTextContainer({}, [this.mobileButtonText({}, data.title), this.mobileButtonInfo({}, data.info)])]) + [this.createToolButton({ ignoreClick: true, icon: data.icon, backgroundColor: "rgba(0,0,0,0)", isSystem: true, btnType: ButtonType.ClickButton, }), this.mobileTextContainer({}, [this.mobileButtonText({}, data.title), this.mobileButtonInfo({}, data.info)])]) ); } @@ -411,52 +414,52 @@ export class CurrentUserUtils { static mobileButton = (opts: DocumentOptions, docs: Doc[]) => Docs.Create.MulticolumnDocument(docs, { ...opts, _nativeWidth: 900, _nativeHeight: 250, _width: 900, _height: 250, _yMargin: 15, - borderRounding: "5px", boxShadow: "0 0", system: true + layout_borderRounding: "5px", layout_boxShadow: "0 0", isSystem: true }) as any as Doc // sets up the text container for the information contained within the mobile button static mobileTextContainer = (opts: DocumentOptions, docs: Doc[]) => Docs.Create.MultirowDocument(docs, { ...opts, _nativeWidth: 450, _nativeHeight: 250, _width: 450, _height: 250, _yMargin: 25, - backgroundColor: "rgba(0,0,0,0)", borderRounding: "0", boxShadow: "0 0", ignoreClick: true, system: true + backgroundColor: "rgba(0,0,0,0)", layout_borderRounding: "0", layout_boxShadow: "0 0", ignoreClick: true, isSystem: true }) as any as Doc // Sets up the title of the button static mobileButtonText = (opts: DocumentOptions, buttonTitle: string) => Docs.Create.TextDocument(buttonTitle, { ...opts, - title: buttonTitle, _fontSize: "37px", _xMargin: 0, _yMargin: 0, ignoreClick: true, backgroundColor: "rgba(0,0,0,0)", system: true + title: buttonTitle, _text_fontSize: "37px", _xMargin: 0, _yMargin: 0, ignoreClick: true, backgroundColor: "rgba(0,0,0,0)", isSystem: true }) as any as Doc // Sets up the description of the button static mobileButtonInfo = (opts: DocumentOptions, buttonInfo: string) => Docs.Create.TextDocument(buttonInfo, { ...opts, - title: "info", _fontSize: "25px", _xMargin: 0, _yMargin: 0, ignoreClick: true, backgroundColor: "rgba(0,0,0,0)", _dimMagnitude: 2, system: true + title: "info", _text_fontSize: "25px", _xMargin: 0, _yMargin: 0, ignoreClick: true, backgroundColor: "rgba(0,0,0,0)", _dimMagnitude: 2, isSystem: true }) as any as Doc static setupMobileInkingDoc(userDoc: Doc) { - return Docs.Create.FreeformDocument([], { title: "Mobile Inking", backgroundColor: "white", system: true }); + return Docs.Create.FreeformDocument([], { title: "Mobile Inking", backgroundColor: "white", isSystem: true }); } static setupMobileUploadDoc(userDoc: Doc) { // const addButton = Docs.Create.FontIconDocument({ onDragStart: ScriptField.MakeScript('addWebToMobileUpload()'), title: "Add Web Doc to Upload Collection", icon: "plus", backgroundColor: "black" }) const webDoc = Docs.Create.WebDocument("https://www.britannica.com/biography/Miles-Davis", { - title: "Upload Images From the Web", _lockedPosition: true, system: true + title: "Upload Images From the Web", _lockedPosition: true, isSystem: true }); const uploadDoc = Docs.Create.StackingDocument([], { - title: "Mobile Upload Collection", backgroundColor: "white", _lockedPosition: true, system: true, _chromeHidden: true, + title: "Mobile Upload Collection", backgroundColor: "white", _lockedPosition: true, isSystem: true, _chromeHidden: true, }); return Docs.Create.StackingDocument([webDoc, uploadDoc], { - _width: screen.width, _lockedPosition: true, title: "Upload", _autoHeight: true, _yMargin: 80, backgroundColor: "lightgray", system: true, _chromeHidden: true, + _width: screen.width, _lockedPosition: true, title: "Upload", _layout_autoHeight: true, _yMargin: 80, backgroundColor: "lightgray", isSystem: true, _chromeHidden: true, }); } /// Search option on the left side button panel static setupSearcher(doc: Doc, field:string) { return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.SearchDocument(opts), { - dontRegisterView: true, backgroundColor: "dimgray", ignoreClick: true, title: "Search Panel", system: true, childDropAction: "alias", - _lockedPosition: true, _viewType: CollectionViewType.Schema, _searchDoc: true, }); + dontRegisterView: true, backgroundColor: "dimgray", ignoreClick: true, title: "Search Panel", isSystem: true, childDropAction: "embed", + _lockedPosition: true, _type_collection: CollectionViewType.Schema }); } /// Initializes the panel of draggable tools that is opened from the left sidebar. @@ -465,8 +468,8 @@ export class CurrentUserUtils { const creatorBtns = CurrentUserUtils.setupCreatorButtons(doc, DocListCast(myTools?.data)?.length ? DocListCast(myTools.data)[0]:undefined); const templateBtns = CurrentUserUtils.setupExperimentalTemplateButtons(doc,DocListCast(myTools?.data)?.length > 1 ? DocListCast(myTools.data)[1]:undefined); const reqdToolOps:DocumentOptions = { - title: "My Tools", system: true, ignoreClick: true, boxShadow: "0 0", - _showTitle: "title", _width: 500, _yMargin: 20, _lockedPosition: true, _forceActive: true, _stayInCollection: true, _hideContextMenu: true, _chromeHidden: true, + title: "My Tools", isSystem: true, ignoreClick: true, layout_boxShadow: "0 0", + _layout_showTitle: "title", _width: 500, _yMargin: 20, _lockedPosition: true, _forceActive: true, _stayInCollection: true, _layout_hideContextMenu: true, _chromeHidden: true, }; return DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.StackingDocument(items??[], opts), reqdToolOps, [creatorBtns, templateBtns]); } @@ -478,10 +481,10 @@ export class CurrentUserUtils { const toggleDarkTheme = `this.colorScheme = this.colorScheme ? undefined : "${ColorScheme.Dark}"`; const newDashboard = `createNewDashboard()`; - const reqdBtnOpts:DocumentOptions = { _forceActive: true, _width: 30, _height: 30, _stayInCollection: true, _hideContextMenu: true, - title: "new dashboard", btnType: ButtonType.ClickButton, toolTip: "Create new dashboard", buttonText: "New trail", icon: "plus", system: true }; + const reqdBtnOpts:DocumentOptions = { _forceActive: true, _width: 30, _height: 30, _stayInCollection: true, _layout_hideContextMenu: true, + title: "new dashboard", btnType: ButtonType.ClickButton, toolTip: "Create new dashboard", buttonText: "New trail", icon: "plus", isSystem: true }; const reqdBtnScript = {onClick: newDashboard,} - const newDashboardButton = DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(myDashboards?.buttonMenuDoc), reqdBtnOpts) ?? Docs.Create.FontIconDocument(reqdBtnOpts), reqdBtnScript); + const newDashboardButton = DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(myDashboards?.layout_headerButton), reqdBtnOpts) ?? Docs.Create.FontIconDocument(reqdBtnOpts), reqdBtnScript); const contextMenuScripts = [/*newDashboard*/] as string[]; const contextMenuLabels = [/*"Create New Dashboard"*/] as string[]; @@ -491,15 +494,15 @@ export class CurrentUserUtils { const childContextMenuLabels = ["Toggle Dark Theme", "Toggle Comic Mode", "Snapshot Dashboard", "Share Dashboard", "Remove Dashboard", "Reset Dashboard"];// entries must be kept in synch with childContextMenuScripts, childContextMenuIcons, and childContextMenuFilters const childContextMenuIcons = ["chalkboard", "tv", "camera", "users", "times", "trash"]; // entries must be kept in synch with childContextMenuScripts, childContextMenuLabels, and childContextMenuFilters const reqdOpts:DocumentOptions = { - title: "My Dashboards", childHideLinkButton: true, freezeChildren: "remove|add", treeViewHideTitle: true, boxShadow: "0 0", childDontRegisterViews: true, - targetDropAction: "same", treeViewType: TreeViewType.fileSystem, isFolder: true, system: true, treeViewTruncateTitleWidth: 350, ignoreClick: true, - buttonMenu: true, buttonMenuDoc: newDashboardButton, childDropAction: "alias", - _showTitle: "title", _height: 400, _gridGap: 5, _forceActive: true, _lockedPosition: true, + title: "My Dashboards", childHideLinkButton: true, treeViewFreezeChildren: "remove|add", treeViewHideTitle: true, layout_boxShadow: "0 0", childDontRegisterViews: true, + targetDropAction: "same", treeViewType: TreeViewType.fileSystem, isFolder: true, isSystem: true, treeViewTruncateTitleWidth: 350, ignoreClick: true, + layout_headerButton: newDashboardButton, childDropAction: "embed", + _layout_showTitle: "title", _height: 400, _gridGap: 5, _forceActive: true, _lockedPosition: true, contextMenuLabels:new List<string>(contextMenuLabels), contextMenuIcons:new List<string>(contextMenuIcons), childContextMenuLabels:new List<string>(childContextMenuLabels), childContextMenuIcons:new List<string>(childContextMenuIcons), - explainer: "This is your collection of dashboards. A dashboard represents the tab configuration of your workspace. To manage documents as folders, go to the Files." + layout_explainer: "This is your collection of dashboards. A dashboard represents the tab configuration of your workspace. To manage documents as folders, go to the Files." }; myDashboards = DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.TreeDocument([], opts), reqdOpts); if (Cast(myDashboards.contextMenuScripts, listSpec(ScriptField), null)?.length !== contextMenuScripts.length) { @@ -517,23 +520,23 @@ export class CurrentUserUtils { /// initializes the left sidebar File system pane static setupFilesystem(doc: Doc, field:string) { var myFilesystem = DocCast(doc[field]); - const myFileOrphans = DocUtils.AssignDocField(doc, "myFileOrphans", (opts) => Docs.Create.TreeDocument([], opts), { title: "Unfiled", _stayInCollection: true, system: true, isFolder: true }); + const myFileOrphans = DocUtils.AssignDocField(doc, "myFileOrphans", (opts) => Docs.Create.TreeDocument([], opts), { title: "Unfiled", _stayInCollection: true, isSystem: true, isFolder: true }); const newFolder = `TreeView_addNewFolder()`; const newFolderOpts: DocumentOptions = { - _forceActive: true, _stayInCollection: true, _hideContextMenu: true, _width: 30, _height: 30, - title: "New folder", btnType: ButtonType.ClickButton, toolTip: "Create new folder", buttonText: "New folder", icon: "folder-plus", system: true + _forceActive: true, _stayInCollection: true, _layout_hideContextMenu: true, _width: 30, _height: 30, + title: "New folder", btnType: ButtonType.ClickButton, toolTip: "Create new folder", buttonText: "New folder", icon: "folder-plus", isSystem: true }; const newFolderScript = { onClick: newFolder}; - const newFolderButton = DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(myFilesystem?.buttonMenuDoc), newFolderOpts) ?? Docs.Create.FontIconDocument(newFolderOpts), newFolderScript); + const newFolderButton = DocUtils.AssignScripts(DocUtils.AssignOpts(DocCast(myFilesystem?.layout_headerButton), newFolderOpts) ?? Docs.Create.FontIconDocument(newFolderOpts), newFolderScript); - const reqdOpts:DocumentOptions = { _showTitle: "title", _height: 100, _gridGap: 5, _forceActive: true, _lockedPosition: true, - title: "My Documents", buttonMenu: true, buttonMenuDoc: newFolderButton, treeViewHideTitle: true, targetDropAction: "proto", system: true, - isFolder: true, treeViewType: TreeViewType.fileSystem, childHideLinkButton: true, boxShadow: "0 0", childDontRegisterViews: true, - treeViewTruncateTitleWidth: 350, ignoreClick: true, childDropAction: "alias", + const reqdOpts:DocumentOptions = { _layout_showTitle: "title", _height: 100, _gridGap: 5, _forceActive: true, _lockedPosition: true, + title: "My Documents", layout_headerButton: newFolderButton, treeViewHideTitle: true, targetDropAction: "proto", isSystem: true, + isFolder: true, treeViewType: TreeViewType.fileSystem, childHideLinkButton: true, layout_boxShadow: "0 0", childDontRegisterViews: true, + treeViewTruncateTitleWidth: 350, ignoreClick: true, childDropAction: "embed", childContextMenuLabels: new List<string>(["Create new folder"]), childContextMenuIcons: new List<string>(["plus"]), - explainer: "This is your file manager where you can create folders to keep track of documents independently of your dashboard." + layout_explainer: "This is your file manager where you can create folders to keep track of documents independently of your dashboard." }; myFilesystem = DocUtils.AssignDocField(doc, field, (opts, items) => Docs.Create.TreeDocument(items??[], opts), reqdOpts, [myFileOrphans]); const childContextMenuScripts = [newFolder]; @@ -545,22 +548,22 @@ export class CurrentUserUtils { /// initializes the panel displaying docs that have been recently closed static setupRecentlyClosed(doc: Doc, field:string) { - const reqdOpts:DocumentOptions = { _showTitle: "title", _lockedPosition: true, _gridGap: 5, _forceActive: true, - title: "My Recently Closed", buttonMenu: true, childHideLinkButton: true, treeViewHideTitle: true, childDropAction: "alias", system: true, - treeViewTruncateTitleWidth: 350, ignoreClick: true, boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", + const reqdOpts:DocumentOptions = { _layout_showTitle: "title", _lockedPosition: true, _gridGap: 5, _forceActive: true, + title: "My Recently Closed", childHideLinkButton: true, treeViewHideTitle: true, childDropAction: "embed", isSystem: true, + treeViewTruncateTitleWidth: 350, ignoreClick: true, layout_boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", contextMenuLabels: new List<string>(["Empty recently closed"]), contextMenuIcons:new List<string>(["trash"]), - explainer: "Recently closed documents appear in this menu. They will only be deleted if you explicity empty this list." + layout_explainer: "Recently closed documents appear in this menu. They will only be deleted if you explicity empty this list." }; const recentlyClosed = DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.TreeDocument([], opts), reqdOpts); const clearAll = (target:string) => `getProto(${target}).data = new List([])`; - const clearBtnsOpts:DocumentOptions = { _width: 30, _height: 30, _forceActive: true, _stayInCollection: true, _hideContextMenu: true, - title: "Empty", target: recentlyClosed, btnType: ButtonType.ClickButton, buttonText: "Empty", icon: "trash", system: true, + const clearBtnsOpts:DocumentOptions = { _width: 30, _height: 30, _forceActive: true, _stayInCollection: true, _layout_hideContextMenu: true, + title: "Empty", target: recentlyClosed, btnType: ButtonType.ClickButton, buttonText: "Empty", icon: "trash", isSystem: true, toolTip: "Empty recently closed",}; - const clearDocsButton = DocUtils.AssignDocField(recentlyClosed, "clearDocsBtn", (opts) => Docs.Create.FontIconDocument(opts), clearBtnsOpts, undefined, {onClick: clearAll("self.target")}); + DocUtils.AssignDocField(recentlyClosed, "layout_headerButton", (opts) => Docs.Create.FontIconDocument(opts), clearBtnsOpts, undefined, {onClick: clearAll("self.target")}); - if (recentlyClosed.buttonMenuDoc !== clearDocsButton) Doc.GetProto(recentlyClosed).buttonMenuDoc = clearDocsButton; + //if (recentlyClosed.layout_headerButton !== clearDocsButton) Doc.GetProto(recentlyClosed).layout_headerButton = clearDocsButton; if (!Cast(recentlyClosed.contextMenuScripts, listSpec(ScriptField),null)?.find((script) => script.script.originalScript === clearAll("self"))) { recentlyClosed.contextMenuScripts = new List<ScriptField>([ScriptField.MakeScript(clearAll("self"))!]) @@ -572,7 +575,7 @@ export class CurrentUserUtils { static setupUserDocView(doc: Doc, field:string) { const reqdOpts:DocumentOptions = { _lockedPosition: true, _gridGap: 5, _forceActive: true, title: Doc.CurrentUserEmail +"-view", - boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", ignoreClick: true, system: true, + layout_boxShadow: "0 0", childDontRegisterViews: true, targetDropAction: "same", ignoreClick: true, isSystem: true, treeViewHideTitle: true, treeViewTruncateTitleWidth: 350 }; if (!doc[field]) DocUtils.AssignOpts(doc, {treeViewOpen: true, treeViewExpandedView: "fields" }); @@ -580,15 +583,15 @@ export class CurrentUserUtils { } static linearButtonList = (opts: DocumentOptions, docs: Doc[]) => Docs.Create.LinearDocument(docs, { - ...opts, _gridGap: 0, _xMargin: 5, _yMargin: 5, boxShadow: "0 0", _forceActive: true, + ...opts, _gridGap: 0, _xMargin: 5, _yMargin: 5, layout_boxShadow: "0 0", _forceActive: true, dropConverter: ScriptField.MakeScript("convertToButtons(dragData)", { dragData: DragManager.DocumentDragData.name }), - _lockedPosition: true, system: true, flexDirection: "row" + _lockedPosition: true, isSystem: true, flexDirection: "row" }) static createToolButton = (opts: DocumentOptions) => Docs.Create.FontIconDocument({ - btnType: ButtonType.ToolButton, _forceActive: true, _hideContextMenu: true, - _removeDropProperties: new List<string>([ "_hideContextMenu", "stayInCollection"]), - _nativeWidth: 40, _nativeHeight: 40, _width: 40, _height: 40, system: true, ...opts, + btnType: ButtonType.ToolButton, _forceActive: true, _layout_hideContextMenu: true, + _removeDropProperties: new List<string>([ "_layout_hideContextMenu", "stayInCollection"]), + _nativeWidth: 40, _nativeHeight: 40, _width: 40, _height: 40, isSystem: true, ...opts, }) /// initializes the required buttons in the expanding button menu at the bottom of the Dash window @@ -599,15 +602,16 @@ export class CurrentUserUtils { CurrentUserUtils.createToolButton(opts), scripts, funcs); const btnDescs = [// setup reactions to change the highlights on the undo/redo buttons -- would be better to encode this in the undo/redo buttons, but the undo/redo stacks are not wired up that way yet - { scripts: { onClick: "undo()"}, opts: { title: "undo", icon: "undo-alt", toolTip: "Click to undo" }}, - { scripts: { onClick: "redo()"}, opts: { title: "redo", icon: "redo-alt", toolTip: "Click to redo" }}, - { scripts: { }, opts: { title: "linker", icon: "linkui", toolTip: "link started"}}, - { scripts: { }, opts: { title: "currently playing", icon: "currentlyplayingui", toolTip: "currently playing media"}}, + { scripts: { onClick: "undo()"}, opts: { title: "undo", icon: "undo-alt", toolTip: "Click to undo" }}, + { scripts: { onClick: "redo()"}, opts: { title: "redo", icon: "redo-alt", toolTip: "Click to redo" }}, + { scripts: { }, opts: { title: "undoStack", layout: "<UndoStack>", toolTip: "Undo/Redo Stack"}}, // note: layout fields are hacks -- they don't actually run through the JSX parser (yet) + { scripts: { }, opts: { title: "linker", layout: "<LinkingUI>", toolTip: "link started"}}, + { scripts: { }, opts: { title: "currently playing", layout: "<CurrentlyPlayingUI>", toolTip: "currently playing media"}}, ]; - const btns = btnDescs.map(desc => dockBtn({_width: 30, _height: 30, dontUndo: true, _stayInCollection: true, ...desc.opts}, desc.scripts)); + const btns = btnDescs.map(desc => dockBtn({_width: 30, _height: 30, defaultDoubleClick: 'ignore', dontUndo: true, _stayInCollection: true, ...desc.opts}, desc.scripts)); const dockBtnsReqdOpts:DocumentOptions = { - title: "docked buttons", _height: 40, flexGap: 0, boxShadow: "standard", childDropAction: 'alias', - childDontRegisterViews: true, linearViewIsExpanded: true, linearViewExpandable: true, ignoreClick: true + title: "docked buttons", _height: 40, flexGap: 0, layout_boxShadow: "standard", childDropAction: 'embed', + childDontRegisterViews: true, linearView_IsExpanded: true, linearView_Expandable: true, ignoreClick: true }; reaction(() => UndoManager.redoStack.slice(), () => Doc.GetProto(btns.find(btn => btn.title === "redo")!).opacity = UndoManager.CanRedo() ? 1 : 0.4, { fireImmediately: true }); reaction(() => UndoManager.undoStack.slice(), () => Doc.GetProto(btns.find(btn => btn.title === "undo")!).opacity = UndoManager.CanUndo() ? 1 : 0.4, { fireImmediately: true }); @@ -623,31 +627,31 @@ export class CurrentUserUtils { } static viewTools(): Button[] { return [ - { title: "Snap\xA0Lines", icon: "th", toolTip: "Show Snap Lines", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"snap lines", funcs: {}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform + { title: "Snap\xA0Lines", icon: "th", toolTip: "Show Snap Lines", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"snaplines", funcs: {}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform { title: "Grid", icon: "border-all", toolTip: "Show Grid", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"grid", funcs: {}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform { title: "View\xA0All", icon: "object-group", toolTip: "Fit all Docs to View",btnType: ButtonType.ToggleButton, expertMode: false, toolType:"viewAll", funcs: {}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform { title: "Clusters", icon: "braille", toolTip: "Show Doc Clusters", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"clusters", funcs: {}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform - { title: "Arrange",icon: "arrow-down-short-wide",toolTip: "Toggle Auto Arrange",btnType: ButtonType.ToggleButton, expertMode: false, toolType:"arrange", funcs: {hidden: 'IsNoviceMode()'}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform - { title: "Reset", icon: "check", toolTip: "Reset View", btnType: ButtonType.ClickButton, expertMode: false, backgroundColor:"transparent", scripts: { onClick: 'resetView()'}}, // Only when floating document is selected in freeform + { title: "Cards", icon: "brain", toolTip: "Flashcards", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"flashcards", funcs: {}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform + { title: "Arrange",icon:"arrow-down-short-wide",toolTip:"Toggle Auto Arrange", btnType: ButtonType.ToggleButton, expertMode: false, toolType:"arrange", funcs: {hidden: 'IsNoviceMode()'}, scripts: { onClick: 'showFreeform(self.toolType, _readOnly_)'}}, // Only when floating document is selected in freeform ] } static textTools():Button[] { return [ - { title: "Font", toolTip: "Font", width: 100, btnType: ButtonType.DropdownList, toolType:"font", ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, value, _readOnly_);}'}, + { title: "Font", toolTip: "Font", width: 100, btnType: ButtonType.DropdownList, toolType:"font", ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, value, _readOnly_);}'}, btnList: new List<string>(["Roboto", "Roboto Mono", "Nunito", "Times New Roman", "Arial", "Georgia", "Comic Sans MS", "Tahoma", "Impact", "Crimson Text"]) }, - { title: "Size", toolTip: "Font size (%size)", btnType: ButtonType.NumberButton, width: 75, toolType:"fontSize", ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 0, numBtnType: NumButtonType.DropdownOptions }, - { title: "Color", toolTip: "Font color (%color)", btnType: ButtonType.ColorButton, icon: "font", toolType:"fontColor",ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, value, _readOnly_);}'}}, + { title: "Font Size",toolTip: "Font size (%size)", btnType: ButtonType.NumberDropdownButton, width: 75, toolType:"fontSize", ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, value, _readOnly_);}'}, numBtnMax: 200, numBtnMin: 0 }, + { title: "Color", toolTip: "Font color (%color)", btnType: ButtonType.ColorButton, icon: "font", toolType:"fontColor",ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, value, _readOnly_);}'}}, { title: "Highlight",toolTip:"Font highlight", btnType: ButtonType.ColorButton, icon: "highlighter", toolType:"highlight",ignoreClick: true, scripts: {script: '{ return setFontAttr(self.toolType, value, _readOnly_);}'},funcs: {hidden: "IsNoviceMode()"} }, - { title: "Bold", toolTip: "Bold (Ctrl+B)", btnType: ButtonType.ToggleButton, icon: "bold", toolType:"bold", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, - { title: "Italic", toolTip: "Italic (Ctrl+I)", btnType: ButtonType.ToggleButton, icon: "italic", toolType:"italics", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, - { title: "Under", toolTip: "Underline (Ctrl+U)", btnType: ButtonType.ToggleButton, icon: "underline", toolType:"underline", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, - { title: "Bullets", toolTip: "Bullet List", btnType: ButtonType.ToggleButton, icon: "list", toolType:"bullet", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, - { title: "#", toolTip: "Number List", btnType: ButtonType.ToggleButton, icon: "list-ol", toolType:"decimal", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, - { title: "Left", toolTip: "Left align (Cmd-[)", btnType: ButtonType.ToggleButton, icon: "align-left", toolType:"left", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}' }}, - { title: "Center", toolTip: "Center align (Cmd-\\)",btnType: ButtonType.ToggleButton, icon: "align-center",toolType:"center", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, - { title: "Right", toolTip: "Right align (Cmd-])", btnType: ButtonType.ToggleButton, icon: "align-right", toolType:"right", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, - { title: "Dictate", toolTip: "Dictate", btnType: ButtonType.ToggleButton, icon: "microphone", toolType:"dictation", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'}}, - { title: "NoLink", toolTip: "Auto Link", btnType: ButtonType.ToggleButton, icon: "link", toolType:"noAutoLink", expertMode:true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'}, funcs: {hidden: 'IsNoviceMode()'}}, + { title: "Bold", toolTip: "Bold (Ctrl+B)", btnType: ButtonType.ToggleButton, icon: "bold", toolType:"bold", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, + { title: "Italic", toolTip: "Italic (Ctrl+I)", btnType: ButtonType.ToggleButton, icon: "italic", toolType:"italics", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, + { title: "Under", toolTip: "Underline (Ctrl+U)", btnType: ButtonType.ToggleButton, icon: "underline", toolType:"underline", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, + { title: "Bullets", toolTip: "Bullet List", btnType: ButtonType.ToggleButton, icon: "list", toolType:"bullet", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, + { title: "#", toolTip: "Number List", btnType: ButtonType.ToggleButton, icon: "list-ol", toolType:"decimal", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, + { title: "Left", toolTip: "Left align (Cmd-[)", btnType: ButtonType.ToggleButton, icon: "align-left", toolType:"left", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}' }}, + { title: "Center", toolTip: "Center align (Cmd-\\)",btnType: ButtonType.ToggleButton, icon: "align-center",toolType:"center", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, + { title: "Right", toolTip: "Right align (Cmd-])", btnType: ButtonType.ToggleButton, icon: "align-right", toolType:"right", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'} }, + { title: "Dictate", toolTip: "Dictate", btnType: ButtonType.ToggleButton, icon: "microphone", toolType:"dictation", scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'}}, + { title: "NoLink", toolTip: "Auto Link", btnType: ButtonType.ToggleButton, icon: "link", toolType:"noAutoLink", expertMode:true, scripts: {onClick: '{ return toggleCharStyle(self.toolType, _readOnly_);}'}, funcs: {hidden: 'IsNoviceMode()'}}, // { title: "Strikethrough", tooltip: "Strikethrough", btnType: ButtonType.ToggleButton, icon: "strikethrough", scripts: {onClick:: 'toggleStrikethrough()'}}, // { title: "Superscript", tooltip: "Superscript", btnType: ButtonType.ToggleButton, icon: "superscript", scripts: {onClick:: 'toggleSuperscript()'}}, // { title: "Subscript", tooltip: "Subscript", btnType: ButtonType.ToggleButton, icon: "subscript", scripts: {onClick:: 'toggleSubscript()'}}, @@ -663,13 +667,16 @@ export class CurrentUserUtils { { title: "Square", toolTip: "Square (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "square", toolType:GestureUtils.Gestures.Rectangle, scripts: {onClick:`{ return setActiveTool(self.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(self.toolType, true, _readOnly_);}`} }, { title: "Line", toolTip: "Line (double tap to lock mode)", btnType: ButtonType.ToggleButton, icon: "minus", toolType:GestureUtils.Gestures.Line, scripts: {onClick:`{ return setActiveTool(self.toolType, false, _readOnly_);}`, onDoubleClick:`{ return setActiveTool(self.toolType, true, _readOnly_);}`} }, { title: "Mask", toolTip: "Mask", btnType: ButtonType.ToggleButton, icon: "user-circle",toolType: "inkMask", scripts: {onClick:'{ return setInkProperty(self.toolType, value, _readOnly_);}'} }, - { title: "Width", toolTip: "Stroke width", btnType: ButtonType.NumberButton, toolType: "strokeWidth", ignoreClick: true, scripts: {script: '{ return setInkProperty(self.toolType, value, _readOnly_);}'}, numBtnType: NumButtonType.Slider, numBtnMin: 1}, + { title: "Width", toolTip: "Stroke width", btnType: ButtonType.NumberSliderButton, toolType: "strokeWidth", ignoreClick: true, scripts: {script: '{ return setInkProperty(self.toolType, value, _readOnly_);}'}, numBtnMin: 1}, { title: "Ink", toolTip: "Stroke color", btnType: ButtonType.ColorButton, icon: "pen", toolType: "strokeColor", ignoreClick: true, scripts: {script: '{ return setInkProperty(self.toolType, value, _readOnly_);}'} }, ]; } static schemaTools():Button[] { - return [{ title: "Show preview", toolTip: "Show selection preview", btnType: ButtonType.ToggleButton, buttonText: "Show Preview", icon: "eye", scripts:{ onClick: '{ return toggleSchemaPreview(_readOnly_); }'}, }]; + return [ + {title: "Show preview", toolTip: "Show selection preview", btnType: ButtonType.ToggleButton, buttonText: "Show Preview", icon: "eye", scripts:{ onClick: '{ return toggleSchemaPreview(_readOnly_); }'} }, + {title: "Single Lines", toolTip: "Single Line Rows", btnType: ButtonType.ToggleButton, buttonText: "Single Line", icon: "eye", scripts:{ onClick: '{ return toggleSingleLineSchema(_readOnly_); }'} }, + ]; } static webTools() { @@ -695,12 +702,12 @@ export class CurrentUserUtils { { title: "Back", icon: "chevron-left", toolTip: "Prev Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)'}, width: 20, scripts: { onClick: 'prevKeyFrame(_readOnly_)'}}, { title: "Num", icon:"",toolTip: "Frame Number (click to toggle edit mode)",btnType: ButtonType.TextButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)', buttonText: 'selectedDocs()?.lastElement()?.currentFrame?.toString()'}, width: 20, scripts: { onClick: '{ return curKeyFrame(_readOnly_);}'}}, { title: "Fwd", icon: "chevron-right", toolTip: "Next Animation Frame", btnType: ButtonType.ClickButton, expertMode: true, toolType:CollectionViewType.Freeform, funcs: {hidden: '!SelectionManager_selectedDocType(self.toolType, self.expertMode)'}, width: 20, scripts: { onClick: 'nextKeyFrame(_readOnly_)'}}, - { title: "Text", icon: "Text", toolTip: "Text functions", subMenu: CurrentUserUtils.textTools(), expertMode: false, toolType:DocumentType.RTF, funcs: { linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available - { title: "Ink", icon: "Ink", toolTip: "Ink functions", subMenu: CurrentUserUtils.inkTools(), expertMode: false, toolType:DocumentType.INK, funcs: { linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`}, scripts: { onClick: 'setInkToolDefaults()'} }, // Always available - { title: "Doc", icon: "Doc", toolTip: "Freeform Doc tools", subMenu: CurrentUserUtils.freeTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode, true)`, linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available - { title: "View", icon: "View", toolTip: "View tools", subMenu: CurrentUserUtils.viewTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available - { title: "Web", icon: "Web", toolTip: "Web functions", subMenu: CurrentUserUtils.webTools(), expertMode: false, toolType:DocumentType.WEB, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Only when Web is selected - { title: "Schema", icon: "Schema",linearBtnWidth:58,toolTip: "Schema functions", subMenu: CurrentUserUtils.schemaTools(), expertMode: false, toolType:CollectionViewType.Schema, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearViewIsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} } // Only when Schema is selected + { title: "Text", icon: "Text", toolTip: "Text functions", subMenu: CurrentUserUtils.textTools(), expertMode: false, toolType:DocumentType.RTF, funcs: { linearView_IsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available + { title: "Ink", icon: "Ink", toolTip: "Ink functions", subMenu: CurrentUserUtils.inkTools(), expertMode: false, toolType:DocumentType.INK, funcs: { linearView_IsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`}, scripts: { onClick: 'setInkToolDefaults()'} }, // Always available + { title: "Doc", icon: "Doc", toolTip: "Freeform Doc tools", subMenu: CurrentUserUtils.freeTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode, true)`, linearView_IsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available + { title: "View", icon: "View", toolTip: "View tools", subMenu: CurrentUserUtils.viewTools(), expertMode: false, toolType:CollectionViewType.Freeform, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearView_IsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Always available + { title: "Web", icon: "Web", toolTip: "Web functions", subMenu: CurrentUserUtils.webTools(), expertMode: false, toolType:DocumentType.WEB, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearView_IsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} }, // Only when Web is selected + { title: "Schema", icon: "Schema",linearBtnWidth:58,toolTip: "Schema functions",subMenu: CurrentUserUtils.schemaTools(), expertMode: false, toolType:CollectionViewType.Schema, funcs: {hidden: `!SelectionManager_selectedDocType(self.toolType, self.expertMode)`, linearView_IsExpanded: `SelectionManager_selectedDocType(self.toolType, self.expertMode)`} } // Only when Schema is selected ]; } @@ -709,11 +716,11 @@ export class CurrentUserUtils { const reqdOpts:DocumentOptions = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit, backgroundColor: params.backgroundColor ??"transparent", /// a bit hacky. if an onClick is specified, then assume a toggle uses onClick to get the backgroundColor (see below). Otherwise, assume a transparent background - color: Colors.WHITE, system: true, dontUndo: true, + color: Colors.WHITE, isSystem: true, //dontUndo: true, _nativeWidth: params.width ?? 30, _width: params.width ?? 30, _height: 30, _nativeHeight: 30, linearBtnWidth: params.linearBtnWidth, toolType: params.toolType, expertMode: params.expertMode, - _stayInCollection: true, _hideContextMenu: true, _lockedPosition: true, + _stayInCollection: true, _layout_hideContextMenu: true, _lockedPosition: true, _removeDropProperties: new List<string>([ "_stayInCollection"]), }; const reqdFuncs:{[key:string]:any} = { @@ -725,16 +732,16 @@ export class CurrentUserUtils { /// Initializes all the default buttons for the top bar context menu static setupContextMenuButtons(doc: Doc, field="myContextMenuBtns") { - const reqdCtxtOpts:DocumentOptions = { title: "context menu buttons", flexGap: 0, childDropAction: 'alias', childDontRegisterViews: true, linearViewIsExpanded: true, ignoreClick: true, linearViewExpandable: false, _height: 35 }; + const reqdCtxtOpts:DocumentOptions = { title: "context menu buttons", dontUndo:true, flexGap: 0, childDropAction: 'embed', childDontRegisterViews: true, linearView_IsExpanded: true, ignoreClick: true, linearView_Expandable: false, _height: 35 }; const ctxtMenuBtnsDoc = DocUtils.AssignDocField(doc, field, (opts, items) => this.linearButtonList(opts, items??[]), reqdCtxtOpts, undefined); const ctxtMenuBtns = CurrentUserUtils.contextMenuTools().map(params => { const menuBtnDoc = DocListCast(ctxtMenuBtnsDoc?.data).find(doc => doc.title === params.title); if (!params.subMenu) { return this.setupContextMenuButton(params, menuBtnDoc); } else { - const reqdSubMenuOpts = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit, + const reqdSubMenuOpts = { ...OmitKeys(params, ["scripts", "funcs", "subMenu"]).omit, dontUndo: true, childDontRegisterViews: true, flexGap: 0, _height: 30, ignoreClick: params.scripts?.onClick ? false : true, - linearViewSubMenu: true, linearViewExpandable: true, }; + linearView_SubMenu: true, linearView_Expandable: true, }; const items = params.subMenu?.map(sub => this.setupContextMenuButton(sub, DocListCast(menuBtnDoc?.data).find(doc => doc.title === sub.title)) ); @@ -747,11 +754,11 @@ export class CurrentUserUtils { /// collection of documents rendered in the overlay layer above all tabs and other UI static setupOverlays(doc: Doc, field = "myOverlayDocs") { - return DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.FreeformDocument([], opts), { title: "overlay documents", backgroundColor: "#aca3a6", system: true }); + return DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.FreeformDocument([], opts), { title: "overlay documents", backgroundColor: "#aca3a6", isSystem: true }); } static setupPublished(doc:Doc, field = "myPublishedDocs") { - return DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.TreeDocument([], opts), { title: "published docs", backgroundColor: "#aca3a6", system: true }); + return DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.TreeDocument([], opts), { title: "published docs", backgroundColor: "#aca3a6", isSystem: true }); } /// The database of all links on all documents @@ -772,7 +779,7 @@ export class CurrentUserUtils { // The sharing document also stores the user's color value which helps distinguish shared documents from personal documents static setupSharedDocs(doc: Doc, sharingDocumentId: string) { const addToDashboards = ScriptField.MakeScript(`addToDashboards(self)`); - const dashboardFilter = ScriptField.MakeFunction(`doc._viewType === '${CollectionViewType.Docking}'`, { doc: Doc.name }); + const dashboardFilter = ScriptField.MakeFunction(`doc._type_collection === '${CollectionViewType.Docking}'`, { doc: Doc.name }); const dblClkScript = "{scriptContext.openLevel(documentView); addDocToList(scriptContext.props.treeView.props.Document, 'viewed', documentView.rootDoc);}"; const sharedScripts = { treeViewChildDoubleClick: dblClkScript, } @@ -785,10 +792,10 @@ export class CurrentUserUtils { childContextMenuLabels: new List<string>(["Add to Dashboards",]), childContextMenuIcons: new List<string>(["user-plus",]), "acl-Public": SharingPermissions.Augment, "_acl-Public": SharingPermissions.Augment, - childDropAction: "alias", system: true, contentPointerEvents: "all", childLimitHeight: 0, _yMargin: 0, _gridGap: 15, childDontRegisterViews:true, - // NOTE: treeViewHideTitle & _showTitle is for a TreeView's editable title, _showTitle is for DocumentViews title bar - _showTitle: "title", treeViewHideTitle: true, ignoreClick: true, _lockedPosition: true, boxShadow: "0 0", _chromeHidden: true, dontRegisterView: true, - explainer: "This is where documents or dashboards that other users have shared with you will appear. To share a document or dashboard right click and select 'Share'" + childDropAction: "embed", isSystem: true, contentPointerEvents: "all", childLimitHeight: 0, _yMargin: 0, _gridGap: 15, childDontRegisterViews:true, + // NOTE: treeViewHideTitle & _layout_showTitle is for a TreeView's editable title, _layout_showTitle is for DocumentViews title bar + _layout_showTitle: "title", treeViewHideTitle: true, ignoreClick: true, _lockedPosition: true, layout_boxShadow: "0 0", _chromeHidden: true, dontRegisterView: true, + layout_explainer: "This is where documents or dashboards that other users have shared with you will appear. To share a document or dashboard right click and select 'Share'" }; DocUtils.AssignDocField(doc, "mySharedDocs", opts => Docs.Create.TreeDocument([], opts, sharingDocumentId + "layout", sharingDocumentId), sharedDocOpts, undefined, sharedScripts); @@ -797,17 +804,17 @@ export class CurrentUserUtils { /// Import option on the left side button panel static setupImportSidebar(doc: Doc, field:string) { const reqdOpts:DocumentOptions = { - title: "My Imports", _forceActive: true, buttonMenu: true, ignoreClick: true, _showTitle: "title", - _stayInCollection: true, _hideContextMenu: true, childLimitHeight: 0, - childDropAction: "copy", _autoHeight: true, _yMargin: 50, _gridGap: 15, boxShadow: "0 0", _lockedPosition: true, system: true, _chromeHidden: true, - dontRegisterView: true, explainer: "This is where documents that are Imported into Dash will go." + title: "My Imports", _forceActive: true, ignoreClick: true, _layout_showTitle: "title", + _stayInCollection: true, _layout_hideContextMenu: true, childLimitHeight: 0, + childDropAction: "copy", _layout_autoHeight: true, _yMargin: 50, _gridGap: 15, layout_boxShadow: "0 0", _lockedPosition: true, isSystem: true, _chromeHidden: true, + dontRegisterView: true, layout_explainer: "This is where documents that are Imported into Dash will go." }; const myImports = DocUtils.AssignDocField(doc, field, (opts) => Docs.Create.StackingDocument([], opts), reqdOpts); const reqdBtnOpts:DocumentOptions = { _forceActive: true, toolTip: "Import from computer", - _width: 30, _height: 30, _stayInCollection: true, _hideContextMenu: true, title: "Import", btnType: ButtonType.ClickButton, - buttonText: "Import", icon: "upload", system: true }; - DocUtils.AssignDocField(myImports, "buttonMenuDoc", (opts) => Docs.Create.FontIconDocument(opts), reqdBtnOpts, undefined, { onClick: "importDocument()" }); + _width: 30, _height: 30, _stayInCollection: true, _layout_hideContextMenu: true, title: "Import", btnType: ButtonType.ClickButton, + buttonText: "Import", icon: "upload", isSystem: true }; + DocUtils.AssignDocField(myImports, "layout_headerButton", (opts) => Docs.Create.FontIconDocument(opts), reqdBtnOpts, undefined, { onClick: "importDocument()" }); return myImports; } /// Updates the UserDoc to have all required fields, docs, etc. No changes should need to be @@ -815,13 +822,13 @@ export class CurrentUserUtils { /// whether to revert to "default" values, or to leave them as the user/system last set them. static updateUserDocument(doc: Doc, sharingDocumentId: string, linkDatabaseId: string) { DocUtils.AssignDocField(doc, "globalGroupDatabase", () => Docs.Prototypes.MainGroupDocument(), {}); - reaction(() => DateCast(DocCast(doc.globalGroupDatabase)["data-lastModified"]), + reaction(() => DateCast(DocCast(doc.globalGroupDatabase)["data_modificationDate"]), async () => { const groups = await DocListCastAsync(DocCast(doc.globalGroupDatabase).data); const mygroups = groups?.filter(group => JSON.parse(StrCast(group.members)).includes(Doc.CurrentUserEmail)) || []; SetCachedGroups(["Public", ...mygroups?.map(g => StrCast(g.title))]); }, { fireImmediately: true }); - doc.system ?? (doc.system = true); + doc.isSystem ?? (doc.isSystem = true); doc.title ?? (doc.title = Doc.CurrentUserEmail); Doc.noviceMode ?? (Doc.noviceMode = true); doc._raiseWhenDragged ?? (doc._raiseWhenDragged = true); @@ -844,7 +851,7 @@ export class CurrentUserUtils { doc.defaultAclPrivate ?? (doc.defaultAclPrivate = false); doc.savedFilters ?? (doc.savedFilters = new List<Doc>()); doc.filterDocCount = 0; - doc.freezeChildren = "remove|add"; + doc.treeViewFreezeChildren = "remove|add"; this.setupLinkDocs(doc, linkDatabaseId); this.setupSharedDocs(doc, sharingDocumentId); // sets up the right sidebar collection for mobile upload documents and sharing this.setupDefaultIconTemplates(doc); // creates a set of icon templates triggered by the document deoration icon @@ -857,7 +864,7 @@ export class CurrentUserUtils { this.setupDocTemplates(doc); // sets up the template menu of templates this.setupFieldInfos(doc); // sets up the collection of field info descriptions for each possible DocumentOption DocUtils.AssignDocField(doc, "globalScriptDatabase", (opts) => Docs.Prototypes.MainScriptDocument(), {}); - DocUtils.AssignDocField(doc, "myHeaderBar", (opts) => Docs.Create.MulticolumnDocument([], opts), { title: "header bar", system: true }); // drop down panel at top of dashboard for stashing documents + DocUtils.AssignDocField(doc, "myHeaderBar", (opts) => Docs.Create.MulticolumnDocument([], opts), { title: "header bar", isSystem: true }); // drop down panel at top of dashboard for stashing documents Doc.AddDocToList(Doc.MyFilesystem, undefined, Doc.MySharedDocs) @@ -871,13 +878,13 @@ export class CurrentUserUtils { return doc; } static setupFieldInfos(doc:Doc, field="fieldInfos") { - const fieldInfoOpts = { title: "Field Infos", system: true}; // bcz: all possible document options have associated field infos which are stored onn the FieldInfos document **except for title and system which are used as part of the definition of the fieldInfos object + const fieldInfoOpts = { title: "Field Infos", isSystem: true}; // bcz: all possible document options have associated field infos which are stored onn the FieldInfos document **except for title and system which are used as part of the definition of the fieldInfos object const infos = DocUtils.AssignDocField(doc, field, opts => Doc.assign(new Doc(), opts as any), fieldInfoOpts); const entries = Object.entries(new DocumentOptions()); entries.forEach(pair => { if (!Array.from(Object.keys(fieldInfoOpts)).includes(pair[0])) { const options = pair[1] as FInfo; - const opts:DocumentOptions = { system: true, title: pair[0], ...OmitKeys(options, ["values"]).omit, fieldIsLayout: pair[0].startsWith("_")}; + const opts:DocumentOptions = { isSystem: true, title: pair[0], ...OmitKeys(options, ["values"]).omit, fieldIsLayout: pair[0].startsWith("_")}; switch (options.fieldType) { case "boolean": opts.fieldValues = new List<boolean>(options.values as any); break; case "number": opts.fieldValues = new List<number>(options.values as any); break; diff --git a/src/client/util/DictationManager.ts b/src/client/util/DictationManager.ts index e116ae14b..717473aa1 100644 --- a/src/client/util/DictationManager.ts +++ b/src/client/util/DictationManager.ts @@ -5,7 +5,7 @@ import { Doc, Opt } from '../../fields/Doc'; import { List } from '../../fields/List'; import { RichTextField } from '../../fields/RichTextField'; import { listSpec } from '../../fields/Schema'; -import { Cast, CastCtor } from '../../fields/Types'; +import { Cast, CastCtor, DocCast } from '../../fields/Types'; import { AudioField, ImageField } from '../../fields/URLField'; import { Utils } from '../../Utils'; import { Docs } from '../documents/Documents'; @@ -327,8 +327,8 @@ export namespace DictationManager { 'new outline', { action: (target: DocumentView) => { - const newBox = Docs.Create.TextDocument('', { _width: 400, _height: 200, title: 'My Outline', _autoHeight: true }); - const proto = newBox.proto!; + const newBox = Docs.Create.TextDocument('', { _width: 400, _height: 200, title: 'My Outline', _layout_autoHeight: true }); + const proto = DocCast(newBox.proto); const prompt = 'Press alt + r to start dictating here...'; const head = 3; const anchor = head + prompt.length; @@ -372,7 +372,7 @@ export namespace DictationManager { expression: /view as (freeform|stacking|masonry|schema|tree)/g, action: (target: DocumentView, matches: RegExpExecArray) => { const mode = matches[1]; - mode && (target.props.Document._viewType = mode); + mode && (target.props.Document._type_collection = mode); }, restrictTo: [DocumentType.COL], } diff --git a/src/client/util/DocumentManager.ts b/src/client/util/DocumentManager.ts index 3a192f712..612f16ce9 100644 --- a/src/client/util/DocumentManager.ts +++ b/src/client/util/DocumentManager.ts @@ -1,5 +1,6 @@ -import { action, observable, ObservableSet } from 'mobx'; -import { AnimationSym, Doc, Opt } from '../../fields/Doc'; +import { action, computed, observable, ObservableSet } from 'mobx'; +import { Doc, Opt } from '../../fields/Doc'; +import { Animation } from '../../fields/DocSymbols'; import { Id } from '../../fields/FieldSymbols'; import { listSpec } from '../../fields/Schema'; import { Cast, DocCast, StrCast } from '../../fields/Types'; @@ -10,6 +11,7 @@ import { TabDocView } from '../views/collections/TabDocView'; import { LightboxView } from '../views/LightboxView'; import { DocFocusOptions, DocumentView, DocumentViewInternal, OpenWhere, OpenWhereMod } from '../views/nodes/DocumentView'; import { FormattedTextBox } from '../views/nodes/formattedText/FormattedTextBox'; +import { KeyValueBox } from '../views/nodes/KeyValueBox'; import { LinkAnchorBox } from '../views/nodes/LinkAnchorBox'; import { PresBox } from '../views/nodes/trails'; import { ScriptingGlobals } from './ScriptingGlobals'; @@ -18,10 +20,13 @@ const { Howl } = require('howler'); export class DocumentManager { //global holds all of the nodes (regardless of which collection they're in) - @observable public DocumentViews = new Set<DocumentView>(); + @observable _documentViews = new Set<DocumentView>(); @observable public LinkAnchorBoxViews: DocumentView[] = []; @observable public RecordingEvent = 0; @observable public LinkedDocumentViews: { a: DocumentView; b: DocumentView; l: Doc }[] = []; + @computed public get DocumentViews() { + return Array.from(this._documentViews).filter(view => !(view.ComponentView instanceof KeyValueBox)); + } private static _instance: DocumentManager; public static get Instance(): DocumentManager { @@ -65,12 +70,12 @@ export class DocumentManager { public AddView = (view: DocumentView) => { //console.log("MOUNT " + view.props.Document.title + "/" + view.props.LayoutTemplateString); if (view.props.LayoutTemplateString?.includes(LinkAnchorBox.name)) { - const viewAnchorIndex = view.props.LayoutTemplateString.includes('anchor2') ? 'anchor2' : 'anchor1'; + const viewAnchorIndex = view.props.LayoutTemplateString.includes('link_anchor_2') ? 'link_anchor_2' : 'link_anchor_1'; const link = view.rootDoc; this.LinkAnchorBoxViews?.filter(dv => Doc.AreProtosEqual(dv.rootDoc, link) && !dv.props.LayoutTemplateString?.includes(viewAnchorIndex)).forEach(otherView => this.LinkedDocumentViews.push({ - a: viewAnchorIndex === 'anchor2' ? otherView : view, - b: viewAnchorIndex === 'anchor2' ? view : otherView, + a: viewAnchorIndex === 'link_anchor_2' ? otherView : view, + b: viewAnchorIndex === 'link_anchor_2' ? view : otherView, l: link, }) ); @@ -78,7 +83,7 @@ export class DocumentManager { // this.LinkedDocumentViews.forEach(view => console.log(" LV = " + view.a.props.Document.title + "/" + view.a.props.LayoutTemplateString + " --> " + // view.b.props.Document.title + "/" + view.b.props.LayoutTemplateString)); } else { - this.DocumentViews.add(view); + this._documentViews.add(view); } this.callAddViewFuncs(view); }; @@ -96,7 +101,7 @@ export class DocumentManager { const index = this.LinkAnchorBoxViews.indexOf(view); this.LinkAnchorBoxViews.splice(index, 1); } else { - this.DocumentViews.delete(view); + this._documentViews.delete(view); } SelectionManager.DeselectView(view); }); @@ -104,15 +109,14 @@ export class DocumentManager { //gets all views public getDocumentViewsById(id: string) { const toReturn: DocumentView[] = []; - Array.from(DocumentManager.Instance.DocumentViews).map(view => { + DocumentManager.Instance.DocumentViews.forEach(view => { if (view.rootDoc[Id] === id) { toReturn.push(view); } }); if (toReturn.length === 0) { - Array.from(DocumentManager.Instance.DocumentViews).map(view => { - const doc = view.rootDoc.proto; - if (doc && doc[Id] && doc[Id] === id) { + DocumentManager.Instance.DocumentViews.forEach(view => { + if (Doc.GetProto(view.rootDoc)?.[Id] === id) { toReturn.push(view); } }); @@ -133,20 +137,20 @@ export class DocumentManager { // ((DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href && (DocCast(dv.rootDoc.annotationOn)?.data as any)?.url?.href === (DocCast(toFind.annotationOn)?.data as any)?.url?.href) // )?.rootDoc ?? toFind; - const docViewArray = Array.from(DocumentManager.Instance.DocumentViews); + const docViewArray = DocumentManager.Instance.DocumentViews; const passes = !doc ? [] : preferredCollection ? [preferredCollection, undefined] : [undefined]; return passes.reduce( (pass, toReturn) => toReturn ?? docViewArray.filter(view => view.rootDoc === doc).find(view => !pass || view.props.docViewPath().lastElement() === preferredCollection) ?? - docViewArray.filter(view => Doc.GetProto(view.rootDoc) === doc).find(view => !pass || view.props.docViewPath().lastElement() === preferredCollection), + docViewArray.filter(view => Doc.AreProtosEqual(view.rootDoc, doc)).find(view => !pass || view.props.docViewPath().lastElement() === preferredCollection), undefined as Opt<DocumentView> ); } public getLightboxDocumentView = (toFind: Doc, originatingDoc: Opt<Doc> = undefined): DocumentView | undefined => { const views: DocumentView[] = []; - Array.from(DocumentManager.Instance.DocumentViews).map(view => LightboxView.IsLightboxDocView(view.docViewPath) && Doc.AreProtosEqual(view.rootDoc, toFind) && views.push(view)); + DocumentManager.Instance.DocumentViews.forEach(view => LightboxView.IsLightboxDocView(view.docViewPath) && Doc.AreProtosEqual(view.rootDoc, toFind) && views.push(view)); return views?.find(view => view.ContentDiv?.getBoundingClientRect().width /*&& view.props.focus !== returnFalse) || views?.find(view => view.props.focus !== returnFalse*/) || (views.length ? views[0] : undefined); }; public getFirstDocumentView = (toFind: Doc, originatingDoc: Opt<Doc> = undefined): DocumentView | undefined => { @@ -164,12 +168,12 @@ export class DocumentManager { toFindIn; const toReturn: DocumentView[] = []; - const docViews = Array.from(DocumentManager.Instance.DocumentViews).filter(view => !LightboxView.IsLightboxDocView(view.docViewPath)); - const lightViews = Array.from(DocumentManager.Instance.DocumentViews).filter(view => LightboxView.IsLightboxDocView(view.docViewPath)); + const docViews = DocumentManager.Instance.DocumentViews.filter(view => !LightboxView.IsLightboxDocView(view.docViewPath)); + const lightViews = DocumentManager.Instance.DocumentViews.filter(view => LightboxView.IsLightboxDocView(view.docViewPath)); // heuristic to return the "best" documents first: // choose a document in the lightbox first - // choose an exact match over an alias match + // choose an exact match over an embedding match lightViews.map(view => view.rootDoc === toFind && toReturn.push(view)); lightViews.map(view => view.rootDoc !== toFind && Doc.AreProtosEqual(view.rootDoc, toFind) && toReturn.push(view)); docViews.map(view => view.rootDoc === toFind && toReturn.push(view)); @@ -180,15 +184,15 @@ export class DocumentManager { static GetContextPath(doc: Opt<Doc>, includeExistingViews?: boolean) { if (!doc) return []; - const srcContext = DocCast(doc.annotationOn, DocCast(doc.context)); + const srcContext = DocCast(doc.annotationOn, DocCast(doc.embedContainer)); var containerDocContext = srcContext ? [srcContext, doc] : [doc]; while ( containerDocContext.length && - containerDocContext[0]?.context && - DocCast(containerDocContext[0].context)?.viewType !== CollectionViewType.Docking && + containerDocContext[0]?.embedContainer && + DocCast(containerDocContext[0].embedContainer)?._type_collection !== CollectionViewType.Docking && (includeExistingViews || !DocumentManager.Instance.getDocumentView(containerDocContext[0])) ) { - containerDocContext = [Cast(containerDocContext[0].context, Doc, null), ...containerDocContext]; + containerDocContext = [Cast(containerDocContext[0].embedContainer, Doc, null), ...containerDocContext]; } return containerDocContext; } @@ -244,7 +248,10 @@ export class DocumentManager { if (docContextPath.some(doc => doc.hidden)) options.toggleTarget = false; let rootContextView = await new Promise<DocumentView>(res => { const viewIndex = docContextPath.findIndex(doc => this.getDocumentView(doc)); - if (viewIndex !== -1) return res(this.getDocumentView(docContextPath[viewIndex])!); + if (viewIndex !== -1) { + viewIndex && docContextPath.splice(0, viewIndex); + return res(this.getDocumentView(docContextPath[0])!); + } options.didMove = true; docContextPath.some(doc => TabDocView.Activate(doc)) || DocumentViewInternal.addDocTabFunc(docContextPath[0], options.openLocation ?? OpenWhere.addRight); this.AddViewRenderedCb(docContextPath[0], dv => res(dv)); @@ -252,7 +259,7 @@ export class DocumentManager { if (options.openLocation === OpenWhere.lightbox) { // even if we found the document view, if the target is a lightbox, we try to open it in the lightbox to preserve lightbox semantics (eg, there's only one active doc in the lightbox) const target = DocCast(targetDoc.annotationOn, targetDoc); - const contextView = this.getDocumentView(DocCast(target.context)); + const contextView = this.getDocumentView(DocCast(target.embedContainer)); if (contextView?.docView?._componentView?.addDocTab?.(target, OpenWhere.lightbox)) { await new Promise<void>(waitres => setTimeout(() => waitres())); } @@ -260,7 +267,7 @@ export class DocumentManager { docContextPath.shift(); const childViewIterator = async (docView: DocumentView) => { const innerDoc = docContextPath.shift(); - return { viewSpec: innerDoc, childDocView: innerDoc && !innerDoc.unrendered ? (await docView.ComponentView?.getView?.(innerDoc)) ?? this.getDocumentView(innerDoc) : undefined }; + return { viewSpec: innerDoc, childDocView: innerDoc && !innerDoc.layout_unrendered ? (await docView.ComponentView?.getView?.(innerDoc)) ?? this.getDocumentView(innerDoc) : undefined }; }; const target = await this.focusViewsInPath(rootContextView, options, childViewIterator); this.restoreDocView(target.viewSpec, target.docView, options, target.contextView ?? target.docView, targetDoc); @@ -271,7 +278,7 @@ export class DocumentManager { focusViewsInPath = async (docView: DocumentView, options: DocFocusOptions, iterator: (docView: DocumentView) => Promise<{ viewSpec: Opt<Doc>; childDocView: Opt<DocumentView> }>) => { let contextView: DocumentView | undefined; // view containing context that contains target while (true) { - docView.rootDoc.layoutKey === 'layout_icon' ? await new Promise<void>(res => docView.iconify(res)) : undefined; + docView.rootDoc.layout_fieldKey === 'layout_icon' ? await new Promise<void>(res => docView.iconify(res)) : undefined; docView.props.focus(docView.rootDoc, options); // focus the view within its container const { childDocView, viewSpec } = await iterator(docView); if (!childDocView) return { viewSpec: options.anchorDoc ?? viewSpec ?? docView.rootDoc, docView, contextView }; @@ -288,7 +295,7 @@ export class DocumentManager { Doc.linkFollowHighlight(docView.rootDoc, undefined, options.effect); if (options.playAudio) DocumentManager.playAudioAnno(docView.rootDoc); if (options.toggleTarget && (!options.didMove || docView.rootDoc.hidden)) docView.rootDoc.hidden = !docView.rootDoc.hidden; - if (options.effect) docView.rootDoc[AnimationSym] = options.effect; + if (options.effect) docView.rootDoc[Animation] = options.effect; if (options.zoomTextSelections && Doc.UnhighlightTimer && contextView && viewSpec.textHtml) { // if the docView is a text anchor, the contextView is the PDF/Web/Text doc @@ -297,7 +304,7 @@ export class DocumentManager { DocumentManager._overlayViews.add(contextView); } Doc.AddUnHighlightWatcher(() => { - docView.rootDoc[AnimationSym] = undefined; + docView.rootDoc[Animation] = undefined; DocumentManager.removeOverlayViews(); contextView && (contextView.htmlOverlayEffect = ''); }); @@ -311,7 +318,7 @@ export function DocFocusOrOpen(doc: Doc, options: DocFocusOptions = { willZoomCe if (dv && (!containingDoc || dv.props.docViewPath().lastElement()?.Document === containingDoc)) { DocumentManager.Instance.showDocumentView(dv, options).then(() => dv && Doc.linkFollowHighlight(dv.rootDoc)); } else { - const container = DocCast(containingDoc ?? doc.context); + const container = DocCast(containingDoc ?? doc.embedContainer ?? doc); const showDoc = !Doc.IsSystem(container) ? container : doc; options.toggleTarget = undefined; DocumentManager.Instance.showDocument(showDoc, options, () => DocumentManager.Instance.showDocument(doc, { ...options, openLocation: undefined })).then(() => { diff --git a/src/client/util/DragManager.ts b/src/client/util/DragManager.ts index b6de5604d..87833d322 100644 --- a/src/client/util/DragManager.ts +++ b/src/client/util/DragManager.ts @@ -15,7 +15,7 @@ import { SelectionManager } from './SelectionManager'; import { SnappingManager } from './SnappingManager'; import { UndoManager } from './UndoManager'; -export type dropActionType = 'alias' | 'copy' | 'move' | 'same' | 'proto' | 'none' | undefined; // undefined = move, "same" = move but don't call removeDropProperties +export type dropActionType = 'embed' | 'copy' | 'move' | 'same' | 'proto' | 'none' | undefined; // undefined = move, "same" = move but don't call removeDropProperties /** * Initialize drag @@ -131,7 +131,7 @@ export namespace DragManager { canEmbed?: boolean; userDropAction: dropActionType; // the user requested drop action -- this will be honored as specified by modifier keys defaultDropAction?: dropActionType; // an optionally specified default drop action when there is no user drop actionl - this will be honored if there is no user drop action - dropAction: dropActionType; // a drop action request by the initiating code. the actual drop action may be different -- eg, if the request is 'alias', but the document is dropped within the same collection, the drop action will be switched to 'move' + dropAction: dropActionType; // a drop action request by the initiating code. the actual drop action may be different -- eg, if the request is 'embed', but the document is dropped within the same collection, the drop action will be switched to 'move' removeDropProperties?: string[]; moveDocument?: MoveFunction; removeDocument?: RemoveFunction; @@ -174,6 +174,13 @@ export namespace DragManager { userDropAction: dropActionType; } + let defaultPreDropFunc = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => { + if (de.complete.docDragData) { + targetAction && (de.complete.docDragData.dropAction = targetAction); + e.stopPropagation(); + } + }; + export function MakeDropTarget(element: HTMLElement, dropFunc: (e: Event, de: DropEvent) => void, doc?: Doc, preDropFunc?: (e: Event, de: DropEvent, targetAction: dropActionType) => void): DragDropDisposer { if ('canDrop' in element.dataset) { throw new Error("Element is already droppable, can't make it droppable again"); @@ -182,7 +189,7 @@ export namespace DragManager { const handler = (e: Event) => dropFunc(e, (e as CustomEvent<DropEvent>).detail); const preDropHandler = (e: Event) => { const de = (e as CustomEvent<DropEvent>).detail; - preDropFunc?.(e, de, StrCast(doc?.targetDropAction) as dropActionType); + (preDropFunc ?? defaultPreDropFunc)(e, de, StrCast(doc?.targetDropAction) as dropActionType); }; element.addEventListener('dashOnDrop', handler); doc && element.addEventListener('dashPreDrop', preDropHandler); @@ -193,10 +200,10 @@ export namespace DragManager { }; } - // drag a document and drop it (or make an alias/copy on drop) + // drag a document and drop it (or make an embed/copy on drop) export function StartDocumentDrag(eles: HTMLElement[], dragData: DocumentDragData, downX: number, downY: number, options?: DragOptions, dropEvent?: () => any) { const addAudioTag = (dropDoc: any) => { - dropDoc && !dropDoc.creationDate && (dropDoc.creationDate = new DateField()); + dropDoc && !dropDoc.author_date && (dropDoc.author_date = new DateField()); dropDoc instanceof Doc && DocUtils.MakeLinkToActiveAudio(() => dropDoc); return dropDoc; }; @@ -210,8 +217,8 @@ export namespace DragManager { dragData.draggedDocuments.map(async d => !dragData.isDocDecorationMove && !dragData.userDropAction && ScriptCast(d.onDragStart) ? addAudioTag(ScriptCast(d.onDragStart).script.run({ this: d }).result) - : docDragData.dropAction === 'alias' - ? Doc.BestAlias(d) + : docDragData.dropAction === 'embed' + ? Doc.BestEmbedding(d) : docDragData.dropAction === 'proto' ? Doc.GetProto(d) : docDragData.dropAction === 'copy' @@ -263,7 +270,7 @@ export namespace DragManager { // drags a column from a schema view export function StartColumnDrag(ele: HTMLElement[], dragData: ColumnDragData, downX: number, downY: number, options?: DragOptions) { - StartDrag(ele, dragData, downX, downY, options); + StartDrag(ele, dragData, downX, downY, options, undefined, 'Drag Column'); } export function SetSnapLines(horizLines: number[], vertLines: number[]) { @@ -325,10 +332,10 @@ export namespace DragManager { export let docsBeingDragged: Doc[] = observable([] as Doc[]); export let CanEmbed = false; export let DocDragData: DocumentDragData | undefined; - export function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: DragCompleteEvent) => void) { + export function StartDrag(eles: HTMLElement[], dragData: { [id: string]: any }, downX: number, downY: number, options?: DragOptions, finishDrag?: (dropData: DragCompleteEvent) => void, dragUndoName?: string) { if (dragData.dropAction === 'none') return; DocDragData = dragData as DocumentDragData; - const batch = UndoManager.StartBatch('dragging'); + const batch = UndoManager.StartBatch(dragUndoName ?? 'document drag'); eles = eles.filter(e => e); CanEmbed = dragData.canEmbed || false; if (!dragDiv) { @@ -487,7 +494,7 @@ export namespace DragManager { const moveHandler = (e: PointerEvent) => { e.preventDefault(); // required or dragging text menu link item ends up dragging the link button as native drag/drop if (dragData instanceof DocumentDragData) { - dragData.userDropAction = e.ctrlKey && e.altKey ? 'copy' : e.ctrlKey ? 'alias' : dragData.defaultDropAction; + dragData.userDropAction = e.ctrlKey && e.altKey ? 'copy' : e.ctrlKey ? 'embed' : dragData.defaultDropAction; } if (((e.target as any)?.className === 'lm_tabs' || (e.target as any)?.className === 'lm_header' || e?.shiftKey) && dragData.draggedDocuments.length === 1) { if (!startWindowDragTimer) { @@ -510,7 +517,7 @@ export namespace DragManager { const target = document.elementFromPoint(e.x, e.y); - if (target && !Doc.UserDoc()._noAutoscroll && !options?.noAutoscroll && !dragData.draggedDocuments?.some((d: any) => d._noAutoscroll)) { + if (target && !Doc.UserDoc()._noAutoscroll && !options?.noAutoscroll && !dragData.draggedDocuments?.some((d: any) => d._freeform_noAutoPan)) { const autoScrollHandler = () => { target.dispatchEvent( new CustomEvent<React.DragEvent>('dashDragAutoScroll', { @@ -607,8 +614,8 @@ ScriptingGlobals.add(function toggleRaiseOnDrag(forAllDocs: boolean, readOnly?: ? 'transparent' : DragManager.GetRaiseWhenDragged() ? Colors.MEDIUM_BLUE_ALT - : Colors.PINK; - return DragManager.GetRaiseWhenDragged() ? Colors.PINK : 'transparent'; + : Colors.LIGHT_BLUE; + return DragManager.GetRaiseWhenDragged() ? Colors.MEDIUM_BLUE_ALT : 'transparent'; } if (!forAllDocs) SelectionManager.Views().map(dv => (dv.rootDoc.raiseWhenDragged ? (dv.rootDoc.raiseWhenDragged = undefined) : dv.rootDoc.raiseWhenDragged === false ? (dv.rootDoc.raiseWhenDragged = true) : (dv.rootDoc.raiseWhenDragged = false))); else DragManager.SetRaiseWhenDragged(!DragManager.GetRaiseWhenDragged()); diff --git a/src/client/util/DropConverter.ts b/src/client/util/DropConverter.ts index 2e1d6ba23..f46ea393a 100644 --- a/src/client/util/DropConverter.ts +++ b/src/client/util/DropConverter.ts @@ -58,8 +58,8 @@ export function convertDropDataToButtons(data: DragManager.DocumentDragData) { // bcz: isButtonBar is intended to allow a collection of linear buttons to be dropped and nested into another collection of buttons... it's not being used yet, and isn't very elegant if (doc.type === DocumentType.FONTICON || StrCast(Doc.Layout(doc).layout).includes('FontIconBox')) { if (data.removeDropProperties || dbox.removeDropProperties) { - //dbox = Doc.MakeAlias(doc); // don't need to do anything if dropping an icon doc onto an icon bar since there should be no layout data for an icon - dbox = Doc.MakeAlias(dbox); + //dbox = Doc.MakeEmbedding(doc); // don't need to do anything if dropping an icon doc onto an icon bar since there should be no layout data for an icon + dbox = Doc.MakeEmbedding(dbox); const dragProps = Cast(dbox.removeDropProperties, listSpec('string'), []); const remProps = (data.removeDropProperties || []).concat(Array.from(dragProps)); remProps.map(prop => (dbox[prop] = undefined)); diff --git a/src/client/util/GroupManager.tsx b/src/client/util/GroupManager.tsx index c8b784390..da947aba6 100644 --- a/src/client/util/GroupManager.tsx +++ b/src/client/util/GroupManager.tsx @@ -155,7 +155,7 @@ export class GroupManager extends React.Component<{}> { addGroup(groupDoc: Doc): boolean { if (this.GroupManagerDoc) { Doc.AddDocToList(this.GroupManagerDoc, 'data', groupDoc); - this.GroupManagerDoc['data-lastModified'] = new DateField(); + this.GroupManagerDoc['data_modificationDate'] = new DateField(); return true; } return false; @@ -176,7 +176,7 @@ export class GroupManager extends React.Component<{}> { const index = DocListCast(this.GroupManagerDoc.data).findIndex(grp => grp === group); index !== -1 && Cast(this.GroupManagerDoc.data, listSpec(Doc), [])?.splice(index, 1); } - this.GroupManagerDoc['data-lastModified'] = new DateField(); + this.GroupManagerDoc['data_modificationDate'] = new DateField(); if (group === this.currentGroup) { this.currentGroup = undefined; } @@ -197,7 +197,7 @@ export class GroupManager extends React.Component<{}> { !memberList.includes(email) && memberList.push(email); groupDoc.members = JSON.stringify(memberList); SharingManager.Instance.shareWithAddedMember(groupDoc, email); - this.GroupManagerDoc && (this.GroupManagerDoc['data-lastModified'] = new DateField()); + this.GroupManagerDoc && (this.GroupManagerDoc['data_modificationDate'] = new DateField()); } } @@ -214,7 +214,7 @@ export class GroupManager extends React.Component<{}> { const user = memberList.splice(index, 1)[0]; groupDoc.members = JSON.stringify(memberList); SharingManager.Instance.removeMember(groupDoc, email); - this.GroupManagerDoc && (this.GroupManagerDoc['data-lastModified'] = new DateField()); + this.GroupManagerDoc && (this.GroupManagerDoc['data_modificationDate'] = new DateField()); } } } diff --git a/src/client/util/HypothesisUtils.ts b/src/client/util/HypothesisUtils.ts index 297520d5d..151f18d6f 100644 --- a/src/client/util/HypothesisUtils.ts +++ b/src/client/util/HypothesisUtils.ts @@ -20,7 +20,7 @@ export namespace Hypothesis { export const getSourceWebDoc = async (uri: string) => { const result = await findWebDoc(uri); console.log(result ? 'existing doc found' : 'existing doc NOT found'); - return result || Docs.Create.WebDocument(uri, { title: uri, _nativeWidth: 850, _height: 512, _width: 400, useCors: true }); // create and return a new Web doc with given uri if no matching docs are found + return result || Docs.Create.WebDocument(uri, { title: uri, _nativeWidth: 850, _height: 512, _width: 400, data_useCors: true }); // create and return a new Web doc with given uri if no matching docs are found }; /** diff --git a/src/client/util/Import & Export/DirectoryImportBox.tsx b/src/client/util/Import & Export/DirectoryImportBox.tsx index b9bb22564..1a4c2450e 100644 --- a/src/client/util/Import & Export/DirectoryImportBox.tsx +++ b/src/client/util/Import & Export/DirectoryImportBox.tsx @@ -112,7 +112,7 @@ export class DirectoryImportBox extends React.Component<FieldViewProps> { sizes.push(file.size); modifiedDates.push(file.lastModified); }); - collector.push(...(await Networking.UploadFilesToServer<Upload.ImageInformation>(batch))); + collector.push(...(await Networking.UploadFilesToServer<Upload.ImageInformation>(batch.map(file =>({file}))))); runInAction(() => (this.completed += batch.length)); }); diff --git a/src/client/util/Import & Export/ImageUtils.ts b/src/client/util/Import & Export/ImageUtils.ts index 9bd92a316..55d37f544 100644 --- a/src/client/util/Import & Export/ImageUtils.ts +++ b/src/client/util/Import & Export/ImageUtils.ts @@ -1,12 +1,11 @@ -import { Doc } from "../../../fields/Doc"; -import { ImageField } from "../../../fields/URLField"; -import { Cast, StrCast, NumCast } from "../../../fields/Types"; -import { Networking } from "../../Network"; -import { Id } from "../../../fields/FieldSymbols"; -import { Utils } from "../../../Utils"; +import { Doc } from '../../../fields/Doc'; +import { ImageField } from '../../../fields/URLField'; +import { Cast, StrCast, NumCast } from '../../../fields/Types'; +import { Networking } from '../../Network'; +import { Id } from '../../../fields/FieldSymbols'; +import { Utils } from '../../../Utils'; export namespace ImageUtils { - export const ExtractExif = async (document: Doc): Promise<boolean> => { const field = Cast(document.data, ImageField); if (!field) { @@ -17,23 +16,22 @@ export namespace ImageUtils { contentSize, nativeWidth, nativeHeight, - exifData: { error, data } - } = await Networking.PostToServer("/inspectImage", { source }); + exifData: { error, data }, + } = await Networking.PostToServer('/inspectImage', { source }); document.exif = error || Doc.Get.FromJson({ data }); const proto = Doc.GetProto(document); - nativeWidth && (document._height = NumCast(document._width) * nativeHeight / nativeWidth); - proto["data-nativeWidth"] = nativeWidth; - proto["data-nativeHeight"] = nativeHeight; - proto["data-path"] = source; - proto.contentSize = contentSize ? contentSize : undefined; + nativeWidth && (document._height = (NumCast(document._width) * nativeHeight) / nativeWidth); + proto['data_nativeWidth'] = nativeWidth; + proto['data_nativeHeight'] = nativeHeight; + proto['data-path'] = source; + proto.data_contentSize = contentSize ? contentSize : undefined; return data !== undefined; }; export const ExportHierarchyToFileSystem = async (collection: Doc): Promise<void> => { - const a = document.createElement("a"); + const a = document.createElement('a'); a.href = Utils.prepend(`/imageHierarchyExport/${collection[Id]}`); a.download = `Dash Export [${StrCast(collection.title)}].zip`; a.click(); }; - -}
\ No newline at end of file +} diff --git a/src/client/util/InteractionUtils.tsx b/src/client/util/InteractionUtils.tsx index 9591dbed3..043f0f1f3 100644 --- a/src/client/util/InteractionUtils.tsx +++ b/src/client/util/InteractionUtils.tsx @@ -161,6 +161,7 @@ export namespace InteractionUtils { <polygon style={{ stroke: color }} strokeLinejoin={lineJoin as any} + strokeDasharray={dashArray} strokeWidth={(markerStrokeWidth * 2) / 3} points={`0 ${-markerStrokeWidth * arrowWidthFactor}, ${markerStrokeWidth * arrowNotchFactor} 0, 0 ${markerStrokeWidth * arrowWidthFactor}, ${arrowLengthFactor * markerStrokeWidth} 0`} /> diff --git a/src/client/util/LinkFollower.ts b/src/client/util/LinkFollower.ts index ba2edb65c..f74409e42 100644 --- a/src/client/util/LinkFollower.ts +++ b/src/client/util/LinkFollower.ts @@ -12,8 +12,8 @@ import { SelectionManager } from './SelectionManager'; import { UndoManager } from './UndoManager'; /* * link doc: - * - anchor1: doc - * - anchor2: doc + * - link_anchor_1: doc + * - link_anchor_2: doc * * group doc: * - type: string representing the group type/name/category @@ -28,7 +28,7 @@ export class LinkFollower { // if the target isn't onscreen, then it will open up the target in the lightbox, or in place // depending on the followLinkLocation property of the source (or the link itself as a fallback); public static FollowLink = (linkDoc: Opt<Doc>, sourceDoc: Doc, altKey: boolean) => { - const batch = UndoManager.StartBatch('follow link click'); + const batch = UndoManager.StartBatch('Follow Link'); runInAction(() => (LinkFollower.IsFollowing = true)); // turn off decoration bounds while following links since animations may occur, and DocDecorations is based on screenToLocal which is not always an observable value LinkFollower.traverseLink( linkDoc, @@ -42,26 +42,28 @@ export class LinkFollower { }; public static traverseLink(link: Opt<Doc>, sourceDoc: Doc, finished?: () => void, traverseBacklink?: boolean) { + const getView = (doc: Doc) => DocumentManager.Instance.getFirstDocumentView(DocCast(doc.layout_unrendered ? doc.annotationOn : doc)); + const isAnchor = (sourceDoc: Doc, anchor: Doc) => Doc.AreProtosEqual(anchor, sourceDoc) || Doc.AreProtosEqual(anchor.annotationOn as Doc, sourceDoc); const linkDocs = link ? [link] : LinkManager.Links(sourceDoc); - const firstDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor1 as Doc, sourceDoc) || Doc.AreProtosEqual((linkDoc.anchor1 as Doc).annotationOn as Doc, sourceDoc)); // link docs where 'doc' is anchor1 - const secondDocs = linkDocs.filter(linkDoc => Doc.AreProtosEqual(linkDoc.anchor2 as Doc, sourceDoc) || Doc.AreProtosEqual((linkDoc.anchor2 as Doc).annotationOn as Doc, sourceDoc)); // link docs where 'doc' is anchor2 - const fwdLinkWithoutTargetView = firstDocs.find(d => DocumentManager.Instance.getDocumentViews((d.anchor2 as Doc).type === DocumentType.MARKER ? DocCast((d.anchor2 as Doc).annotationOn) : (d.anchor2 as Doc)).length === 0); - const backLinkWithoutTargetView = secondDocs.find(d => DocumentManager.Instance.getDocumentViews((d.anchor1 as Doc).type === DocumentType.MARKER ? DocCast((d.anchor1 as Doc).annotationOn) : (d.anchor1 as Doc)).length === 0); - const linkWithoutTargetDoc = traverseBacklink === undefined ? fwdLinkWithoutTargetView || backLinkWithoutTargetView : traverseBacklink ? backLinkWithoutTargetView : fwdLinkWithoutTargetView; - const linkDocList = linkWithoutTargetDoc && !sourceDoc.followAllLinks ? [linkWithoutTargetDoc] : traverseBacklink === undefined ? firstDocs.concat(secondDocs) : traverseBacklink ? secondDocs : firstDocs; + const fwdLinks = linkDocs.filter(l => isAnchor(sourceDoc, l.link_anchor_1 as Doc)); // link docs where 'sourceDoc' is link_anchor_1 + const backLinks = linkDocs.filter(l => isAnchor(sourceDoc, l.link_anchor_2 as Doc)); // link docs where 'sourceDoc' is link_anchor_2 + const fwdLinkWithoutTargetView = fwdLinks.find(l => !getView(DocCast(l.link_anchor_2))); + const backLinkWithoutTargetView = backLinks.find(l => !getView(DocCast(l.link_anchor_1))); + const linkWithoutTargetDoc = traverseBacklink === undefined ? fwdLinkWithoutTargetView ?? backLinkWithoutTargetView : traverseBacklink ? backLinkWithoutTargetView : fwdLinkWithoutTargetView; + const linkDocList = linkWithoutTargetDoc && !sourceDoc.followAllLinks ? [linkWithoutTargetDoc] : traverseBacklink === undefined ? fwdLinks.concat(backLinks) : traverseBacklink ? backLinks : fwdLinks; const followLinks = sourceDoc.followLinkToggle || sourceDoc.followAllLinks ? linkDocList : linkDocList.slice(0, 1); var count = 0; const allFinished = () => ++count === followLinks.length && finished?.(); if (!followLinks.length) finished?.(); followLinks.forEach(async linkDoc => { const target = ( - sourceDoc === linkDoc.anchor1 - ? linkDoc.anchor2 - : sourceDoc === linkDoc.anchor2 - ? linkDoc.anchor1 - : Doc.AreProtosEqual(sourceDoc, linkDoc.anchor1 as Doc) || Doc.AreProtosEqual((linkDoc.anchor1 as Doc).annotationOn as Doc, sourceDoc) - ? linkDoc.anchor2 - : linkDoc.anchor1 + sourceDoc === linkDoc.link_anchor_1 + ? linkDoc.link_anchor_2 + : sourceDoc === linkDoc.link_anchor_2 + ? linkDoc.link_anchor_1 + : Doc.AreProtosEqual(sourceDoc, linkDoc.link_anchor_1 as Doc) || Doc.AreProtosEqual((linkDoc.link_anchor_1 as Doc).annotationOn as Doc, sourceDoc) + ? linkDoc.link_anchor_2 + : linkDoc.link_anchor_1 ) as Doc; if (target) { const doFollow = (canToggle?: boolean) => { @@ -92,16 +94,16 @@ export class LinkFollower { }; let movedTarget = false; if (sourceDoc.followLinkLocation === OpenWhere.inParent) { - const sourceDocParent = DocCast(sourceDoc.context); - if (target.context instanceof Doc && target.context !== sourceDocParent) { - Doc.RemoveDocFromList(target.context, Doc.LayoutFieldKey(target.context), target); + const sourceDocParent = DocCast(sourceDoc.embedContainer); + if (target.embedContainer instanceof Doc && target.embedContainer !== sourceDocParent) { + Doc.RemoveDocFromList(target.embedContainer, Doc.LayoutFieldKey(target.embedContainer), target); movedTarget = true; } if (!DocListCast(sourceDocParent[Doc.LayoutFieldKey(sourceDocParent)]).includes(target)) { Doc.AddDocToList(sourceDocParent, Doc.LayoutFieldKey(sourceDocParent), target); movedTarget = true; } - target.context = sourceDocParent; + target.embedContainer = sourceDocParent; const moveTo = [NumCast(sourceDoc.x) + NumCast(sourceDoc.followLinkXoffset), NumCast(sourceDoc.y) + NumCast(sourceDoc.followLinkYoffset)]; if (sourceDoc.followLinkXoffset !== undefined && moveTo[0] !== target.x) { target.x = moveTo[0]; diff --git a/src/client/util/LinkManager.ts b/src/client/util/LinkManager.ts index 555215417..dbb05917e 100644 --- a/src/client/util/LinkManager.ts +++ b/src/client/util/LinkManager.ts @@ -1,14 +1,15 @@ import { action, observable, observe } from 'mobx'; import { computedFn } from 'mobx-utils'; -import { DirectLinksSym, Doc, DocListCast, DocListCastAsync, Field, Opt } from '../../fields/Doc'; +import { Doc, DocListCast, DocListCastAsync, Field, Opt } from '../../fields/Doc'; +import { DirectLinks } from '../../fields/DocSymbols'; import { List } from '../../fields/List'; import { ProxyField } from '../../fields/Proxy'; import { Cast, DocCast, PromiseValue, StrCast } from '../../fields/Types'; import { ScriptingGlobals } from './ScriptingGlobals'; /* * link doc: - * - anchor1: doc - * - anchor2: doc + * - link_anchor_1: doc + * - link_anchor_2: doc * * group doc: * - type: string representing the group type/name/category @@ -33,7 +34,7 @@ export class LinkManager { await Promise.all( ((await DocListCastAsync(linkDb.data)) ?? []).map(link => // makes sure link anchors are loaded to avoid incremental updates to computedFns in LinkManager - [PromiseValue(link?.anchor1), PromiseValue(link?.anchor2)] + [PromiseValue(link?.link_anchor_1), PromiseValue(link?.link_anchor_2)] ) ); LinkManager.userLinkDBs.push(linkDb); @@ -42,21 +43,21 @@ export class LinkManager { static _links: Doc[] = []; constructor() { LinkManager._instance = this; - this.createLinkrelationshipLists(); + this.createlink_relationshipLists(); LinkManager.userLinkDBs = []; const addLinkToDoc = (link: Doc) => { Promise.all([link]).then(linkdoc => { const link = DocCast(linkdoc[0]); Promise.all([link.proto]).then(linkproto => { - Promise.all([link.anchor1, link.anchor2]).then(linkdata => { + Promise.all([link.link_anchor_1, link.link_anchor_2]).then(linkdata => { const a1 = DocCast(linkdata[0]); const a2 = DocCast(linkdata[1]); a1 && a2 && Promise.all([Doc.GetProto(a1), Doc.GetProto(a2)]).then( action(protos => { - (protos[0] as Doc)?.[DirectLinksSym].add(link); - (protos[1] as Doc)?.[DirectLinksSym].add(link); + (protos[0] as Doc)?.[DirectLinks].add(link); + (protos[1] as Doc)?.[DirectLinks].add(link); }) ); }); @@ -64,14 +65,14 @@ export class LinkManager { }); }; const remLinkFromDoc = (link: Doc) => { - const a1 = link?.anchor1; - const a2 = link?.anchor2; + const a1 = link?.link_anchor_1; + const a2 = link?.link_anchor_2; Promise.all([a1, a2]).then( action(() => { if (a1 instanceof Doc && a2 instanceof Doc && ((a1.author !== undefined && a2.author !== undefined) || link.author === Doc.CurrentUserEmail)) { - Doc.GetProto(a1)[DirectLinksSym].delete(link); - Doc.GetProto(a2)[DirectLinksSym].delete(link); - Doc.GetProto(link)[DirectLinksSym].delete(link); + Doc.GetProto(a1)[DirectLinks].delete(link); + Doc.GetProto(a2)[DirectLinks].delete(link); + Doc.GetProto(link)[DirectLinks].delete(link); } }) ); @@ -130,11 +131,11 @@ export class LinkManager { LinkManager.addLinkDB(Doc.LinkDBDoc()); } - public createLinkrelationshipLists = () => { + public createlink_relationshipLists = () => { //create new lists for link relations and their associated colors if the lists don't already exist - !Doc.UserDoc().linkRelationshipList && (Doc.UserDoc().linkRelationshipList = new List<string>()); - !Doc.UserDoc().linkColorList && (Doc.UserDoc().linkColorList = new List<string>()); - !Doc.UserDoc().linkRelationshipSizes && (Doc.UserDoc().linkRelationshipSizes = new List<number>()); + !Doc.UserDoc().link_relationshipList && (Doc.UserDoc().link_relationshipList = new List<string>()); + !Doc.UserDoc().link_ColorList && (Doc.UserDoc().link_ColorList = new List<string>()); + !Doc.UserDoc().link_relationshipSizes && (Doc.UserDoc().link_relationshipSizes = new List<number>()); }; public addLink(linkDoc: Doc, checkExists = false) { @@ -153,7 +154,7 @@ export class LinkManager { return this.relatedLinker(anchor); } // finds all links that contain the given anchor public getAllDirectLinks(anchor: Doc): Doc[] { - return Array.from(Doc.GetProto(anchor)[DirectLinksSym] ?? []); + return Array.from(Doc.GetProto(anchor)[DirectLinks] ?? []); } // finds all links that contain the given anchor relatedLinker = computedFn(function relatedLinker(this: any, anchor: Doc): Doc[] { @@ -161,8 +162,8 @@ export class LinkManager { console.log('WAITING FOR DOC/PROTO IN LINKMANAGER'); return []; } - const dirLinks = Doc.GetProto(anchor)[DirectLinksSym]; - const annos = DocListCast(anchor[Doc.LayoutFieldKey(anchor) + '-annotations']); + const dirLinks = Doc.GetProto(anchor)[DirectLinks]; + const annos = DocListCast(anchor[Doc.LayoutFieldKey(anchor) + '_annotations']); if (!annos) debugger; return annos.reduce((list, anno) => [...list, ...LinkManager.Instance.relatedLinker(anno)], Array.from(dirLinks).slice()); }, true); @@ -171,9 +172,9 @@ export class LinkManager { public getRelatedGroupedLinks(anchor: Doc): Map<string, Array<Doc>> { const anchorGroups = new Map<string, Array<Doc>>(); this.relatedLinker(anchor).forEach(link => { - if (link.linkRelationship && link.linkRelationship !== '-ungrouped-') { - const relation = StrCast(link.linkRelationship); - const anchorRelation = relation.indexOf(':') !== -1 ? relation.split(':')[Doc.AreProtosEqual(Cast(link.anchor1, Doc, null), anchor) ? 0 : 1] : relation; + if (link.link_relationship && link.link_relationship !== '-ungrouped-') { + const relation = StrCast(link.link_relationship); + const anchorRelation = relation.indexOf(':') !== -1 ? relation.split(':')[Doc.AreProtosEqual(Cast(link.link_anchor_1, Doc, null), anchor) ? 0 : 1] : relation; const group = anchorGroups.get(anchorRelation); anchorGroups.set(anchorRelation, group ? [...group, link] : [link]); } else { @@ -189,8 +190,8 @@ export class LinkManager { //TODO This should probably return undefined if there isn't an opposite anchor //TODO This should also await the return value of the anchor so we don't filter out promises public static getOppositeAnchor(linkDoc: Doc, anchor: Doc): Doc | undefined { - const a1 = Cast(linkDoc.anchor1, Doc, null); - const a2 = Cast(linkDoc.anchor2, Doc, null); + const a1 = Cast(linkDoc.link_anchor_1, Doc, null); + const a2 = Cast(linkDoc.link_anchor_2, Doc, null); if (Doc.AreProtosEqual(anchor, a1)) return a2; if (Doc.AreProtosEqual(anchor, a2)) return a1; if (Doc.AreProtosEqual(anchor, a1.annotationOn as Doc)) return a2; diff --git a/src/client/util/ReplayMovements.ts b/src/client/util/ReplayMovements.ts index 22cca4a2e..cbc465d6a 100644 --- a/src/client/util/ReplayMovements.ts +++ b/src/client/util/ReplayMovements.ts @@ -127,8 +127,8 @@ export class ReplayMovements { zoomAndPan = (movement: Movement, document: CollectionFreeFormView) => { const { panX, panY, scale } = movement; scale !== 0 && document.zoomSmoothlyAboutPt([panX, panY], scale, 0); - document.Document._panX = panX; - document.Document._panY = panY; + document.Document._freeform_panX = panX; + document.Document._freeform_panY = panY; }; getFirstMovements = (movements: Movement[]): Map<Doc, Movement> => { diff --git a/src/client/util/ReportManager.tsx b/src/client/util/ReportManager.tsx index 51742d455..4c1020455 100644 --- a/src/client/util/ReportManager.tsx +++ b/src/client/util/ReportManager.tsx @@ -173,7 +173,7 @@ export class ReportManager extends React.Component<{}> { // upload the files to the server if (input.files && input.files.length !== 0) { const fileArray: File[] = Array.from(input.files); - (Networking.UploadFilesToServer(fileArray)).then(links => { + (Networking.UploadFilesToServer(fileArray.map(file =>({file})))).then(links => { console.log('finshed uploading', links.map(this.getServerPath)); this.setFileLinks((links ?? []).map(this.getServerPath)); }) diff --git a/src/client/util/Scripting.ts b/src/client/util/Scripting.ts index f17a98616..70c2e3842 100644 --- a/src/client/util/Scripting.ts +++ b/src/client/util/Scripting.ts @@ -7,8 +7,6 @@ import * as typescriptlib from '!!raw-loader!./type_decls.d'; import * as ts from 'typescript'; import { Doc, Field } from '../../fields/Doc'; -import { ToScriptString } from '../../fields/FieldSymbols'; -import { ObjectField } from '../../fields/ObjectField'; import { RefField } from '../../fields/RefField'; import { ScriptField } from '../../fields/ScriptField'; import { scriptingGlobals, ScriptingGlobals } from './ScriptingGlobals'; @@ -60,7 +58,14 @@ function Run(script: string | undefined, customParams: string[], diagnostics: an // let fieldTypes = [Doc, ImageField, PdfField, VideoField, AudioField, List, RichTextField, ScriptField, ComputedField, CompileScript]; // let paramNames = ["Docs", ...fieldTypes.map(fn => fn.name)]; // let params: any[] = [Docs, ...fieldTypes]; - const compiledFunction = new Function(...paramNames, `return ${script}`); + const compiledFunction = (() => { + try { + return new Function(...paramNames, `return ${script}`); + } catch { + return undefined; + } + })(); + if (!compiledFunction) return { compiled: false, errors }; const { capturedVariables = {} } = options; const run = (args: { [name: string]: any } = {}, onError?: (e: any) => void, errorVal?: any): ScriptResult => { const argsArray: any[] = []; @@ -155,7 +160,7 @@ export type Traverser = (node: ts.Node, indentation: string) => boolean | void; export type TraverserParam = Traverser | { onEnter: Traverser; onLeave: Traverser }; export type Transformer = { transformer: ts.TransformerFactory<ts.SourceFile>; - getVars?: () => { capturedVariables: { [name: string]: Field } }; + getVars?: () => { [name: string]: Field }; }; export interface ScriptOptions { requiredType?: string; // does function required a typed return value @@ -205,16 +210,17 @@ export function CompileScript(script: string, options: ScriptOptions = {}): Comp if (options.transformer) { const sourceFile = ts.createSourceFile('script.ts', script, ts.ScriptTarget.ES2015, true); const result = ts.transform(sourceFile, [options.transformer.transformer]); - if (options.transformer.getVars) { - const newCaptures = options.transformer.getVars(); + const newCaptures = options.transformer.getVars?.(); + if (Object.keys(newCaptures ?? {}).length) { // tslint:disable-next-line: prefer-object-spread - options.capturedVariables = Object.assign(capturedVariables, newCaptures.capturedVariables) as any; + //options.capturedVariables = Object.assign(capturedVariables, newCaptures!) as any; + + const transformed = result.transformed; + const printer = ts.createPrinter({ + newLine: ts.NewLineKind.LineFeed, + }); + script = printer.printFile(transformed[0]); } - const transformed = result.transformed; - const printer = ts.createPrinter({ - newLine: ts.NewLineKind.LineFeed, - }); - script = printer.printFile(transformed[0]); result.dispose(); } const paramNames: string[] = []; diff --git a/src/client/util/SearchUtil.ts b/src/client/util/SearchUtil.ts index 79759a71d..d154c48a4 100644 --- a/src/client/util/SearchUtil.ts +++ b/src/client/util/SearchUtil.ts @@ -24,51 +24,57 @@ export namespace SearchUtil { export interface SearchParams { hl?: string; - "hl.fl"?: string; + 'hl.fl'?: string; start?: number; rows?: number; fq?: string; sort?: string; - allowAliases?: boolean; - onlyAliases?: boolean; - "facet"?: string; - "facet.field"?: string; + allowEmbeddings?: boolean; + onlyEmbeddings?: boolean; + facet?: string; + 'facet.field'?: string; } export function Search(query: string, returnDocs: true, options?: SearchParams): Promise<DocSearchResult>; export function Search(query: string, returnDocs: false, options?: SearchParams): Promise<IdSearchResult>; export async function Search(query: string, returnDocs: boolean, options: SearchParams = {}) { - query = query || "*"; //If we just have a filter query, search for * as the query - const rpquery = Utils.prepend("/dashsearch"); + query = query || '*'; //If we just have a filter query, search for * as the query + const rpquery = Utils.prepend('/dashsearch'); let replacedQuery = query.replace(/type_t:([^ )])/g, (substring, arg) => `{!join from=id to=proto_i}*:* AND ${arg}`); - if (options.onlyAliases) { - const header = query.match(/_[atnb]?:/) ? replacedQuery : "DEFAULT:" + replacedQuery; + if (options.onlyEmbeddings) { + const header = query.match(/_[atnb]?:/) ? replacedQuery : 'DEFAULT:' + replacedQuery; replacedQuery = `{!join from=id to=proto_i}* AND ${header}`; } //console.log("Q: " + replacedQuery + " fq: " + options.fq); const gotten = await rp.get(rpquery, { qs: { ...options, q: replacedQuery } }); - const result: IdSearchResult = gotten.startsWith("<") ? { ids: [], docs: [], numFound: 0, lines: [] } : JSON.parse(gotten); + const result: IdSearchResult = gotten.startsWith('<') ? { ids: [], docs: [], numFound: 0, lines: [] } : JSON.parse(gotten); if (!returnDocs) { return result; } const { ids, highlighting } = result; - const txtresult = query !== "*" && JSON.parse(await rp.get(Utils.prepend("/textsearch"), { - qs: { ...options, q: query.replace(/^[ \+\?\*\|]*/, "") }, // a leading '+' leads to a server crash since findInFiles doesn't handle regex failures - })); + const txtresult = + query !== '*' && + JSON.parse( + await rp.get(Utils.prepend('/textsearch'), { + qs: { ...options, q: query.replace(/^[ \+\?\*\|]*/, '') }, // a leading '+' leads to a server crash since findInFiles doesn't handle regex failures + }) + ); const fileids = txtresult ? txtresult.ids : []; const newIds: string[] = []; const newLines: string[][] = []; - if (fileids) { - await Promise.all(fileids.map(async (tr: string, i: number) => { - const docQuery = "fileUpload_t:" + tr.substr(0, 7); //If we just have a filter query, search for * as the query - const docResult = JSON.parse(await rp.get(Utils.prepend("/dashsearch"), { qs: { ...options, q: docQuery } })); - newIds.push(...docResult.ids); - newLines.push(...docResult.ids.map((dr: any) => txtresult.lines[i])); - })); - } - + // bcz: we stopped storing fileUpload id's, so this won't find anything + // if (fileids) { + // await Promise.all( + // fileids.map(async (tr: string, i: number) => { + // const docQuery = 'fileUpload_t:' + tr.substr(0, 7); //If we just have a filter query, search for * as the query + // const docResult = JSON.parse(await rp.get(Utils.prepend('/dashsearch'), { qs: { ...options, q: docQuery } })); + // newIds.push(...docResult.ids); + // newLines.push(...docResult.ids.map((dr: any) => txtresult.lines[i])); + // }) + // ); + // } const theDocs: Doc[] = []; const theLines: string[][] = []; @@ -86,7 +92,7 @@ export namespace SearchUtil { const docs = ids.map((id: string) => docMap[id]).map(doc => doc as Doc); for (let i = 0; i < ids.length; i++) { const testDoc = docs[i]; - if (testDoc instanceof Doc && testDoc.type !== DocumentType.KVP && (options.allowAliases || testDoc.proto === undefined || theDocs.findIndex(d => Doc.AreProtosEqual(d, testDoc)) === -1)) { + if (testDoc instanceof Doc && testDoc.type !== DocumentType.KVP && (options.allowEmbeddings || testDoc.proto === undefined || theDocs.findIndex(d => Doc.AreProtosEqual(d, testDoc)) === -1)) { theDocs.push(testDoc); theLines.push([]); } else { @@ -97,48 +103,46 @@ export namespace SearchUtil { return { docs: theDocs, numFound: Math.max(0, result.numFound), highlighting, lines: theLines }; } - export async function GetAliasesOfDocument(doc: Doc): Promise<Doc[]>; - export async function GetAliasesOfDocument(doc: Doc, returnDocs: false): Promise<string[]>; - export async function GetAliasesOfDocument(doc: Doc, returnDocs = true): Promise<Doc[] | string[]> { + export async function GetEmbeddingsOfDocument(doc: Doc): Promise<Doc[]>; + export async function GetEmbeddingsOfDocument(doc: Doc, returnDocs: false): Promise<string[]>; + export async function GetEmbeddingsOfDocument(doc: Doc, returnDocs = true): Promise<Doc[] | string[]> { const proto = Doc.GetProto(doc); const protoId = proto[Id]; if (returnDocs) { - return (await Search("", returnDocs, { fq: `proto_i:"${protoId}"`, allowAliases: true })).docs; + return (await Search('', returnDocs, { fq: `proto_i:"${protoId}"`, allowEmbeddings: true })).docs; } else { - return (await Search("", returnDocs, { fq: `proto_i:"${protoId}"`, allowAliases: true })).ids; + return (await Search('', returnDocs, { fq: `proto_i:"${protoId}"`, allowEmbeddings: true })).ids; } // return Search(`{!join from=id to=proto_i}id:${protoId}`, true); } export async function GetViewsOfDocument(doc: Doc): Promise<Doc[]> { - const results = await Search("", true, { fq: `proto_i:"${doc[Id]}"` }); + const results = await Search('', true, { fq: `proto_i:"${doc[Id]}"` }); return results.docs; } - export async function GetContextsOfDocument(doc: Doc): Promise<{ contexts: Doc[], aliasContexts: Doc[] }> { - const docContexts = (await Search("", true, { fq: `data_l:"${doc[Id]}"` })).docs; - const aliases = await GetAliasesOfDocument(doc, false); - const aliasContexts = (await Promise.all(aliases.map(doc => Search("", true, { fq: `data_l:"${doc}"` })))); - const contexts = { contexts: docContexts, aliasContexts: [] as Doc[] }; - aliasContexts.forEach(result => contexts.aliasContexts.push(...result.docs)); + export async function GetContextsOfDocument(doc: Doc): Promise<{ contexts: Doc[]; embeddingContexts: Doc[] }> { + const docContexts = (await Search('', true, { fq: `data_l:"${doc[Id]}"` })).docs; + const embeddings = await GetEmbeddingsOfDocument(doc, false); + const embeddingContexts = await Promise.all(embeddings.map(doc => Search('', true, { fq: `data_l:"${doc}"` }))); + const contexts = { contexts: docContexts, embeddingContexts: [] as Doc[] }; + embeddingContexts.forEach(result => contexts.embeddingContexts.push(...result.docs)); return contexts; } - export async function GetContextIdsOfDocument(doc: Doc): Promise<{ contexts: string[], aliasContexts: string[] }> { - const docContexts = (await Search("", false, { fq: `data_l:"${doc[Id]}"` })).ids; - const aliases = await GetAliasesOfDocument(doc, false); - const aliasContexts = (await Promise.all(aliases.map(doc => Search("", false, { fq: `data_l:"${doc}"` })))); - const contexts = { contexts: docContexts, aliasContexts: [] as string[] }; - aliasContexts.forEach(result => contexts.aliasContexts.push(...result.ids)); + export async function GetContextIdsOfDocument(doc: Doc): Promise<{ contexts: string[]; embeddingContexts: string[] }> { + const docContexts = (await Search('', false, { fq: `data_l:"${doc[Id]}"` })).ids; + const embeddings = await GetEmbeddingsOfDocument(doc, false); + const embeddingContexts = await Promise.all(embeddings.map(doc => Search('', false, { fq: `data_l:"${doc}"` }))); + const contexts = { contexts: docContexts, embeddingContexts: [] as string[] }; + embeddingContexts.forEach(result => contexts.embeddingContexts.push(...result.ids)); return contexts; } export async function GetAllDocs() { - const query = "*"; + const query = '*'; const response = await rp.get(Utils.prepend('/dashsearch'), { - qs: - { start: 0, rows: 10000, q: query }, - + qs: { start: 0, rows: 10000, q: query }, }); const result: IdSearchResult = JSON.parse(response); const { ids, numFound, highlighting } = result; diff --git a/src/client/util/SelectionManager.ts b/src/client/util/SelectionManager.ts index bfad93334..0125331ec 100644 --- a/src/client/util/SelectionManager.ts +++ b/src/client/util/SelectionManager.ts @@ -2,7 +2,7 @@ import { action, observable, ObservableMap } from 'mobx'; import { computedFn } from 'mobx-utils'; import { Doc, Opt } from '../../fields/Doc'; import { DocCast } from '../../fields/Types'; -import { CollectionViewType, DocumentType } from '../documents/DocumentTypes'; +import { CollectionViewType } from '../documents/DocumentTypes'; import { DocumentView } from '../views/nodes/DocumentView'; import { LinkManager } from './LinkManager'; import { ScriptingGlobals } from './ScriptingGlobals'; @@ -21,7 +21,7 @@ export namespace SelectionManager { @action SelectView(docView: DocumentView, ctrlPressed: boolean): void { // if doc is not in SelectedDocuments, add it - if (!manager.SelectedViewsMap.get(docView) && docView.props.Document.type !== DocumentType.MARKER) { + if (!manager.SelectedViewsMap.get(docView)) { if (!ctrlPressed) { if (LinkManager.currentLink && !LinkManager.Links(docView.rootDoc).includes(LinkManager.currentLink) && docView.rootDoc !== LinkManager.currentLink) { LinkManager.currentLink = undefined; @@ -103,14 +103,14 @@ export namespace SelectionManager { export function Views(): Array<DocumentView> { return manager.SelectedViews; - // Array.from(manager.SelectedViewsMap.keys()); //.filter(dv => manager.SelectedViews.get(dv)?._viewType !== CollectionViewType.Docking); + // Array.from(manager.SelectedViewsMap.keys()); //.filter(dv => manager.SelectedViews.get(dv)?._type_collection !== CollectionViewType.Docking); } export function SelectedSchemaDoc(): Doc | undefined { return manager.SelectedSchemaDocument; } export function Docs(): Doc[] { - return manager.SelectedViews.map(dv => dv.rootDoc).filter(doc => doc?._viewType !== CollectionViewType.Docking); - // Array.from(manager.SelectedViewsMap.values()).filter(doc => doc?._viewType !== CollectionViewType.Docking); + return manager.SelectedViews.map(dv => dv.rootDoc).filter(doc => doc?._type_collection !== CollectionViewType.Docking); + // Array.from(manager.SelectedViewsMap.values()).filter(doc => doc?._type_collection !== CollectionViewType.Docking); } } ScriptingGlobals.add(function SelectionManager_selectedDocType(type: string, expertMode: boolean, checkContext?: boolean) { @@ -118,6 +118,6 @@ ScriptingGlobals.add(function SelectionManager_selectedDocType(type: string, exp if (type === 'tab') { return SelectionManager.Views().lastElement()?.props.renderDepth === 0; } - let selected = (sel => (checkContext ? DocCast(sel?.context) : sel))(SelectionManager.SelectedSchemaDoc() ?? SelectionManager.Docs().lastElement()); - return selected?.type === type || selected?.viewType === type || !type; + let selected = (sel => (checkContext ? DocCast(sel?.embedContainer) : sel))(SelectionManager.SelectedSchemaDoc() ?? SelectionManager.Docs().lastElement()); + return selected?.type === type || selected?.type_collection === type || !type; }); diff --git a/src/client/util/SettingsManager.tsx b/src/client/util/SettingsManager.tsx index 396d754b6..f886ce2ca 100644 --- a/src/client/util/SettingsManager.tsx +++ b/src/client/util/SettingsManager.tsx @@ -74,7 +74,7 @@ export class SettingsManager extends React.Component<{}> { }; @undoBatch selectUserMode = action((e: React.ChangeEvent) => (Doc.noviceMode = (e.currentTarget as any)?.value === 'Novice')); - @undoBatch changeShowTitle = action((e: React.ChangeEvent) => (Doc.UserDoc().showTitle = (e.currentTarget as any).value ? 'title' : undefined)); + @undoBatch changelayout_showTitle = action((e: React.ChangeEvent) => (Doc.UserDoc().layout_showTitle = (e.currentTarget as any).value ? 'title' : undefined)); @undoBatch changeFontFamily = action((e: React.ChangeEvent) => (Doc.UserDoc().fontFamily = (e.currentTarget as any).value)); @undoBatch changeFontSize = action((e: React.ChangeEvent) => (Doc.UserDoc().fontSize = (e.currentTarget as any).value)); @undoBatch switchActiveBackgroundColor = action((color: ColorState) => (Doc.UserDoc().activeCollectionBackground = String(color.hex))); @@ -176,7 +176,7 @@ export class SettingsManager extends React.Component<{}> { return ( <div className="prefs-content"> <div> - <input type="checkbox" onChange={e => (Doc.UserDoc().showTitle = Doc.UserDoc().showTitle ? undefined : 'creationDate')} checked={Doc.UserDoc().showTitle !== undefined} /> + <input type="checkbox" onChange={e => (Doc.UserDoc().layout_showTitle = Doc.UserDoc().layout_showTitle ? undefined : 'author_date')} checked={Doc.UserDoc().layout_showTitle !== undefined} /> <div className="preferences-check">Show doc header</div> </div> <div> diff --git a/src/client/util/SharingManager.tsx b/src/client/util/SharingManager.tsx index d9ab0cb5f..6112b5b69 100644 --- a/src/client/util/SharingManager.tsx +++ b/src/client/util/SharingManager.tsx @@ -5,7 +5,8 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import Select from 'react-select'; import * as RequestPromise from 'request-promise'; -import { AclAdmin, AclPrivate, AclSym, AclUnset, DataSym, Doc, DocListCast, DocListCastAsync, HierarchyMapping } from '../../fields/Doc'; +import { Doc, DocListCast, DocListCastAsync, HierarchyMapping } from '../../fields/Doc'; +import { AclAdmin, AclPrivate, DocAcl, AclUnset, DocData } from '../../fields/DocSymbols'; import { Id } from '../../fields/FieldSymbols'; import { List } from '../../fields/List'; import { NumCast, StrCast } from '../../fields/Types'; @@ -170,7 +171,7 @@ export class SharingManager extends React.Component<{}> { // ! ensures it returns true if document has been shared successfully, false otherwise return !docs - .map(doc => (this.layoutDocAcls ? doc : doc[DataSym])) + .map(doc => (this.layoutDocAcls ? doc : doc[DocData])) .map(doc => { doc.author === Doc.CurrentUserEmail && !doc[myAcl] && distributeAcls(myAcl, SharingPermissions.Admin, doc, undefined, undefined, isDashboard); @@ -182,7 +183,7 @@ export class SharingManager extends React.Component<{}> { distributeAcls(acl, permission as SharingPermissions, doc, undefined, undefined, isDashboard); this.setDashboardBackground(doc, permission as SharingPermissions); if (permission !== SharingPermissions.None) return Doc.AddDocToList(sharingDoc, storage, doc); - else return GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc) || doc); + return GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.createdFrom as Doc) || doc); }) .some(success => !success); }; @@ -210,7 +211,7 @@ export class SharingManager extends React.Component<{}> { // ! ensures it returns true if document has been shared successfully, false otherwise return !docs - .map(doc => (this.layoutDocAcls ? doc : doc[DataSym])) + .map(doc => (this.layoutDocAcls ? doc : doc[DocData])) .map(doc => { doc.author === Doc.CurrentUserEmail && !doc[`acl-${Doc.CurrentUserEmailNormalized}`] && distributeAcls(`acl-${Doc.CurrentUserEmailNormalized}`, SharingPermissions.Admin, doc, undefined, undefined, isDashboard); @@ -233,7 +234,7 @@ export class SharingManager extends React.Component<{}> { return users .map(({ user, sharingDoc }) => { if (permission !== SharingPermissions.None) return Doc.AddDocToList(sharingDoc, storage, doc); // add the doc to the sharingDoc if it hasn't already been added - else return GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.aliasOf as Doc) || doc); // remove the doc from the list if it already exists + else return GetEffectiveAcl(doc, user.email) === AclPrivate && Doc.RemoveDocFromList(sharingDoc, storage, (doc.createdFrom as Doc) || doc); // remove the doc from the list if it already exists }) .some(success => !success); } @@ -292,7 +293,7 @@ export class SharingManager extends React.Component<{}> { doc.isShared = true; doc.backgroundColor = 'green'; } else { - const acls = doc[DataSym][AclSym]; + const acls = doc[DocData][DocAcl]; if ( Object.keys(acls) .filter(key => key !== `acl-${Doc.CurrentUserEmailNormalized}` && key !== 'acl-Me') @@ -495,7 +496,7 @@ export class SharingManager extends React.Component<{}> { const groups = this.groupSort === 'ascending' ? groupList.slice().sort(this.sortGroups) : this.groupSort === 'descending' ? groupList.slice().sort(this.sortGroups).reverse() : groupList; // handles the case where multiple documents are selected - let docs = SelectionManager.Views().length < 2 ? [this.layoutDocAcls ? this.targetDoc : this.targetDoc?.[DataSym]] : SelectionManager.Views().map(docView => (this.layoutDocAcls ? docView.props.Document : docView.props.Document?.[DataSym])); + let docs = SelectionManager.Views().length < 2 ? [this.layoutDocAcls ? this.targetDoc : this.targetDoc?.[DocData]] : SelectionManager.Views().map(docView => (this.layoutDocAcls ? docView.props.Document : docView.props.Document?.[DocData])); if (this.myDocAcls) { const newDocs: Doc[] = []; @@ -503,14 +504,14 @@ export class SharingManager extends React.Component<{}> { docs = newDocs.filter(doc => GetEffectiveAcl(doc) === AclAdmin); } - const targetDoc = this.layoutDocAcls ? docs[0] : docs[0]?.[DataSym]; + const targetDoc = this.layoutDocAcls ? docs[0] : docs[0]?.[DocData]; // tslint:disable-next-line: no-unnecessary-callback-wrapper const effectiveAcls = docs.map(doc => GetEffectiveAcl(doc)); const admin = this.myDocAcls ? Boolean(docs.length) : effectiveAcls.every(acl => acl === AclAdmin); // users in common between all docs - const commonKeys = intersection(...docs.map(doc => (this.layoutDocAcls ? doc : doc[DataSym])).map(doc => doc?.[AclSym] && Object.keys(doc[AclSym]))); + const commonKeys = intersection(...docs.map(doc => (this.layoutDocAcls ? doc : doc[DocData])).map(doc => doc?.[DocAcl] && Object.keys(doc[DocAcl]))); // the list of users shared with const userListContents: (JSX.Element | null)[] = users @@ -526,7 +527,7 @@ export class SharingManager extends React.Component<{}> { const userKey = `acl-${normalizeEmail(user.email)}`; const uniform = docs.map(doc => (this.layoutDocAcls ? doc : doc[DataSym])).every(doc => doc?.[AclSym]?.[userKey] === docs[0]?.[AclSym]?.[userKey]); - const permissions = uniform ? StrCast(docToUse?.[userKey]) : '-multiple-'; + const permissions = uniform ? StrCast(targetDoc?.[userKey]) : '-multiple-'; return !permissions ? null : ( <div key={userKey} className={'container'}> @@ -551,7 +552,7 @@ export class SharingManager extends React.Component<{}> { userListContents.unshift( sameAuthor ? ( <div key={'owner'} className={'container'}> - <span className={'padding'}>{targetDoc?.author === Doc.CurrentUserEmail ? 'Me' : targetDoc?.author}</span> + <span className="padding">{targetDoc?.author === Doc.CurrentUserEmail ? 'Me' : StrCast(targetDoc?.author)}</span> <div className="edit-actions"> <div className={'permissions-dropdown'}>Owner</div> </div> @@ -575,7 +576,7 @@ export class SharingManager extends React.Component<{}> { const uniform = docs .map(doc => (this.layoutDocAcls ? doc : doc[DataSym])) .every(doc => (this.layoutDocAcls ? doc?.[AclSym]?.[groupKey] === docs[0]?.[AclSym]?.[groupKey] : doc?.[DataSym]?.[AclSym]?.[groupKey] === docs[0]?.[DataSym]?.[AclSym]?.[groupKey])); - const permissions = uniform ? StrCast(targetDoc?.[groupKey]) : '-multiple-'; + const permissions = uniform ? StrCast(targetDoc?.[`acl-${StrCast(group.title)}`]) : '-multiple-'; return !permissions ? null : ( <div key={groupKey} className={'container'}> diff --git a/src/client/util/SnappingManager.ts b/src/client/util/SnappingManager.ts index 2c0a1da8b..ed9819fc0 100644 --- a/src/client/util/SnappingManager.ts +++ b/src/client/util/SnappingManager.ts @@ -40,9 +40,9 @@ export namespace SnappingManager { } export function SetShowSnapLines(show: boolean) { - runInAction(() => (Doc.UserDoc().showSnapLines = show)); + runInAction(() => (Doc.UserDoc().freeform_snapLines = show)); } export function GetShowSnapLines() { - return Doc.UserDoc().showSnapLines; + return Doc.UserDoc().freeform_snapLines; } } diff --git a/src/client/util/TrackMovements.ts b/src/client/util/TrackMovements.ts index cb8225643..0e56ee1bc 100644 --- a/src/client/util/TrackMovements.ts +++ b/src/client/util/TrackMovements.ts @@ -3,6 +3,7 @@ import { NumCast } from '../../fields/Types'; import { Doc, DocListCast } from '../../fields/Doc'; import { CollectionDockingView } from '../views/collections/CollectionDockingView'; import { Id } from '../../fields/FieldSymbols'; +import { CollectionViewType } from '../documents/DocumentTypes'; export type Movement = { time: number; @@ -68,7 +69,7 @@ export class TrackMovements { } const disposeFunc = reaction( - () => ({ x: NumCast(doc.panX, -1), y: NumCast(doc.panY, -1), scale: NumCast(doc.viewScale, 0) }), + () => ({ x: NumCast(doc.freeform_panX, -1), y: NumCast(doc.freeform_panY, -1), scale: NumCast(doc.freeform_scale, 0) }), res => res.x !== -1 && res.y !== -1 && this.tracking && this.trackMovement(res.x, res.y, doc, res.scale) ); this.recordingFFViews?.set(doc, disposeFunc); @@ -89,7 +90,7 @@ export class TrackMovements { if (this.recordingFFViews === null) return; // so that the size comparisons are correct, we must filter to only the FFViews - const isFFView = (doc: Doc) => doc && 'viewType' in doc && doc.viewType === 'freeform'; + const isFFView = (doc: Doc) => doc && doc._type_collection === CollectionViewType.Freeform; const tabbedFFViews = new Set<Doc>(); for (const DashDoc of tabbedDocs) { if (isFFView(DashDoc)) tabbedFFViews.add(DashDoc); diff --git a/src/client/util/UndoManager.ts b/src/client/util/UndoManager.ts index d0aec45a6..6fef9d660 100644 --- a/src/client/util/UndoManager.ts +++ b/src/client/util/UndoManager.ts @@ -3,7 +3,7 @@ import { Without } from '../../Utils'; function getBatchName(target: any, key: string | symbol): string { const keyName = key.toString(); - if (target && target.constructor && target.constructor.name) { + if (target?.constructor?.name) { return `${target.constructor.name}.${keyName}`; } return keyName; @@ -34,6 +34,17 @@ function propertyDecorator(target: any, key: string | symbol) { }); } +export function undoable(fn: (...args: any[]) => any, batchName: string): (...args: any[]) => any { + return function () { + const batch = UndoManager.StartBatch(batchName); + try { + return fn.apply(undefined, arguments as any); + } finally { + batch.end(); + } + }; +} + export function undoBatch(target: any, key: string | symbol, descriptor?: TypedPropertyDescriptor<any>): any; export function undoBatch(fn: (...args: any[]) => any): (...args: any[]) => any; export function undoBatch(target: any, key?: string | symbol, descriptor?: TypedPropertyDescriptor<any>): any { @@ -73,15 +84,18 @@ export namespace UndoManager { } type UndoBatch = UndoEvent[]; + export let undoStackNames: string[] = observable([]); + export let redoStackNames: string[] = observable([]); export let undoStack: UndoBatch[] = observable([]); export let redoStack: UndoBatch[] = observable([]); let currentBatch: UndoBatch | undefined; - export let batchCounter = 0; + export let batchCounter = observable.box(0); let undoing = false; let tempEvents: UndoEvent[] | undefined = undefined; - export function AddEvent(event: UndoEvent): void { - if (currentBatch && batchCounter && !undoing) { + export function AddEvent(event: UndoEvent, value?: any): void { + if (currentBatch && batchCounter.get() && !undoing) { + console.log(' '.slice(0, batchCounter.get()) + 'UndoEvent : ' + event.prop + ' = ' + value); currentBatch.push(event); tempEvents?.push(event); } @@ -135,11 +149,13 @@ export namespace UndoManager { private dispose = (cancel: boolean) => { if (this.disposed) { - throw new Error('Cannot dispose an already disposed batch'); + console.log('WARNING: undo batch already disposed'); + return false; + } else { + this.disposed = true; + openBatches.splice(openBatches.indexOf(this)); + return EndBatch(this.batchName, cancel); } - this.disposed = true; - openBatches.splice(openBatches.indexOf(this)); - return EndBatch(cancel); }; end = () => this.dispose(false); @@ -147,22 +163,23 @@ export namespace UndoManager { } export function StartBatch(batchName: string): Batch { - // console.log("Start " + batchCounter + " " + batchName); - batchCounter++; - if (batchCounter > 0 && currentBatch === undefined) { + console.log(' '.slice(0, batchCounter.get()) + 'Start ' + batchCounter + ' ' + batchName); + runInAction(() => batchCounter.set(batchCounter.get() + 1)); + if (currentBatch === undefined) { currentBatch = []; } return new Batch(batchName); } - const EndBatch = action((cancel: boolean = false) => { - batchCounter--; - // console.log("End " + batchCounter); - if (batchCounter === 0 && currentBatch?.length) { - // console.log("------ended----") + const EndBatch = action((batchName: string, cancel: boolean = false) => { + runInAction(() => batchCounter.set(batchCounter.get() - 1)); + console.log(' '.slice(0, batchCounter.get()) + 'End ' + batchName + ' (' + currentBatch?.length + ')'); + if (batchCounter.get() === 0 && currentBatch?.length) { if (!cancel) { undoStack.push(currentBatch); + undoStackNames.push(batchName ?? '???'); } + redoStackNames.length = 0; redoStack.length = 0; currentBatch = undefined; return true; @@ -204,6 +221,7 @@ export namespace UndoManager { return; } + const names = undoStackNames.pop(); const commands = undoStack.pop(); if (!commands) { return; @@ -215,6 +233,7 @@ export namespace UndoManager { } undoing = false; + redoStackNames.push(names ?? '???'); redoStack.push(commands); }); @@ -223,6 +242,7 @@ export namespace UndoManager { return; } + const names = redoStackNames.pop(); const commands = redoStack.pop(); if (!commands) { return; @@ -234,6 +254,7 @@ export namespace UndoManager { } undoing = false; + undoStackNames.push(names ?? '???'); undoStack.push(commands); }); } diff --git a/src/client/views/.DS_Store b/src/client/views/.DS_Store Binary files differindex 5e39387b8..5fa69f28c 100644 --- a/src/client/views/.DS_Store +++ b/src/client/views/.DS_Store diff --git a/src/client/views/AudioWaveform.tsx b/src/client/views/AudioWaveform.tsx index 525c0ce5a..14c922526 100644 --- a/src/client/views/AudioWaveform.tsx +++ b/src/client/views/AudioWaveform.tsx @@ -1,27 +1,25 @@ -import React = require("react"); -import axios from "axios"; -import { action, computed, IReactionDisposer, reaction } from "mobx"; -import { observer } from "mobx-react"; -import Waveform from "react-audio-waveform"; -import { Doc } from "../../fields/Doc"; -import { List } from "../../fields/List"; -import { listSpec } from "../../fields/Schema"; -import { Cast } from "../../fields/Types"; -import { numberRange } from "../../Utils"; -import "./AudioWaveform.scss"; -import { Colors } from "./global/globalEnums"; - +import React = require('react'); +import axios from 'axios'; +import { action, computed, IReactionDisposer, reaction } from 'mobx'; +import { observer } from 'mobx-react'; +import Waveform from 'react-audio-waveform'; +import { Doc } from '../../fields/Doc'; +import { List } from '../../fields/List'; +import { listSpec } from '../../fields/Schema'; +import { Cast } from '../../fields/Types'; +import { numberRange } from '../../Utils'; +import './AudioWaveform.scss'; +import { Colors } from './global/globalEnums'; /** * AudioWaveform - * + * * Used in CollectionStackedTimeline to render a canvas with a visual of an audio waveform for AudioBox and VideoBox documents. * Uses react-audio-waveform package. * Bins the audio data into audioBuckets which are passed to package to render the lines. * Calculates new buckets each time a new zoom factor or new set of trim bounds is created and stores it in a field on the layout doc with a title indicating the bounds and zoom for that list (see audioBucketField) */ - export interface AudioWaveformProps { duration: number; // length of media clip rawDuration: number; // length of underlying media data @@ -32,6 +30,7 @@ export interface AudioWaveformProps { zoomFactor: number; PanelHeight: number; PanelWidth: number; + fieldKey: string; } @observer @@ -40,80 +39,71 @@ export class AudioWaveform extends React.Component<AudioWaveformProps> { _disposer: IReactionDisposer | undefined; - @computed get waveHeight() { return Math.max(50, this.props.PanelHeight); } - - @computed get clipStart() { return this.props.clipStart; } - @computed get clipEnd() { return this.props.clipEnd; } - @computed get zoomFactor() { return this.props.zoomFactor; } + @computed get waveHeight() { + return Math.max(50, this.props.PanelHeight); + } - @computed get audioBuckets() { return Cast(this.props.layoutDoc[this.audioBucketField(this.clipStart, this.clipEnd, this.zoomFactor)], listSpec("number"), []); } - audioBucketField = (start: number, end: number, zoomFactor: number) => "audioBuckets/" + "/" + start.toFixed(2).replace(".", "_") + "/" + end.toFixed(2).replace(".", "_") + "/" + (zoomFactor * 10); + @computed get clipStart() { + return this.props.clipStart; + } + @computed get clipEnd() { + return this.props.clipEnd; + } + @computed get zoomFactor() { + return this.props.zoomFactor; + } + @computed get audioBuckets() { + return Cast(this.props.layoutDoc[this.audioBucketField(this.clipStart, this.clipEnd, this.zoomFactor)], listSpec('number'), []); + } + audioBucketField = (start: number, end: number, zoomFactor: number) => this.props.fieldKey + '_audioBuckets/' + '/' + start.toFixed(2).replace('.', '_') + '/' + end.toFixed(2).replace('.', '_') + '/' + zoomFactor * 10; componentWillUnmount() { this._disposer?.(); } componentDidMount() { - this._disposer = reaction(() => ({ clipStart: this.clipStart, clipEnd: this.clipEnd, fieldKey: this.audioBucketField(this.clipStart, this.clipEnd, this.zoomFactor), zoomFactor: this.props.zoomFactor }), + this._disposer = reaction( + () => ({ clipStart: this.clipStart, clipEnd: this.clipEnd, fieldKey: this.audioBucketField(this.clipStart, this.clipEnd, this.zoomFactor), zoomFactor: this.props.zoomFactor }), ({ clipStart, clipEnd, fieldKey, zoomFactor }) => { if (!this.props.layoutDoc[fieldKey]) { // setting these values here serves as a "lock" to prevent multiple attempts to create the waveform at nerly the same time. - const waveform = Cast(this.props.layoutDoc[this.audioBucketField(0, this.props.rawDuration, 1)], listSpec("number")); - this.props.layoutDoc[fieldKey] = waveform && new List<number>(waveform.slice(clipStart / this.props.rawDuration * waveform.length, clipEnd / this.props.rawDuration * waveform.length)); + const waveform = Cast(this.props.layoutDoc[this.audioBucketField(0, this.props.rawDuration, 1)], listSpec('number')); + this.props.layoutDoc[fieldKey] = waveform && new List<number>(waveform.slice((clipStart / this.props.rawDuration) * waveform.length, (clipEnd / this.props.rawDuration) * waveform.length)); setTimeout(() => this.createWaveformBuckets(fieldKey, clipStart, clipEnd, zoomFactor)); } - }, { fireImmediately: true }); + }, + { fireImmediately: true } + ); } // decodes the audio file into peaks for generating the waveform createWaveformBuckets = async (fieldKey: string, clipStart: number, clipEnd: number, zoomFactor: number) => { - axios({ url: this.props.mediaPath, responseType: "arraybuffer" }).then( - (response) => { - const context = new window.AudioContext(); - context.decodeAudioData( - response.data, - action((buffer) => { - const rawDecodedAudioData = buffer.getChannelData(0); - const startInd = clipStart / this.props.rawDuration; - const endInd = clipEnd / this.props.rawDuration; - const decodedAudioData = rawDecodedAudioData.slice(Math.floor(startInd * rawDecodedAudioData.length), Math.floor(endInd * rawDecodedAudioData.length)); - const numBuckets = Math.floor(AudioWaveform.NUMBER_OF_BUCKETS * zoomFactor); + axios({ url: this.props.mediaPath, responseType: 'arraybuffer' }).then(response => { + const context = new window.AudioContext(); + context.decodeAudioData( + response.data, + action(buffer => { + const rawDecodedAudioData = buffer.getChannelData(0); + const startInd = clipStart / this.props.rawDuration; + const endInd = clipEnd / this.props.rawDuration; + const decodedAudioData = rawDecodedAudioData.slice(Math.floor(startInd * rawDecodedAudioData.length), Math.floor(endInd * rawDecodedAudioData.length)); + const numBuckets = Math.floor(AudioWaveform.NUMBER_OF_BUCKETS * zoomFactor); - const bucketDataSize = Math.floor( - decodedAudioData.length / numBuckets - ); - const brange = Array.from(Array(bucketDataSize)); - const bucketList = numberRange(numBuckets).map( - (i: number) => - brange.reduce( - (p, x, j) => - Math.abs( - Math.max(p, decodedAudioData[i * bucketDataSize + j]) - ), - 0 - ) / 2 - ); - this.props.layoutDoc[fieldKey] = new List<number>(bucketList); - }) - ); - } - ); - } + const bucketDataSize = Math.floor(decodedAudioData.length / numBuckets); + const brange = Array.from(Array(bucketDataSize)); + const bucketList = numberRange(numBuckets).map((i: number) => brange.reduce((p, x, j) => Math.abs(Math.max(p, decodedAudioData[i * bucketDataSize + j])), 0) / 2); + this.props.layoutDoc[fieldKey] = new List<number>(bucketList); + }) + ); + }); + }; render() { return ( <div className="audioWaveform"> - <Waveform - color={Colors.MEDIUM_BLUE_ALT} - height={this.waveHeight} - barWidth={200 / this.audioBuckets.length} - pos={this.props.duration} - duration={this.props.duration} - peaks={this.audioBuckets} - progressColor={Colors.MEDIUM_BLUE_ALT} - /> + <Waveform color={Colors.MEDIUM_BLUE_ALT} height={this.waveHeight} barWidth={200 / this.audioBuckets.length} pos={this.props.duration} duration={this.props.duration} peaks={this.audioBuckets} progressColor={Colors.MEDIUM_BLUE_ALT} /> </div> ); } -}
\ No newline at end of file +} diff --git a/src/client/views/ContextMenuItem.tsx b/src/client/views/ContextMenuItem.tsx index e87d2046b..33f250986 100644 --- a/src/client/views/ContextMenuItem.tsx +++ b/src/client/views/ContextMenuItem.tsx @@ -40,10 +40,7 @@ export class ContextMenuItem extends React.Component<ContextMenuProps & { select handleEvent = async (e: React.MouseEvent<HTMLDivElement>) => { if ('event' in this.props) { this.props.closeMenu?.(); - let batch: UndoManager.Batch | undefined; - if (this.props.undoable !== false) { - batch = UndoManager.StartBatch(`Context menu event: ${this.props.description}`); - } + const batch = this.props.undoable !== false ? UndoManager.StartBatch(`Click Menu item: ${this.props.description}`) : undefined; await this.props.event({ x: e.clientX, y: e.clientY }); batch?.end(); } diff --git a/src/client/views/DashboardView.tsx b/src/client/views/DashboardView.tsx index dee161931..50cf2226e 100644 --- a/src/client/views/DashboardView.tsx +++ b/src/client/views/DashboardView.tsx @@ -3,7 +3,8 @@ import { Button, ColorPicker, FontSize, IconButton, Size } from 'browndash-compo import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { DataSym, Doc, DocListCast, DocListCastAsync } from '../../fields/Doc'; +import { Doc, DocListCast, DocListCastAsync } from '../../fields/Doc'; +import { DocData } from '../../fields/DocSymbols'; import { Id } from '../../fields/FieldSymbols'; import { List } from '../../fields/List'; import { PrefetchProxy } from '../../fields/Proxy'; @@ -66,18 +67,18 @@ export class DashboardView extends React.Component { if (this.selectedDashboardGroup === DashboardGroup.MyDashboards) { return allDashboards.filter(dashboard => Doc.GetProto(dashboard).author === Doc.CurrentUserEmail); } else { - const sharedDashboards = DocListCast(Doc.MySharedDocs.data).filter(doc => doc._viewType === CollectionViewType.Docking); + const sharedDashboards = DocListCast(Doc.MySharedDocs.data).filter(doc => doc._type_collection === CollectionViewType.Docking); return sharedDashboards; } }; isUnviewedSharedDashboard = (dashboard: Doc): boolean => { - // const sharedDashboards = DocListCast(Doc.MySharedDocs.data).filter(doc => doc._viewType === CollectionViewType.Docking); + // const sharedDashboards = DocListCast(Doc.MySharedDocs.data).filter(doc => doc._type_collection === CollectionViewType.Docking); return !DocListCast(Doc.MySharedDocs.viewed).includes(dashboard); }; getSharedDashboards = () => { - const sharedDashs = DocListCast(Doc.MySharedDocs.data).filter(doc => doc._viewType === CollectionViewType.Docking); + const sharedDashs = DocListCast(Doc.MySharedDocs.data).filter(doc => doc._type_collection === CollectionViewType.Docking); return sharedDashs.filter(dashboard => !DocListCast(Doc.MySharedDocs.viewed).includes(dashboard)); }; @@ -358,18 +359,17 @@ export class DashboardView extends React.Component { y: 400, _width: 1500, _height: 1000, - _fitWidth: true, - _backgroundGridShow: true, + _layout_fitWidth: true, + _freeform_backgroundGrid: true, backgroundColor: background, title: `Untitled Tab 1`, }; const title = name ? name : `Dashboard ${dashboardCount}`; const freeformDoc = Doc.GuestTarget || Docs.Create.FreeformDocument([], freeformOptions); const dashboardDoc = Docs.Create.StandardCollectionDockingDocument([{ doc: freeformDoc, initialWidth: 600 }], { title: title }, id, 'row'); - freeformDoc.context = dashboardDoc; // switching the tabs from the datadoc to the regular doc - const dashboardTabs = DocListCast(dashboardDoc[DataSym].data); + const dashboardTabs = DocListCast(dashboardDoc[DocData].data); dashboardDoc.data = new List<Doc>(dashboardTabs); dashboardDoc['pane-count'] = 1; @@ -390,13 +390,13 @@ export class DashboardView extends React.Component { _width: 30, _height: 30, _stayInCollection: true, - _hideContextMenu: true, + _layout_hideContextMenu: true, title: 'New trail', toolTip: 'Create new trail', btnType: ButtonType.ClickButton, buttonText: 'New trail', icon: 'plus', - system: true, + isSystem: true, }; const reqdBtnScript = { onClick: `createNewPresentation()` }; const myTrailsBtn = DocUtils.AssignScripts(Docs.Create.FontIconDocument(reqdBtnOpts), reqdBtnScript); @@ -406,25 +406,24 @@ export class DashboardView extends React.Component { // but we don't want to create the list of trails here-- but rather in createDashboard const reqdOpts: DocumentOptions = { title: 'My Trails', - _showTitle: 'title', + _layout_showTitle: 'title', _height: 100, treeViewHideTitle: true, - _fitWidth: true, + _layout_fitWidth: true, _gridGap: 5, _forceActive: true, - childDropAction: 'alias', + childDropAction: 'embed', treeViewTruncateTitleWidth: 150, ignoreClick: true, - buttonMenu: true, - buttonMenuDoc: myTrailsBtn, + layout_headerButton: myTrailsBtn, contextMenuIcons: new List<string>(['plus']), contextMenuLabels: new List<string>(['Create New Trail']), _lockedPosition: true, - boxShadow: '0 0', + layout_boxShadow: '0 0', childDontRegisterViews: true, targetDropAction: 'same', - system: true, - explainer: 'All of the trails that you have created will appear here.', + isSystem: true, + layout_explainer: 'All of the trails that you have created will appear here.', }; const myTrails = DocUtils.AssignScripts(Docs.Create.TreeDocument([], reqdOpts), { treeViewChildDoubleClick: 'openPresentation(documentView.rootDoc)' }); dashboardDoc.myTrails = new PrefetchProxy(myTrails); @@ -453,7 +452,7 @@ ScriptingGlobals.add(function resetDashboard(dashboard: Doc) { DashboardView.resetDashboard(dashboard); }, 'move all dashboard tabs to single stack'); ScriptingGlobals.add(function addToDashboards(dashboard: Doc) { - DashboardView.openDashboard(Doc.MakeAlias(dashboard)); + DashboardView.openDashboard(Doc.MakeEmbedding(dashboard)); }, 'adds Dashboard to set of Dashboards'); ScriptingGlobals.add(function snapshotDashboard() { DashboardView.snapshotDashboard(); diff --git a/src/client/views/DocComponent.tsx b/src/client/views/DocComponent.tsx index ab97691ee..dc9c8a0ce 100644 --- a/src/client/views/DocComponent.tsx +++ b/src/client/views/DocComponent.tsx @@ -1,7 +1,7 @@ import { action, computed, observable } from 'mobx'; import { DateField } from '../../fields/DateField'; -import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, AclSym, DataSym, Doc, DocListCast, Opt } from '../../fields/Doc'; -import { InkTool } from '../../fields/InkField'; +import { DocListCast, Opt, Doc } from '../../fields/Doc'; +import { AclAdmin, AclAugment, AclEdit, AclPrivate, AclReadonly, DocAcl, DocData } from '../../fields/DocSymbols'; import { List } from '../../fields/List'; import { Cast, ScriptCast } from '../../fields/Types'; import { denormalizeEmail, distributeAcls, GetEffectiveAcl, inheritParentAcls, SharingPermissions } from '../../fields/util'; @@ -36,7 +36,7 @@ export function DocComponent<P extends DocComponentProps>() { } // This is the data part of a document -- ie, the data that is constant across all views of the document @computed get dataDoc() { - return this.props.Document[DataSym] as Doc; + return this.props.Document[DocData] as Doc; } // key where data is stored @computed get fieldKey() { @@ -74,7 +74,7 @@ export function ViewBoxBaseComponent<P extends ViewBoxBaseProps>() { } // This is the data part of a document -- ie, the data that is constant across all views of the document @computed get dataDoc() { - return this.props.DataDoc && (this.props.Document.isTemplateForField || this.props.Document.isTemplateDoc) ? this.props.DataDoc : this.props.Document[DataSym]; + return this.props.DataDoc && (this.props.Document.isTemplateForField || this.props.Document.isTemplateDoc) ? this.props.DataDoc : this.props.Document[DocData]; } // key where data is stored @computed get fieldKey() { @@ -118,7 +118,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>() } // This is the data part of a document -- ie, the data that is constant across all views of the document @computed get dataDoc() { - return this.props.DataDoc && (this.props.Document.isTemplateForField || this.props.Document.isTemplateDoc) ? this.props.DataDoc : this.props.Document[DataSym]; + return this.props.DataDoc && (this.props.Document.isTemplateForField || this.props.Document.isTemplateDoc) ? this.props.DataDoc : this.props.Document[DocData]; } // key where data is stored @@ -128,12 +128,10 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>() isAnyChildContentActive = () => this._isAnyChildContentActive; - lookupField = (field: string) => ScriptCast((this.layoutDoc as any).lookupField)?.script.run({ self: this.layoutDoc, data: this.rootDoc, field: field }).result; - protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; @computed public get annotationKey() { - return this.fieldKey + (this._annotationKeySuffix() ? '-' + this._annotationKeySuffix() : ''); + return this.fieldKey + (this._annotationKeySuffix() ? '_' + this._annotationKeySuffix() : ''); } @action.bound @@ -152,7 +150,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>() toRemove.forEach(doc => { leavePushpin && DocUtils.LeavePushpin(doc, annotationKey ?? this.annotationKey); Doc.RemoveDocFromList(targetDataDoc, annotationKey ?? this.annotationKey, doc); - doc.context = undefined; + doc.embedContainer = undefined; if (recent) { Doc.RemoveDocFromList(recent, 'data', doc); doc.type !== DocumentType.LOADING && Doc.AddDocToList(recent, 'data', doc, undefined, true, true); @@ -185,7 +183,7 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>() if (this.props.filterAddDocument?.(docs) === false || docs.find(doc => Doc.AreProtosEqual(doc, this.props.Document) && Doc.LayoutField(doc) === Doc.LayoutField(this.props.Document))) { return false; } - const targetDataDoc = this.props.Document[DataSym]; + const targetDataDoc = this.props.Document[DocData]; const effectiveAcl = GetEffectiveAcl(targetDataDoc); if (effectiveAcl === AclPrivate || effectiveAcl === AclReadonly) { @@ -193,60 +191,38 @@ export function ViewBoxAnnotatableComponent<P extends ViewBoxAnnotatableProps>() } const added = docs; if (added.length) { - if (this.props.Document[AclSym] && Object.keys(this.props.Document[AclSym]).length) { + const aclKeys = Object.keys(this.props.Document[DocAcl] ?? {}); + aclKeys.forEach(key => added.forEach(d => { - for (const key of Object.keys(this.props.Document[AclSym])) { - if (d.author === denormalizeEmail(key.substring(4)) && !d.aliasOf) distributeAcls(key, SharingPermissions.Admin, d); + if (d.author === denormalizeEmail(key.substring(4)) && !d.createdFrom) { + distributeAcls(key, SharingPermissions.Admin, d); } - }); - } + }) + ); if (effectiveAcl === AclAugment) { added.map(doc => { if ([AclAdmin, AclEdit].includes(GetEffectiveAcl(doc)) && Doc.ActiveDashboard) inheritParentAcls(Doc.ActiveDashboard, doc); - doc.context = this.props.Document; + doc.embedContainer = this.props.Document; if (annotationKey ?? this._annotationKeySuffix()) Doc.GetProto(doc).annotationOn = this.props.Document; Doc.AddDocToList(targetDataDoc, annotationKey ?? this.annotationKey, doc); }); } else { - if (this.props.Document[AclSym] && Object.keys(this.props.Document[AclSym]).length) { - added.forEach(d => { - for (const [key, value] of Object.entries(this.props.Document[AclSym])) { - if (d.author === denormalizeEmail(key.substring(4)) && !d.aliasOf) distributeAcls(key, SharingPermissions.Admin, d); - } - }); - } - - if (effectiveAcl === AclAugment) { - added.map(doc => { + added + .filter(doc => [AclAdmin, AclEdit].includes(GetEffectiveAcl(doc))) + .map(doc => { + // only make a pushpin if we have acl's to edit the document + //DocUtils.LeavePushpin(doc); + doc._stayInCollection = undefined; doc.context = this.props.Document; - const contextDoc = Cast(doc.context, Doc, null); - if ([AclAdmin, AclEdit].includes(GetEffectiveAcl(doc))) { - if (contextDoc) inheritParentAcls(contextDoc, doc); - else if (Doc.ActiveDashboard) inheritParentAcls(Doc.ActiveDashboard, doc); - } - if (annotationKey ?? this._annotationKeySuffix()) Doc.GetProto(doc).annotationOn = this.props.Document; - Doc.AddDocToList(targetDataDoc, annotationKey ?? this.annotationKey, doc); - }); - } else { - added - .filter(doc => [AclAdmin, AclEdit].includes(GetEffectiveAcl(doc))) - .map(doc => { - // only make a pushpin if we have acl's to edit the document - //DocUtils.LeavePushpin(doc); - doc._stayInCollection = undefined; - doc.context = this.props.Document; - if (annotationKey ?? this._annotationKeySuffix()) Doc.GetProto(doc).annotationOn = this.rootDoc; + if (annotationKey ?? this._annotationKeySuffix()) Doc.GetProto(doc).annotationOn = this.rootDoc; - const contextDoc = Cast(doc.context, Doc, null); - if (contextDoc) inheritParentAcls(contextDoc, doc); - else if (Doc.ActiveDashboard) inheritParentAcls(Doc.ActiveDashboard, doc); - }); - const annoDocs = targetDataDoc[annotationKey ?? this.annotationKey] as List<Doc>; - if (annoDocs instanceof List) annoDocs.push(...added); - else targetDataDoc[annotationKey ?? this.annotationKey] = new List<Doc>(added); - targetDataDoc[(annotationKey ?? this.annotationKey) + '-lastModified'] = new DateField(new Date(Date.now())); - } + Doc.ActiveDashboard && inheritParentAcls(Doc.ActiveDashboard, doc); + }); + const annoDocs = targetDataDoc[annotationKey ?? this.annotationKey] as List<Doc>; + if (annoDocs instanceof List) annoDocs.push(...added); + else targetDataDoc[annotationKey ?? this.annotationKey] = new List<Doc>(added); + targetDataDoc[(annotationKey ?? this.annotationKey) + '-lastModified'] = new DateField(new Date(Date.now())); } } return true; diff --git a/src/client/views/DocumentButtonBar.tsx b/src/client/views/DocumentButtonBar.tsx index 30e41b06c..35b0b22a8 100644 --- a/src/client/views/DocumentButtonBar.tsx +++ b/src/client/views/DocumentButtonBar.tsx @@ -178,7 +178,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV e.preventDefault(); let googleDoc = await Cast(dataDoc.googleDoc, Doc); if (!googleDoc) { - const options = { _width: 600, _nativeWidth: 960, _nativeHeight: 800, useCors: false }; + const options = { _width: 600, _nativeWidth: 960, _nativeHeight: 800, data_useCors: false }; googleDoc = Docs.Create.WebDocument(googleDocUrl, options); dataDoc.googleDoc = googleDoc; } @@ -473,17 +473,17 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV </Tooltip> ); } - @observable _aliasDown = false; + @observable _embedDown = false; onTemplateButton = action((e: React.PointerEvent): void => { this._tooltipOpen = false; - setupMoveUpEvents(this, e, this.onAliasButtonMoved, emptyFunction, emptyFunction); + setupMoveUpEvents(this, e, this.onEmbedButtonMoved, emptyFunction, emptyFunction); }); - onAliasButtonMoved = () => { + onEmbedButtonMoved = () => { if (this._dragRef.current) { const dragDocView = this.view0!; const dragData = new DragManager.DocumentDragData([dragDocView.props.Document]); const [left, top] = dragDocView.props.ScreenToLocalTransform().inverse().transformPoint(0, 0); - dragData.defaultDropAction = 'alias'; + dragData.defaultDropAction = 'embed'; dragData.canEmbed = true; DragManager.StartDocumentDrag([dragDocView.ContentDiv!], dragData, left, top, { hideSource: false }); return true; @@ -498,14 +498,14 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV const view0 = this.view0; const views = this.props.views(); return !view0 ? null : ( - <Tooltip title={<div className="dash-tooltip">Tap to Customize Layout. Drag an embeddable alias</div>} open={this._tooltipOpen} onClose={action(() => (this._tooltipOpen = false))} placement="bottom"> + <Tooltip title={<div className="dash-tooltip">Tap to Customize Layout. Drag an embedding</div>} open={this._tooltipOpen} onClose={action(() => (this._tooltipOpen = false))} placement="bottom"> <div className="documentButtonBar-linkFlyout" ref={this._dragRef} onPointerEnter={action(() => !this._ref.current?.getBoundingClientRect().width && (this._tooltipOpen = true))}> <Flyout anchorPoint={anchorPoints.LEFT_TOP} - onOpen={action(() => (this._aliasDown = true))} - onClose={action(() => (this._aliasDown = false))} + onOpen={action(() => (this._embedDown = true))} + onClose={action(() => (this._embedDown = false))} content={ - !this._aliasDown ? null : ( + !this._embedDown ? null : ( <div ref={this._ref}> {' '} <TemplateMenu docViews={views.filter(v => v).map(v => v as DocumentView)} /> @@ -564,7 +564,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV const anchor = rootView.ComponentView?.getAnchor?.(false) ?? rootDoc; const trail = DocCast(anchor.presTrail) ?? Doc.MakeCopy(DocCast(Doc.UserDoc().emptyTrail), true); if (trail !== anchor.presTrail) { - DocUtils.MakeLink(anchor, trail, { linkRelationship: 'link trail' }); + DocUtils.MakeLink(anchor, trail, { link_relationship: 'link trail' }); anchor.presTrail = trail; } Doc.ActivePresentation = trail; @@ -589,7 +589,7 @@ export class DocumentButtonBar extends React.Component<{ views: () => (DocumentV <LinkPopup key="popup" showPopup={this._showLinkPopup} - linkCreated={link => (link.linkDisplay = !IsFollowLinkScript(this.props.views().lastElement()?.rootDoc.onClick))} + linkCreated={link => (link.link_displayLine = !IsFollowLinkScript(this.props.views().lastElement()?.rootDoc.onClick))} linkCreateAnchor={() => this.props.views().lastElement()?.ComponentView?.getAnchor?.(true)} linkFrom={() => this.props.views().lastElement()?.rootDoc} /> diff --git a/src/client/views/DocumentDecorations.tsx b/src/client/views/DocumentDecorations.tsx index a6b2db8f3..74cf58c7b 100644 --- a/src/client/views/DocumentDecorations.tsx +++ b/src/client/views/DocumentDecorations.tsx @@ -6,12 +6,13 @@ import { action, computed, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { FaUndo } from 'react-icons/fa'; import { DateField } from '../../fields/DateField'; -import { AclAdmin, AclAugment, AclEdit, Doc, DocListCast, Field, HeightSym, WidthSym } from '../../fields/Doc'; +import { AclAdmin, AclEdit, DataSym, Doc, DocListCast, Field, HeightSym, WidthSym } from '../../fields/Doc'; +import { Document } from '../../fields/documentSchemas'; import { InkField } from '../../fields/InkField'; import { RichTextField } from '../../fields/RichTextField'; import { ScriptField } from '../../fields/ScriptField'; import { Cast, NumCast, StrCast } from '../../fields/Types'; -import { GetEffectiveAcl, normalizeEmail } from '../../fields/util'; +import { GetEffectiveAcl } from '../../fields/util'; import { aggregateBounds, emptyFunction, numberValue, returnFalse, setupMoveUpEvents, Utils } from '../../Utils'; import { Docs } from '../documents/Documents'; import { DocumentType } from '../documents/DocumentTypes'; @@ -20,7 +21,7 @@ import { DragManager } from '../util/DragManager'; import { LinkFollower } from '../util/LinkFollower'; import { SelectionManager } from '../util/SelectionManager'; import { SnappingManager } from '../util/SnappingManager'; -import { undoBatch, UndoManager } from '../util/UndoManager'; +import { UndoManager } from '../util/UndoManager'; import { CollectionDockingView } from './collections/CollectionDockingView'; import { CollectionFreeFormView } from './collections/collectionFreeForm'; import { DocumentButtonBar } from './DocumentButtonBar'; @@ -34,7 +35,8 @@ import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; import { ImageBox } from './nodes/ImageBox'; import React = require('react'); import _ = require('lodash'); -import { SettingsManager } from '../util/SettingsManager'; +import { DocumentManager } from '../util/DocumentManager'; +import { isUndefined } from 'lodash'; @observer export class DocumentDecorations extends React.Component<{ PanelWidth: number; PanelHeight: number; boundsLeft: number; boundsTop: number }, { value: string }> { @@ -120,7 +122,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P titleFieldKey && SelectionManager.Views().forEach(d => { if (titleFieldKey === 'title') { - d.dataDoc['title-custom'] = !this._accumulatedTitle.startsWith('-'); + d.dataDoc.title_custom = !this._accumulatedTitle.startsWith('-'); if (StrCast(d.rootDoc.title).startsWith('@') && !this._accumulatedTitle.startsWith('@')) { Doc.RemoveDocFromList(Doc.MyPublishedDocs, undefined, d.rootDoc); } @@ -137,19 +139,19 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P if (curKey !== title) { if (title) { if (d.dataDoc[title] === undefined || d.dataDoc[title] instanceof RichTextField || typeof d.dataDoc[title] === 'string') { - d.rootDoc.layoutKey = `layout_${title}`; + d.rootDoc.layout_fieldKey = `layout_${title}`; d.rootDoc[`layout_${title}`] = FormattedTextBox.LayoutString(title); - d.rootDoc[`${title}-nativeWidth`] = d.rootDoc[`${title}-nativeHeight`] = 0; + d.rootDoc[`${title}_nativeWidth`] = d.rootDoc[`${title}_nativeHeight`] = 0; } } else { - d.rootDoc.layoutKey = undefined; + d.rootDoc.layout_fieldKey = undefined; } } } else { Doc.SetInPlace(d.rootDoc, titleFieldKey, titleField, true); } }), - 'title blur' + 'edit title' ); } }; @@ -198,7 +200,9 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P @action onBackgroundMove = (dragTitle: boolean, e: PointerEvent): boolean => { const dragDocView = SelectionManager.Views()[0]; - if (DocListCast(Doc.MyOverlayDocs.data).includes(dragDocView.rootDoc)) return false; + const containers = new Set<Doc | undefined>(); + SelectionManager.Views().forEach(v => containers.add(DocCast(v.rootDoc.embedContainer))); + if (containers.size > 1) return false; const { left, top } = dragDocView.getBounds() || { left: 0, top: 0 }; const dragData = new DragManager.DocumentDragData( SelectionManager.Views().map(dv => dv.props.Document), @@ -234,11 +238,6 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P .filter(v => v && v.props.renderDepth > 0); if (forceDeleteOrIconify === false && this._iconifyBatch) return; this._deleteAfterIconify = forceDeleteOrIconify || this._iconifyBatch ? true : false; - if (!this._iconifyBatch) { - this._iconifyBatch = UndoManager.StartBatch('iconifying'); - } else { - forceDeleteOrIconify = false; // can't force immediate close in the middle of iconifying -- have to wait until iconifying completes - } var iconifyingCount = views.length; const finished = action((force?: boolean) => { if ((force || --iconifyingCount === 0) && this._iconifyBatch) { @@ -257,6 +256,12 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P this._iconifyBatch = undefined; } }); + if (!this._iconifyBatch) { + this._iconifyBatch = UndoManager.StartBatch(forceDeleteOrIconify ? 'delete selected docs' : 'iconifying'); + } else { + forceDeleteOrIconify = false; // can't force immediate close in the middle of iconifying -- have to wait until iconifying completes + } + if (forceDeleteOrIconify) finished(forceDeleteOrIconify); else if (!this._deleteAfterIconify) views.forEach(dv => dv.iconify(finished)); }; @@ -279,28 +284,28 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P const selectedDocs = SelectionManager.Views(); if (selectedDocs.length) { if (e.ctrlKey) { - // open an alias in a new tab with Ctrl Key - CollectionDockingView.AddSplit(Doc.BestAlias(selectedDocs[0].props.Document), OpenWhereMod.right); + // open an embedding in a new tab with Ctrl Key + CollectionDockingView.AddSplit(Doc.BestEmbedding(selectedDocs[0].rootDoc), OpenWhereMod.right); } else if (e.shiftKey) { // open centered in a new workspace with Shift Key - const alias = Doc.MakeAlias(selectedDocs[0].props.Document); - alias.context = undefined; - alias.x = -alias[WidthSym]() / 2; - alias.y = -alias[HeightSym]() / 2; - CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([alias], { title: 'Tab for ' + alias.title }), OpenWhereMod.right); + const embedding = Doc.MakeEmbedding(selectedDocs[0].rootDoc); + embedding.embedContainer = undefined; + embedding.x = -embedding[Width]() / 2; + embedding.y = -embedding[Height]() / 2; + CollectionDockingView.AddSplit(Docs.Create.FreeformDocument([embedding], { title: 'Tab for ' + embedding.title }), OpenWhereMod.right); } else if (e.altKey) { // open same document in new tab - CollectionDockingView.ToggleSplit(selectedDocs[0].props.Document, OpenWhereMod.right); + CollectionDockingView.ToggleSplit(selectedDocs[0].rootDoc, OpenWhereMod.right); } else { - var openDoc = selectedDocs[0].props.Document; - if (openDoc.layoutKey === 'layout_icon') { - openDoc = DocListCast(openDoc.aliases).find(alias => !alias.context) ?? Doc.MakeAlias(openDoc); + var openDoc = selectedDocs[0].rootDoc; + if (openDoc.layout_fieldKey === 'layout_icon') { + openDoc = DocListCast(openDoc.proto_embeddings).find(embedding => !embedding.embedContainer) ?? Doc.MakeEmbedding(openDoc); Doc.deiconifyView(openDoc); } LightboxView.SetLightboxDoc( openDoc, undefined, - selectedDocs.slice(1).map(view => view.props.Document) + selectedDocs.slice(1).map(view => view.rootDoc) ); } } @@ -337,7 +342,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P const docMax = Math.min(NumCast(doc.width) / 2, NumCast(doc.height) / 2); const ratio = dist / maxDist; const radius = Math.min(1, ratio) * docMax; - doc.borderRounding = `${radius}px`; + doc.layout_borderRounding = `${radius}px`; }); return false; }, // moveEvent @@ -375,8 +380,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P const newloccentern = seldocview.props.ScreenToLocalTransform().transformPoint(rotCenter[0], rotCenter[1]); const newlocenter = [newloccentern[0] - NumCast(seldocview.layoutDoc._width) / 2, newloccentern[1] - NumCast(seldocview.layoutDoc._height) / 2]; const final = Utils.rotPt(newlocenter[0], newlocenter[1], -(NumCast(seldocview.rootDoc._rotation) / 180) * Math.PI); - seldocview.rootDoc.rotateCenterX = final.x / NumCast(seldocview.layoutDoc._width); - seldocview.rootDoc.rotateCenterY = final.y / NumCast(seldocview.layoutDoc._height); + seldocview.rootDoc.rotation_centerX = final.x / NumCast(seldocview.layoutDoc._width); + seldocview.rootDoc.rotation_centerY = final.y / NumCast(seldocview.layoutDoc._height); }; @action @@ -393,8 +398,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P action(action(() => (this._isRotating = false))), // upEvent action((e, doubleTap) => { if (doubleTap) { - seldocview.rootDoc.rotateCenterX = 0.5; - seldocview.rootDoc.rotateCenterY = 0.5; + seldocview.rootDoc.rotation_centerX = 0.5; + seldocview.rootDoc.rotation_centerY = 0.5; } }) ); @@ -404,7 +409,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P onRotateDown = (e: React.PointerEvent): void => { this._isRotating = true; const rcScreen = { X: this.rotCenter[0], Y: this.rotCenter[1] }; - const rotateUndo = UndoManager.StartBatch('rotatedown'); + const rotateUndo = UndoManager.StartBatch('drag rotation'); const selectedInk = SelectionManager.Views().filter(i => i.ComponentView instanceof InkingStroke); const centerPoint = this.rotCenter.slice(); const infos = new Map<Doc, { unrotatedDocPos: { x: number; y: number }; startRotCtr: { x: number; y: number }; accumRot: number }>(); @@ -472,7 +477,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P const bounds = e.currentTarget.getBoundingClientRect(); this._offX = this._resizeHdlId.toLowerCase().includes('left') ? bounds.right - e.clientX : bounds.left - e.clientX; this._offY = this._resizeHdlId.toLowerCase().includes('top') ? bounds.bottom - e.clientY : bounds.top - e.clientY; - this._resizeUndo = UndoManager.StartBatch('DocDecs resize'); + this._resizeUndo = UndoManager.StartBatch('drag resizing'); this._snapX = e.pageX; this._snapY = e.pageY; const ffviewSet = new Set<CollectionFreeFormView>(); @@ -595,7 +600,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P let dW = docwidth * (dWin / refWidth); let dH = docheight * (dHin / refHeight); const scale = docView.props.ScreenToLocalTransform().Scale; - const modifyNativeDim = (e.ctrlKey || doc.forceReflow) && doc.nativeDimModifiable && ((!dragBottom && !dragTop) || e.ctrlKey || doc.nativeHeightUnfrozen); + const modifyNativeDim = (e.ctrlKey && doc.nativeDimModifiable) || (doc.layout_forceReflow && !dragBottom && !dragTop) || (doc.nativeHeightUnfrozen && (dragBottom || dragTop || e.ctrlKey)); if (nwidth && nheight) { if (nwidth / nheight !== docwidth / docheight && !dragBottom && !dragTop) { docheight = (nheight / nwidth) * docwidth; @@ -611,7 +616,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P let dX = !dWin ? 0 : scale * refCent[0] * (1 - (1 + dWin / refWidth)); let dY = !dHin ? 0 : scale * refCent[1] * (1 - (1 + dHin / refHeight)); const preserveNativeDim = doc._nativeHeightUnfrozen === false && doc._nativeDimModifiable === false; - const fixedAspect = nwidth && nheight && (!doc._fitWidth || preserveNativeDim || e.ctrlKey || doc.nativeHeightUnfrozen || doc.nativeDimModifiable); + const fixedAspect = nwidth && nheight && (!doc._layout_fitWidth || preserveNativeDim || e.ctrlKey || doc.nativeHeightUnfrozen || doc.nativeDimModifiable); if (fixedAspect) { if ((Math.abs(dW) > Math.abs(dH) && ((!dragBottom && !dragTop) || !modifyNativeDim)) || dragRight) { if (dragRight && modifyNativeDim) { @@ -619,7 +624,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P doc._nativeWidth = (actualdW / (docwidth || 1)) * Doc.NativeWidth(doc); } } else { - if (!doc._fitWidth || preserveNativeDim) { + if (!doc._layout_fitWidth || preserveNativeDim) { actualdH = (nheight / nwidth) * actualdW; doc._height = actualdH; } else if (!modifyNativeDim || dragBotRight) { @@ -628,14 +633,14 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P } doc._width = actualdW; } else { - if ((dragBottom || dragTop) && (modifyNativeDim || (docView.layoutDoc.nativeHeightUnfrozen && docView.layoutDoc._fitWidth))) { + if ((dragBottom || dragTop) && (modifyNativeDim || (docView.layoutDoc.nativeHeightUnfrozen && docView.layoutDoc._layout_fitWidth))) { // frozen web pages, PDFs, and some RTFS have frozen nativewidth/height. But they are marked to allow their nativeHeight // to be explicitly modified with fitWidth and vertical resizing. (ie, with fitWidth they can't grow horizontally to match // a vertical resize so it makes more sense to change their nativeheight even if the ctrl key isn't used) doc._nativeHeight = (actualdH / (docheight || 1)) * Doc.NativeHeight(doc); - doc._autoHeight = false; + doc._layout_autoHeight = false; } else { - if (!doc._fitWidth || preserveNativeDim) { + if (!doc._layout_fitWidth || preserveNativeDim) { actualdW = (nwidth / nheight) * actualdH; doc._width = actualdW; } else if (!modifyNativeDim || dragBotRight) { @@ -643,17 +648,18 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P } } if (!modifyNativeDim) { - actualdH = Math.min((nheight / nwidth) * docwidth, actualdH); + actualdH = (nheight / nwidth) * docwidth; //, actualdH); } doc._height = actualdH; } } else { const rotCtr = [docwidth / 2, docheight / 2]; const tlRotated = Utils.rotPt(-rotCtr[0], -rotCtr[1], (NumCast(doc._rotation) / 180) * Math.PI); + const maxHeight = doc.nativeHeightUnfrozen || !nheight ? 0 : Math.max(nheight, NumCast(doc.scrollHeight, NumCast(doc[docView.LayoutFieldKey + '-scrollHeight']))) * docView.NativeDimScaling(); dH && (doc._height = actualdH > maxHeight && maxHeight ? maxHeight : actualdH); dW && (doc._width = actualdW); - dH && (doc._autoHeight = false); + dH && (doc._layout_autoHeight = false); const rotCtr2 = [NumCast(doc._width) / 2, NumCast(doc._height) / 2]; const tlRotated2 = Utils.rotPt(-rotCtr2[0], -rotCtr2[1], (NumCast(doc._rotation) / 180) * Math.PI); @@ -662,7 +668,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P } doc.x = NumCast(doc.x) + dX; doc.y = NumCast(doc.y) + dY; - doc._lastModified = new DateField(); + doc._layout_modificationDate = new DateField(); } const val = this._dragHeights.get(docView.layoutDoc); if (val) this._dragHeights.set(docView.layoutDoc, { start: val.start, lowest: Math.min(val.lowest, NumCast(docView.layoutDoc._height)) }); @@ -678,11 +684,11 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P this._resizeUndo?.end(); SnappingManager.clearSnapLines(); - // detect autoHeight gesture and apply + // detect layout_autoHeight gesture and apply SelectionManager.Views() .map(docView => ({ doc: docView.layoutDoc, hgts: this._dragHeights.get(docView.layoutDoc) })) .filter(pair => pair.hgts && pair.hgts.lowest < pair.hgts.start && pair.hgts.lowest <= 20) - .forEach(pair => (pair.doc._autoHeight = true)); + .forEach(pair => (pair.doc._layout_autoHeight = true)); //need to change points for resize, or else rotation/control points will fail. this._inkDragDocs .map(oldbds => ({ oldbds, inkPts: Cast(oldbds.doc.data, InkField)?.inkData || [] })) @@ -721,7 +727,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P } @computed get hasIcons() { - return SelectionManager.Views().some(docView => docView.rootDoc.layoutKey === 'layout_icon'); + return SelectionManager.Views().some(docView => docView.rootDoc.layout_fieldKey === 'layout_icon'); } @observable _showRotCenter = false; @@ -730,8 +736,8 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P if (SelectionManager.Views().length) { const seldocview = SelectionManager.Views()[0]; const loccenter = Utils.rotPt( - NumCast(seldocview.rootDoc.rotateCenterX) * NumCast(seldocview.layoutDoc._width), - NumCast(seldocview.rootDoc.rotateCenterY) * NumCast(seldocview.layoutDoc._height), + NumCast(seldocview.rootDoc.rotation_centerX) * NumCast(seldocview.layoutDoc._width), + NumCast(seldocview.rootDoc.rotation_centerY) * NumCast(seldocview.layoutDoc._height), (NumCast(seldocview.rootDoc._rotation) / 180) * Math.PI ); return seldocview.props @@ -781,15 +787,15 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P // hide the decorations if the parent chooses to hide it or if the document itself hides it const hideDecorations = seldocview.props.hideDecorations || seldocview.rootDoc.hideDecorations; - const hideResizers = hideDecorations || seldocview.props.hideResizeHandles || seldocview.rootDoc.hideResizeHandles || this._isRounding || this._isRotating; - const hideTitle = hideDecorations || seldocview.props.hideDecorationTitle || seldocview.rootDoc.hideDecorationTitle || this._isRounding || this._isRotating; - const hideDocumentButtonBar = hideDecorations || seldocview.props.hideDocumentButtonBar || seldocview.rootDoc.hideDocumentButtonBar || this._isRounding || this._isRotating; + const hideResizers = hideDecorations || seldocview.props.hideResizeHandles || seldocview.rootDoc.layout_hideResizeHandles || this._isRounding || this._isRotating; + const hideTitle = hideDecorations || seldocview.props.hideDecorationTitle || seldocview.rootDoc.layout_hideDecorationTitle || this._isRounding || this._isRotating; + const hideDocumentButtonBar = hideDecorations || seldocview.props.hideDocumentButtonBar || seldocview.rootDoc.layout_hideDocumentButtonBar || this._isRounding || this._isRotating; // if multiple documents have been opened at the same time, then don't show open button const hideOpenButton = hideDecorations || seldocview.props.hideOpenButton || - seldocview.rootDoc.hideOpenButton || - SelectionManager.Views().some(docView => docView.props.Document._stayInCollection || docView.props.Document.isGroup || docView.props.Document.hideOpenButton) || + seldocview.rootDoc.layout_hideOpenButton || + SelectionManager.Views().some(docView => docView.rootDoc._stayInCollection || docView.rootDoc.isGroup || docView.rootDoc.layout_hideOpenButton) || this._isRounding || this._isRotating; const hideDeleteButton = @@ -799,27 +805,12 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P seldocview.props.hideDeleteButton || seldocview.rootDoc.hideDeleteButton || SelectionManager.Views().some(docView => { - // const collectionAcl = docView.props.docViewPath()?.lastElement() ? GetEffectiveAcl(docView.props.docViewPath().lastElement().rootDoc[DataSym]) : AclEdit; - // return docView.rootDoc.stayInCollection || (collectionAcl !== AclAdmin && collectionAcl !== AclEdit && GetEffectiveAcl(docView.rootDoc) !== AclAdmin); - const effectiveAcl = GetEffectiveAcl(Doc.GetProto(seldocview.rootDoc)); - return docView.rootDoc.stayInCollection || (effectiveAcl !== AclAdmin && GetEffectiveAcl(docView.rootDoc) !== AclAdmin && effectiveAcl !== AclEdit && GetEffectiveAcl(docView.rootDoc) !== AclEdit); + const collectionAcl = docView.props.docViewPath()?.lastElement() ? GetEffectiveAcl(docView.props.docViewPath().lastElement().rootDoc[DataSym]) : AclEdit; + return docView.rootDoc.stayInCollection || (collectionAcl !== AclAdmin && collectionAcl !== AclEdit && GetEffectiveAcl(docView.rootDoc) !== AclAdmin); }); const topBtn = (key: string, icon: string, pointerDown: undefined | ((e: React.PointerEvent) => void), click: undefined | ((e: any) => void), title: string) => ( <Tooltip key={key} title={<div className="dash-tooltip">{title}</div>} placement="top"> - <div - className={`documentDecorations-${key}Button`} - onContextMenu={e => e.preventDefault()} - onPointerDown={ - pointerDown ?? - (e => - setupMoveUpEvents( - this, - e, - returnFalse, - emptyFunction, - undoBatch(e => click!(e)) - )) - }> + <div className={`documentDecorations-${key}Button`} onContextMenu={e => e.preventDefault()} onPointerDown={pointerDown ?? (e => setupMoveUpEvents(this, e, returnFalse, emptyFunction, e => click!(e)))}> <FontAwesomeIcon icon={icon as any} /> </div> </Tooltip> @@ -842,7 +833,7 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P const resizerScheme = colorScheme ? 'documentDecorations-resizer' + colorScheme : ''; // Radius constants - const useRounding = seldocview.ComponentView instanceof ImageBox || seldocview.ComponentView instanceof FormattedTextBox; + const useRounding = seldocview.ComponentView instanceof ImageBox || seldocview.ComponentView instanceof FormattedTextBox || seldocview.ComponentView instanceof CollectionFreeFormView; const borderRadius = numberValue(StrCast(seldocview.rootDoc.borderRounding)); const docMax = Math.min(NumCast(seldocview.rootDoc.width) / 2, NumCast(seldocview.rootDoc.height) / 2); const maxDist = Math.min((this.Bounds.r - this.Bounds.x) / 2, (this.Bounds.b - this.Bounds.y) / 2); @@ -905,18 +896,10 @@ export class DocumentDecorations extends React.Component<{ PanelWidth: number; P width: bounds.r - bounds.x + this._resizeBorderWidth + 'px', height: bounds.b - bounds.y + this._resizeBorderWidth + this._titleHeight + 'px', }}> - <div className="documentDecorations-topbar" onPointerDown={this.onContainerDown}> - {hideDeleteButton ? <div /> : topBtn('close', 'times', undefined, e => this.onCloseClick(true), 'Close')} - {hideResizers || hideDeleteButton ? <div /> : topBtn('minimize', 'window-maximize', undefined, e => this.onCloseClick(undefined), 'Minimize')} + <div className="documentDecorations-topbar" style={{ display: hideDeleteButton && hideTitle && hideOpenButton ? 'none' : undefined }} onPointerDown={this.onContainerDown}> + {hideDeleteButton ? null : topBtn('close', 'times', undefined, e => this.onCloseClick(true), 'Close')} + {hideResizers || hideDeleteButton ? null : topBtn('minimize', 'window-maximize', undefined, e => this.onCloseClick(undefined), 'Minimize')} {hideTitle ? null : titleArea} - {sharingMenu} - {hideOpenButton ? ( - <div /> - ) : seldocview.rootDoc._lockedPosition ? ( - topBtn('lock', 'lock', this.onLockDown, undefined, 'Toggle ability to interact with document') - ) : ( - topBtn('lock', 'unlock', this.onLockDown, undefined, 'Toggle ability to interact with document') - )} {hideOpenButton ? <div /> : topBtn('open', 'external-link-alt', this.onMaximizeDown, undefined, 'Open in Lightbox (ctrl: as alias, shift: in new collection)')} </div> {hideResizers ? null : ( diff --git a/src/client/views/EditableView.scss b/src/client/views/EditableView.scss index ed7ec9dc1..0955ba8ff 100644 --- a/src/client/views/EditableView.scss +++ b/src/client/views/EditableView.scss @@ -10,12 +10,14 @@ .editableView-container-editing-oneLine { width: 100%; + height: max-content; span { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - display: block; + p { + white-space: pre; + overflow: hidden; + text-overflow: ellipsis; + } } input { @@ -33,4 +35,3 @@ border: none; outline: none; } -
\ No newline at end of file diff --git a/src/client/views/EditableView.tsx b/src/client/views/EditableView.tsx index d1311a60a..7043edcee 100644 --- a/src/client/views/EditableView.tsx +++ b/src/client/views/EditableView.tsx @@ -1,9 +1,11 @@ import React = require('react'); -import { action, observable } from 'mobx'; +import { action, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as Autosuggest from 'react-autosuggest'; import { ObjectField } from '../../fields/ObjectField'; import './EditableView.scss'; +import { DocumentIconContainer } from './nodes/DocumentIcon'; +import { OverlayView } from './OverlayView'; export interface EditableProps { /** @@ -37,7 +39,8 @@ export interface EditableProps { onChange: (e: React.ChangeEvent, { newValue }: { newValue: string }) => void; autosuggestProps: Autosuggest.AutosuggestProps<string, any>; }; - oneLine?: boolean; + oneLine?: boolean; // whether to display the editable view as a single input line or as a textarea + allowCRs?: boolean; // can carriage returns be entered editing?: boolean; isEditingCallback?: (isEditing: boolean) => void; menuCallback?: (x: number, y: number) => void; @@ -45,6 +48,7 @@ export interface EditableProps { showMenuOnLoad?: boolean; background?: string | undefined; placeholder?: string; + wrap?: string; // nowrap, pre-wrap, etc } /** @@ -55,7 +59,9 @@ export interface EditableProps { @observer export class EditableView extends React.Component<EditableProps> { private _ref = React.createRef<HTMLDivElement>(); - private _inputref = React.createRef<HTMLInputElement>(); + private _inputref: HTMLInputElement | HTMLTextAreaElement | null = null; + _overlayDisposer?: () => void; + _editingDisposer?: IReactionDisposer; @observable _editing: boolean = false; constructor(props: EditableProps) { @@ -63,21 +69,53 @@ export class EditableView extends React.Component<EditableProps> { this._editing = this.props.editing ? true : false; } + componentDidMount(): void { + this._editingDisposer = reaction( + () => this._editing, + editing => { + if (editing) { + setTimeout(() => { + if (this._inputref?.value.startsWith('=') || this._inputref?.value.startsWith(':=')) { + this._overlayDisposer?.(); + this._overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 }); + } + }); + } else { + this._overlayDisposer?.(); + this._overlayDisposer = undefined; + } + }, + { fireImmediately: true } + ); + } + @action componentDidUpdate() { if (this._editing && this.props.editing === false) { - this._inputref.current?.value && this.finalizeEdit(this._inputref.current.value, false, true, false); + this._inputref?.value && this.finalizeEdit(this._inputref.value, false, true, false); } else if (this.props.editing !== undefined) { this._editing = this.props.editing; } } componentWillUnmount() { - this._inputref.current?.value && this.finalizeEdit(this._inputref.current.value, false, true, false); + this._overlayDisposer?.(); + this._editingDisposer?.(); + this._inputref?.value && this.finalizeEdit(this._inputref.value, false, true, false); } + onChange = (e: React.ChangeEvent) => { + const targVal = (e.target as any).value; + if (!(targVal.startsWith(':=') || targVal.startsWith('='))) { + this._overlayDisposer?.(); + this._overlayDisposer = undefined; + } else if (!this._overlayDisposer) { + this._overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 }); + } + }; + @action - onKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => { + onKeyDown = (e: React.KeyboardEvent<HTMLInputElement | HTMLTextAreaElement>) => { switch (e.key) { case 'Tab': e.stopPropagation(); @@ -89,13 +127,15 @@ export class EditableView extends React.Component<EditableProps> { if (!e.currentTarget.value) this.props.OnEmpty?.(); break; case 'Enter': - e.stopPropagation(); - if (!e.ctrlKey) { - this.finalizeEdit(e.currentTarget.value, e.shiftKey, false, true); - } else if (this.props.OnFillDown) { - this.props.OnFillDown(e.currentTarget.value); - this._editing = false; - this.props.isEditingCallback?.(false); + if (this.props.allowCRs !== true) { + e.stopPropagation(); + if (!e.ctrlKey) { + this.finalizeEdit(e.currentTarget.value, e.shiftKey, false, true); + } else if (this.props.OnFillDown) { + this.props.OnFillDown(e.currentTarget.value); + this._editing = false; + this.props.isEditingCallback?.(false); + } } break; case 'Escape': @@ -103,9 +143,6 @@ export class EditableView extends React.Component<EditableProps> { this._editing = false; this.props.isEditingCallback?.(false); break; - case ':': - this.props.menuCallback?.(e.currentTarget.getBoundingClientRect().x, e.currentTarget.getBoundingClientRect().y); - break; case 'ArrowUp': case 'ArrowDown': case 'ArrowLeft': @@ -117,8 +154,16 @@ export class EditableView extends React.Component<EditableProps> { case 'Meta': case 'Control': break; + case ':': + if (this.props.menuCallback) { + e.stopPropagation(); + this.props.menuCallback(e.currentTarget.getBoundingClientRect().x, e.currentTarget.getBoundingClientRect().y); + break; + } + default: if (this.props.textCallback?.(e.key)) { + e.stopPropagation(); this._editing = false; this.props.isEditingCallback?.(false); } @@ -186,15 +231,32 @@ export class EditableView extends React.Component<EditableProps> { onChange: this.props.autosuggestProps.onChange, }} /> - ) : ( + ) : this.props.oneLine !== false && this.props.GetValue()?.toString().indexOf('\n') === -1 ? ( <input className="editableView-input" - ref={this._inputref} + ref={r => (this._inputref = r)} style={{ display: this.props.display, overflow: 'auto', fontSize: this.props.fontSize, minWidth: 20, background: this.props.background }} placeholder={this.props.placeholder} onBlur={e => this.finalizeEdit(e.currentTarget.value, false, true, false)} defaultValue={this.props.GetValue()} autoFocus={true} + onChange={this.onChange} + onKeyDown={this.onKeyDown} + onKeyPress={this.stopPropagation} + onPointerDown={this.stopPropagation} + onClick={this.stopPropagation} + onPointerUp={this.stopPropagation} + /> + ) : ( + <textarea + className="editableView-input" + ref={r => (this._inputref = r)} + style={{ display: this.props.display, overflow: 'auto', fontSize: this.props.fontSize, minHeight: `min(100%, ${(this.props.GetValue()?.split('\n').length || 1) * 15})`, minWidth: 20, background: this.props.background }} + placeholder={this.props.placeholder} + onBlur={e => this.finalizeEdit(e.currentTarget.value, false, true, false)} + defaultValue={this.props.GetValue()} + autoFocus={true} + onChange={this.onChange} onKeyDown={this.onKeyDown} onKeyPress={this.stopPropagation} onPointerDown={this.stopPropagation} @@ -220,7 +282,7 @@ export class EditableView extends React.Component<EditableProps> { <div className={`editableView-container-editing${this.props.oneLine ? '-oneLine' : ''}`} ref={this._ref} - style={{ display: this.props.display, textOverflow: this.props.overflow, minHeight: '10px', whiteSpace: 'nowrap', height: this.props.height || 'auto', maxHeight: this.props.maxHeight }} + style={{ display: this.props.display, textOverflow: this.props.overflow, minHeight: '10px', whiteSpace: this.props.oneLine ? 'nowrap' : 'pre-line', height: this.props.height, maxHeight: this.props.maxHeight }} //onPointerDown={this.stopPropagation} onClick={this.onClick} placeholder={this.props.placeholder}> diff --git a/src/client/views/FilterPanel.tsx b/src/client/views/FilterPanel.tsx index 84b2e1407..fe42628cd 100644 --- a/src/client/views/FilterPanel.tsx +++ b/src/client/views/FilterPanel.tsx @@ -10,6 +10,7 @@ import { UserOptions } from '../util/GroupManager'; import './FilterPanel.scss'; import { FieldView } from './nodes/FieldView'; import { SearchBox } from './search/SearchBox'; +import { undoable } from '../util/UndoManager'; interface filterProps { rootDoc: Doc; @@ -31,7 +32,7 @@ export class FilterPanel extends React.Component<filterProps> { return targetView?.ComponentView?.annotationKey ?? targetView?.ComponentView?.fieldKey ?? 'data'; } @computed get targetDocChildren() { - return DocListCast(this.targetDoc?.[this.targetDocChildKey] || Doc.ActiveDashboard?.data); + return [...DocListCast(this.targetDoc?.[this.targetDocChildKey] || Doc.ActiveDashboard?.data), ...DocListCast(this.targetDoc[Doc.LayoutFieldKey(this.targetDoc) + '_sidebar'])]; } @computed get allDocs() { @@ -46,14 +47,14 @@ export class FilterPanel extends React.Component<filterProps> { @computed get _allFacets() { // trace(); const noviceReqFields = ['author', 'tags', 'text', 'type']; - const noviceLayoutFields: string[] = []; //["_curPage"]; + const noviceLayoutFields: string[] = []; //["_layout_curPage"]; const noviceFields = [...noviceReqFields, ...noviceLayoutFields]; const keys = new Set<string>(noviceFields); this.allDocs.forEach(doc => SearchBox.documentKeys(doc).filter(key => keys.add(key))); const sortedKeys = Array.from(keys.keys()) .filter(key => key[0]) - .filter(key => key.indexOf('lastModified') !== -1 || (key[0] === key[0].toUpperCase() && !key.startsWith('_')) || noviceFields.includes(key) || !Doc.noviceMode) + .filter(key => key.indexOf('modificationDate') !== -1 || (key[0] === key[0].toUpperCase() && !key.startsWith('_')) || noviceFields.includes(key) || !Doc.noviceMode) .sort(); noviceFields.forEach(key => sortedKeys.splice(sortedKeys.indexOf(key), 1)); return [...noviceFields, ...sortedKeys]; @@ -63,7 +64,7 @@ export class FilterPanel extends React.Component<filterProps> { * The current attributes selected to filter based on */ @computed get activeFilters() { - return StrListCast(this.targetDoc?._docFilters); + return StrListCast(this.targetDoc?._childFilters); } /** @@ -88,7 +89,8 @@ export class FilterPanel extends React.Component<filterProps> { (facetVal === true || facetVal == false) && valueSet.add(Field.toString(!facetVal)); const fieldKey = Doc.LayoutFieldKey(t); const annos = !Field.toString(Doc.LayoutField(t) as Field).includes('CollectionView'); - DocListCast(t[annos ? fieldKey + '-annotations' : fieldKey]).forEach(newdoc => newarray.push(newdoc)); + DocListCast(t[annos ? fieldKey + '_annotations' : fieldKey]).forEach(newdoc => newarray.push(newdoc)); + annos && DocListCast(t[fieldKey + '_sidebar']).forEach(newdoc => newarray.push(newdoc)); }); subDocs = newarray; } @@ -106,8 +108,8 @@ export class FilterPanel extends React.Component<filterProps> { @observable _chosenFacets = new ObservableMap<string, 'text' | 'checkbox' | 'slider' | 'range'>(); @computed get activeFacets() { const facets = new Map<string, 'text' | 'checkbox' | 'slider' | 'range'>(this._chosenFacets); - StrListCast(this.targetDoc?._docFilters).map(filter => facets.set(filter.split(':')[0], filter.split(':')[2] === 'match' ? 'text' : 'checkbox')); - setTimeout(() => StrListCast(this.targetDoc?._docFilters).map(action(filter => this._chosenFacets.set(filter.split(':')[0], filter.split(':')[2] === 'match' ? 'text' : 'checkbox')))); + StrListCast(this.targetDoc?._childFilters).map(filter => facets.set(filter.split(':')[0], filter.split(':')[2] === 'match' ? 'text' : 'checkbox')); + setTimeout(() => StrListCast(this.targetDoc?._childFilters).map(action(filter => this._chosenFacets.set(filter.split(':')[0], filter.split(':')[2] === 'match' ? 'text' : 'checkbox')))); return facets; } /** @@ -172,7 +174,7 @@ export class FilterPanel extends React.Component<filterProps> { <Select placeholder="Add a filter..." options={options} isMulti={false} onChange={val => this.facetClick((val as UserOptions).value)} onKeyDown={e => e.stopPropagation()} value={null} closeMenuOnSelect={true} /> </div> <div className="filterBox-select-bool"> - <select className="filterBox-selection" onChange={action(e => this.targetDoc && (this.targetDoc._filterBoolean = (e.target as any).value))} defaultValue={StrCast(this.targetDoc?.filterBoolean)}> + <select className="filterBox-selection" onChange={action(e => this.targetDoc && (this.targetDoc._childFilters_boolean = (e.target as any).value))} defaultValue={StrCast(this.targetDoc?.childFilters_boolean)}> {['AND', 'OR'].map(bool => ( <option value={bool} key={bool}> {bool} @@ -200,11 +202,12 @@ export class FilterPanel extends React.Component<filterProps> { return ( <input placeholder={ - StrListCast(this.targetDoc._docFilters) + StrListCast(this.targetDoc._childFilters) .find(filter => filter.split(':')[0] === facetHeader) ?.split(':')[1] ?? '-empty-' } - onKeyDown={e => e.key === 'Enter' && Doc.setDocFilter(this.targetDoc, facetHeader, e.currentTarget.value, !e.currentTarget.value ? 'remove' : 'match')} + onBlur={undoable(e => Doc.setDocFilter(this.targetDoc, facetHeader, e.currentTarget.value, !e.currentTarget.value ? 'remove' : 'match'), 'set text filter')} + onKeyDown={e => e.key === 'Enter' && undoable(e => Doc.setDocFilter(this.targetDoc, facetHeader, e.currentTarget.value, !e.currentTarget.value ? 'remove' : 'match'), 'set text filter')(e)} /> ); case 'checkbox': @@ -215,12 +218,12 @@ export class FilterPanel extends React.Component<filterProps> { <input style={{ width: 20, marginLeft: 20 }} checked={ - StrListCast(this.targetDoc._docFilters) + StrListCast(this.targetDoc._childFilters) .find(filter => filter.split(':')[0] === facetHeader && filter.split(':')[1] == facetValue) ?.split(':')[2] === 'check' } type={type} - onChange={e => Doc.setDocFilter(this.targetDoc, facetHeader, fval, e.target.checked ? 'check' : 'remove')} + onChange={undoable(e => Doc.setDocFilter(this.targetDoc, facetHeader, fval, e.target.checked ? 'check' : 'remove'), 'set filter')} /> {facetValue} </div> diff --git a/src/client/views/GestureOverlay.tsx b/src/client/views/GestureOverlay.tsx index b04769c1e..0951bff22 100644 --- a/src/client/views/GestureOverlay.tsx +++ b/src/client/views/GestureOverlay.tsx @@ -102,7 +102,7 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> { icon: 'clipboard', pointerUp: 'GestureOverlay.Instance.closeFloatingDoc()', pointerDown: 'GestureOverlay.Instance.openFloatingDoc(this.clipboard)', - clipboard: Docs.Create.FreeformDocument([], { _width: 300, _height: 300, system: true }), + clipboard: Docs.Create.FreeformDocument([], { _width: 300, _height: 300, isSystem: true }), backgroundColor: 'orange', }, { title: 'interpret text', icon: 'font', toolType: 'inktotext', pointerUp: "setToolglass('none')", pointerDown: 'setToolglass(self.toolType)', backgroundColor: 'orange' }, @@ -125,7 +125,7 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> { onPointerDown: data.pointerDown ? ScriptField.MakeScript(data.pointerDown) : undefined, backgroundColor: data.backgroundColor, dragFactory: data.dragFactory, - system: true, + isSystem: true, }) ); } @@ -138,19 +138,19 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> { ignoreClick: true, _lockedPosition: true, title: 'buttons', - _autoHeight: true, + _layout_autoHeight: true, _yMargin: 5, - linearViewIsExpanded: true, + linearView_IsExpanded: true, backgroundColor: 'white', - system: true, + isSystem: true, }); thumbDoc.inkToTextDoc = Docs.Create.LinearDocument([], { _width: 300, _height: 25, - _autoHeight: true, - linearViewIsExpanded: true, + _layout_autoHeight: true, + linearView_IsExpanded: true, flexDirection: 'column', - system: true, + isSystem: true, }); userDoc.thumbDoc = thumbDoc; } @@ -916,7 +916,7 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> { @computed get elements() { const selView = GestureOverlay.DownDocView; - const width = Number(ActiveInkWidth()) * NumCast(selView?.rootDoc._viewScale, 1); // * (selView?.props.ScreenToLocalTransform().Scale || 1); + const width = Number(ActiveInkWidth()) * NumCast(selView?.rootDoc._freeform_scale, 1); // * (selView?.props.ScreenToLocalTransform().Scale || 1); const rect = this._overlayRef.current?.getBoundingClientRect(); const B = { left: -20000, right: 20000, top: -20000, bottom: 20000, width: 40000, height: 40000 }; //this.getBounds(this._points, true); B.left = B.left - width / 2; @@ -1013,8 +1013,8 @@ export class GestureOverlay extends Touchable<GestureOverlayProps> { focus={emptyFunction} whenChildContentsActiveChanged={emptyFunction} bringToFront={emptyFunction} - docRangeFilters={returnEmptyFilter} - docFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} + childFilters={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} /> ); diff --git a/src/client/views/GlobalKeyHandler.ts b/src/client/views/GlobalKeyHandler.ts index 0f8b46dbe..47dcdd2e4 100644 --- a/src/client/views/GlobalKeyHandler.ts +++ b/src/client/views/GlobalKeyHandler.ts @@ -1,22 +1,18 @@ import { random } from 'lodash'; import { action, runInAction } from 'mobx'; -import { DateField } from '../../fields/DateField'; import { Doc, DocListCast } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; import { InkTool } from '../../fields/InkField'; -import { List } from '../../fields/List'; import { ScriptField } from '../../fields/ScriptField'; import { Cast, PromiseValue } from '../../fields/Types'; import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager'; -import { DocServer } from '../DocServer'; -import { DocumentType } from '../documents/DocumentTypes'; import { DragManager } from '../util/DragManager'; import { GroupManager } from '../util/GroupManager'; import { SelectionManager } from '../util/SelectionManager'; import { SettingsManager } from '../util/SettingsManager'; import { SharingManager } from '../util/SharingManager'; import { SnappingManager } from '../util/SnappingManager'; -import { undoBatch, UndoManager } from '../util/UndoManager'; +import { UndoManager } from '../util/UndoManager'; import { CollectionDockingView } from './collections/CollectionDockingView'; import { CollectionFreeFormView } from './collections/collectionFreeForm'; import { CollectionFreeFormViewChrome } from './collections/CollectionMenu'; @@ -161,12 +157,10 @@ export class KeyManager { case 'delete': case 'backspace': if (document.activeElement?.tagName !== 'INPUT' && document.activeElement?.tagName !== 'TEXTAREA') { - UndoManager.RunInBatch(() => { - if (LightboxView.LightboxDoc) { - LightboxView.SetLightboxDoc(undefined); - SelectionManager.DeselectAll(); - } else DocumentDecorations.Instance.onCloseClick(true); - }, 'backspace'); + if (LightboxView.LightboxDoc) { + LightboxView.SetLightboxDoc(undefined); + SelectionManager.DeselectAll(); + } else DocumentDecorations.Instance.onCloseClick(true); return { stopPropagation: true, preventDefault: true }; } break; @@ -352,38 +346,13 @@ export class KeyManager { }); public paste(e: ClipboardEvent) { - const plain = e.clipboardData?.getData('text/plain'); - const clone = plain?.startsWith('__DashCloneId('); - if (plain && (plain.startsWith('__DashDocId(') || clone)) { - const first = SelectionManager.Views().length ? SelectionManager.Views()[0] : undefined; - if (first?.props.Document.type === DocumentType.COL) { - const docids = plain.split(':'); - let count = 1; - const list: Doc[] = []; - const targetDataDoc = Doc.GetProto(first.props.Document); - const fieldKey = first.LayoutFieldKey; - const docList = DocListCast(targetDataDoc[fieldKey]); - docids.map( - (did, i) => - i && - DocServer.GetRefField(did).then(async doc => { - count++; - if (doc instanceof Doc) { - list.push(doc); - } - if (count === docids.length) { - const added = await Promise.all(list.filter(d => !docList.includes(d)).map(async d => (clone ? (await Doc.MakeClone(d, true)).clone : d))); - if (added.length) { - added.map(doc => (doc.context = targetDataDoc)); - undoBatch(() => { - targetDataDoc[fieldKey] = new List<Doc>([...docList, ...added]); - targetDataDoc[fieldKey + '-lastModified'] = new DateField(new Date(Date.now())); - })(); - } - } - }) - ); - } + const plain = e.clipboardData?.getData('text/plain'); // list of document ids, separated by ':'s + if (!plain) return; + const clone = plain.startsWith('__DashCloneId('); + const docids = plain.split(':'); // hack! docids[0] is the top left of the selection rectangle + const addDocument = SelectionManager.Views().lastElement()?.ComponentView?.addDocument; + if (addDocument && (plain.startsWith('__DashDocId(') || clone)) { + Doc.Paste(docids.slice(1), clone, addDocument); } } diff --git a/src/client/views/InkStrokeProperties.ts b/src/client/views/InkStrokeProperties.ts index 156825f41..d28981e17 100644 --- a/src/client/views/InkStrokeProperties.ts +++ b/src/client/views/InkStrokeProperties.ts @@ -50,21 +50,21 @@ export class InkStrokeProperties { if (!requireCurrPoint || this._currentPoint !== -1) { const doc = inkView.rootDoc; if (doc.type === DocumentType.INK && doc.width && doc.height) { - const ink = Cast(doc.data, InkField)?.inkData; + const ink = Cast(doc.stroke, InkField)?.inkData; if (ink) { const oldXrange = (xs => ({ coord: NumCast(doc.x), min: Math.min(...xs), max: Math.max(...xs) }))(ink.map(p => p.X)); const oldYrange = (ys => ({ coord: NumCast(doc.y), min: Math.min(...ys), max: Math.max(...ys) }))(ink.map(p => p.Y)); - const ptsXscale = (NumCast(doc._width) - NumCast(doc.strokeWidth)) / (oldXrange.max - oldXrange.min || 1) || 1; - const ptsYscale = (NumCast(doc._height) - NumCast(doc.strokeWidth)) / (oldYrange.max - oldYrange.min || 1) || 1; - const newPoints = func(inkView, ink, ptsXscale, ptsYscale, NumCast(doc.strokeWidth)); + const ptsXscale = (NumCast(doc._width) - NumCast(doc.stroke_width)) / (oldXrange.max - oldXrange.min || 1) || 1; + const ptsYscale = (NumCast(doc._height) - NumCast(doc.stroke_width)) / (oldYrange.max - oldYrange.min || 1) || 1; + const newPoints = func(inkView, ink, ptsXscale, ptsYscale, NumCast(doc.stroke_width)); if (newPoints) { const newXrange = (xs => ({ min: Math.min(...xs), max: Math.max(...xs) }))(newPoints.map(p => p.X)); const newYrange = (ys => ({ min: Math.min(...ys), max: Math.max(...ys) }))(newPoints.map(p => p.Y)); - doc._width = (newXrange.max - newXrange.min) * ptsXscale + NumCast(doc.strokeWidth); - doc._height = (newYrange.max - newYrange.min) * ptsYscale + NumCast(doc.strokeWidth); + doc._width = (newXrange.max - newXrange.min) * ptsXscale + NumCast(doc.stroke_width); + doc._height = (newYrange.max - newYrange.min) * ptsYscale + NumCast(doc.stroke_width); doc.x = oldXrange.coord + (newXrange.min - oldXrange.min) * ptsXscale; doc.y = oldYrange.coord + (newYrange.min - oldYrange.min) * ptsYscale; - Doc.GetProto(doc).data = new InkField(newPoints); + Doc.GetProto(doc).stroke = new InkField(newPoints); appliedFunc = true; } } diff --git a/src/client/views/InkTranscription.tsx b/src/client/views/InkTranscription.tsx index 246b887a6..e38474ea0 100644 --- a/src/client/views/InkTranscription.tsx +++ b/src/client/views/InkTranscription.tsx @@ -1,7 +1,8 @@ import * as iink from 'iink-js'; import { action, observable } from 'mobx'; import * as React from 'react'; -import { Doc, DocListCast, HeightSym, WidthSym } from '../../fields/Doc'; +import { Doc, DocListCast } from '../../fields/Doc'; +import { Height, Width } from '../../fields/DocSymbols'; import { InkData, InkField, InkTool } from '../../fields/InkField'; import { Cast, DateCast, NumCast } from '../../fields/Types'; import { aggregateBounds } from '../../Utils'; @@ -128,7 +129,7 @@ export class InkTranscription extends React.Component { const d = Cast(i.data, InkField, null); const inkStroke = DocumentManager.Instance.getDocumentView(i)?.ComponentView as InkingStroke; strokes.push(d.inkData.map(pd => inkStroke.ptToScreen({ X: pd.X, Y: pd.Y }))); - times.push(DateCast(i.creationDate).getDate().getTime()); + times.push(DateCast(i.author_date).getDate().getTime()); }); this.currGroup = groupDoc; @@ -172,7 +173,7 @@ export class InkTranscription extends React.Component { if (!selected) { return; } - const ctx = await Cast(selected[0].context, Doc); + const ctx = await Cast(selected[0].embedContainer, Doc); if (!ctx) { return; } @@ -221,7 +222,7 @@ export class InkTranscription extends React.Component { const docList = DocListCast(this.currGroup.data); docList.forEach((inkDoc: Doc) => { // just having the times match up and be a unique value (actual timestamp doesn't matter) - const ms = DateCast(inkDoc.creationDate).getDate().getTime() + 14400000; + const ms = DateCast(inkDoc.author_date).getDate().getTime() + 14400000; const word = timestampWord.get(ms); if (!word) { return; @@ -283,8 +284,8 @@ export class InkTranscription extends React.Component { action(d => { const x = NumCast(d.x); const y = NumCast(d.y); - const width = d[WidthSym](); - const height = d[HeightSym](); + const width = d[Width](); + const height = d[Height](); bounds.push({ x, y, width, height }); }) ); @@ -323,8 +324,8 @@ export class InkTranscription extends React.Component { // Gets a collection based on the selected nodes using a marquee view ref const newCollection = marqViewRef?.getCollection(selected, undefined, true); if (newCollection) { - newCollection.height = newCollection[HeightSym](); - newCollection.width = newCollection[WidthSym](); + newCollection.height = newCollection[Height](); + newCollection.width = newCollection[Width](); // if the grouping we are creating is an individual word if (word) { newCollection.title = word; diff --git a/src/client/views/InkingStroke.tsx b/src/client/views/InkingStroke.tsx index 2fd6cc4d6..111f1695f 100644 --- a/src/client/views/InkingStroke.tsx +++ b/src/client/views/InkingStroke.tsx @@ -6,7 +6,7 @@ point 1, tangent pt 1, tangent pt 2, point 2, point 3, tangent pt 3, ... (Note that segment endpoints are duplicated ie Point2 = Point 3) brokenIndices - an array of indexes into the data field where the incoming and outgoing tangents are not constrained to be equal text - a text field that will be centered within a closed ink stroke - isInkMask - a flag that makes the ink stroke render as a mask over its collection where the stroke itself is mixBlendMode multiplied by + stroke_isInkMask - a flag that makes the ink stroke render as a mask over its collection where the stroke itself is mixBlendMode multiplied by the underlying collection content, and everything outside the stroke is covered by a semi-opaque dark gray mask. The coordinates of the ink data need to be mapped to the screen since ink points are not changed when the DocumentView is translated or scaled. @@ -23,7 +23,8 @@ import React = require('react'); import { action, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; -import { Doc, HeightSym, WidthSym } from '../../fields/Doc'; +import { Doc } from '../../fields/Doc'; +import { Height, Width } from '../../fields/DocSymbols'; import { InkData, InkField } from '../../fields/InkField'; import { BoolCast, Cast, NumCast, RTFCast, StrCast } from '../../fields/Types'; import { TraceMobx } from '../../fields/util'; @@ -87,7 +88,13 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { if (!addAsAnnotation && !pinProps) return this.rootDoc; - const anchor = Docs.Create.InkAnchorDocument({ title: 'Ink anchor:' + this.rootDoc.title, presDuration: 1100, presTransition: 1000, unrendered: true, annotationOn: this.rootDoc }); + const anchor = Docs.Create.InkConfigDocument({ + title: 'Ink anchor:' + this.rootDoc.title, + // set presentation timing for restoring shape + presDuration: 1100, + presTransition: 1000, + annotationOn: this.rootDoc, + }); if (anchor) { anchor.backgroundColor = 'transparent'; // /* addAsAnnotation &&*/ this.addDocument(anchor); @@ -134,7 +141,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { * dimmed by a semi-opaque overlay mask. */ public static toggleMask = action((inkDoc: Doc) => { - inkDoc.isInkMask = !inkDoc.isInkMask; + inkDoc.stroke_isInkMask = !inkDoc.stroke_isInkMask; }); @observable controlUndo: UndoManager.Batch | undefined; /** @@ -241,7 +248,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { */ inkScaledData = () => { const inkData = Cast(this.dataDoc[this.fieldKey], InkField)?.inkData ?? []; - const inkStrokeWidth = NumCast(this.rootDoc.strokeWidth, 1); + const inkStrokeWidth = NumCast(this.rootDoc.stroke_width, 1); const inkTop = Math.min(...inkData.map(p => p.Y)) - inkStrokeWidth / 2; const inkBottom = Math.max(...inkData.map(p => p.Y)) + inkStrokeWidth / 2; const inkLeft = Math.min(...inkData.map(p => p.X)) - inkStrokeWidth / 2; @@ -310,9 +317,9 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { .map(p => ({ X: p[0], Y: p[1] })); const screenHdlPts = screenPts; - const startMarker = StrCast(this.layoutDoc.strokeStartMarker); - const endMarker = StrCast(this.layoutDoc.strokeEndMarker); - const markerScale = NumCast(this.layoutDoc.strokeMarkerScale); + const startMarker = StrCast(this.layoutDoc.stroke_startMarker); + const endMarker = StrCast(this.layoutDoc.stroke_endMarker); + const markerScale = NumCast(this.layoutDoc.stroke_markerScale); return SnappingManager.GetIsDragging() ? null : !InkStrokeProperties.Instance._controlButton ? ( !this.props.isSelected() || InkingStroke.IsClosed(inkData) ? null : ( <div className="inkstroke-UI" style={{ clip: `rect(${boundsTop}px, 10000px, 10000px, ${boundsLeft}px)` }}> @@ -328,14 +335,14 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { Colors.MEDIUM_BLUE, screenInkWidth[0], screenSpaceCenterlineStrokeWidth, - StrCast(inkDoc.strokeLineJoin), - StrCast(this.layoutDoc.strokeLineCap), - StrCast(inkDoc.strokeBezier), + StrCast(inkDoc.stroke_lineJoin), + StrCast(this.layoutDoc.stroke_lineCap), + StrCast(inkDoc.stroke_bezier), 'none', startMarker, endMarker, markerScale * Math.min(screenSpaceCenterlineStrokeWidth, screenInkWidth[0] / screenSpaceCenterlineStrokeWidth), - StrCast(inkDoc.strokeDash), + StrCast(inkDoc.stroke_dash), 1, 1, '', @@ -355,19 +362,19 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { TraceMobx(); const { inkData, inkStrokeWidth, inkLeft, inkTop, inkScaleX, inkScaleY, inkWidth, inkHeight } = this.inkScaledData(); - const startMarker = StrCast(this.layoutDoc.strokeStartMarker); - const endMarker = StrCast(this.layoutDoc.strokeEndMarker); - const markerScale = NumCast(this.layoutDoc.strokeMarkerScale, 1); + const startMarker = StrCast(this.layoutDoc.stroke_startMarker); + const endMarker = StrCast(this.layoutDoc.stroke_endMarker); + const markerScale = NumCast(this.layoutDoc.stroke_markerScale, 1); const closed = InkingStroke.IsClosed(inkData); - const isInkMask = BoolCast(this.layoutDoc.isInkMask); + const isInkMask = BoolCast(this.layoutDoc.stroke_isInkMask); const fillColor = isInkMask ? DashColor(StrCast(this.layoutDoc.fillColor, 'transparent')).blacken(0).rgb().toString() : this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FillColor) ?? 'transparent'; const strokeColor = !closed && fillColor && fillColor !== 'transparent' ? fillColor : this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color) ?? StrCast(this.layoutDoc.color); // bcz: Hack!! Not really sure why, but having fractional values for width/height of mask ink strokes causes the dragging clone (see DragManager) to be offset from where it should be. - if (isInkMask && (this.layoutDoc[WidthSym]() !== Math.round(this.layoutDoc[WidthSym]()) || this.layoutDoc[HeightSym]() !== Math.round(this.layoutDoc[HeightSym]()))) { + if (isInkMask && (this.layoutDoc[Width]() !== Math.round(this.layoutDoc[Width]()) || this.layoutDoc[Height]() !== Math.round(this.layoutDoc[Height]()))) { setTimeout(() => { - this.layoutDoc._width = Math.round(NumCast(this.layoutDoc[WidthSym]())); - this.layoutDoc._height = Math.round(NumCast(this.layoutDoc[HeightSym]())); + this.layoutDoc._width = Math.round(NumCast(this.layoutDoc[Width]())); + this.layoutDoc._height = Math.round(NumCast(this.layoutDoc[Height]())); }); } @@ -379,14 +386,14 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { strokeColor, inkStrokeWidth, inkStrokeWidth, - StrCast(this.layoutDoc.strokeLineJoin), - StrCast(this.layoutDoc.strokeLineCap), - StrCast(this.layoutDoc.strokeBezier), + StrCast(this.layoutDoc.stroke_lineJoin), + StrCast(this.layoutDoc.stroke_lineCap), + StrCast(this.layoutDoc.stroke_bezier), !closed ? 'none' : fillColor === 'transparent' ? 'none' : fillColor, startMarker, endMarker, markerScale, - StrCast(this.layoutDoc.strokeDash), + StrCast(this.layoutDoc.stroke_dash), inkScaleX, inkScaleY, '', @@ -396,20 +403,23 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { ); const highlight = !this.controlUndo && this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.Highlighting); const highlightIndex = highlight?.highlightIndex; - const highlightColor = highlight?.highlightIndex ? highlight?.highlightColor : StrCast(this.layoutDoc.strokeOutlineColor, !closed && fillColor && fillColor !== 'transparent' ? StrCast(this.layoutDoc.color, 'transparent') : 'transparent'); + const highlightColor = + (!this.props.isSelected() || !isInkMask) && highlight?.highlightIndex + ? highlight?.highlightColor + : StrCast(this.layoutDoc.stroke_outlineColor, !closed && fillColor && fillColor !== 'transparent' ? StrCast(this.layoutDoc.color, 'transparent') : 'transparent'); // Invisible polygonal line that enables the ink to be selected by the user. - const clickableLine = (downHdlr?: (e: React.PointerEvent) => void, suppressFill: boolean = false, mask: boolean = false) => + const clickableLine = (downHdlr?: (e: React.PointerEvent) => void, mask: boolean = false) => InteractionUtils.CreatePolyline( inkData, inkLeft, inkTop, - highlightColor, + mask && highlightColor === 'transparent' ? strokeColor : highlightColor, inkStrokeWidth, inkStrokeWidth + (fillColor ? (closed ? 2 : (highlightIndex ?? 0) + 2) : 2), - StrCast(this.layoutDoc.strokeLineJoin), - StrCast(this.layoutDoc.strokeLineCap), - StrCast(this.layoutDoc.strokeBezier), - !closed ? 'none' : !isInkMask && (fillColor === 'transparent' || suppressFill) ? 'none' : fillColor, + StrCast(this.layoutDoc.stroke_lineJoin), + StrCast(this.layoutDoc.stroke_lineCap), + StrCast(this.layoutDoc.stroke_bezier), + !closed || !fillColor || DashColor(fillColor).alpha() === 0 ? 'none' : fillColor, '', '', markerScale, @@ -423,7 +433,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { downHdlr, mask ); - const fsize = +StrCast(this.props.Document.fontSize, '12px').replace('px', ''); + const fsize = +StrCast(this.props.Document._text_fontSize, '12px').replace('px', ''); // bootsrap 3 style sheet sets line height to be 20px for default 14 point font size. // this attempts to figure out the lineHeight ratio by inquiring the body's lineHeight and dividing by the fontsize which should yield 1.428571429 // see: https://bibwild.wordpress.com/2019/06/10/bootstrap-3-to-4-changes-in-how-font-size-line-height-and-spacing-is-done-or-what-happened-to-line-height-computed/ @@ -444,12 +454,12 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { <svg className="inkStroke" style={{ - transform: this.props.Document.isInkMask ? `rotate(-${NumCast(this.props.CollectionFreeFormDocumentView?.().Rot)}deg) translate(${InkingStroke.MaskDim / 2}px, ${InkingStroke.MaskDim / 2}px)` : undefined, + transform: isInkMask ? `rotate(-${NumCast(this.props.CollectionFreeFormDocumentView?.().Rot)}deg) translate(${InkingStroke.MaskDim / 2}px, ${InkingStroke.MaskDim / 2}px)` : undefined, // mixBlendMode: this.layoutDoc.tool === InkTool.Highlighter ? 'multiply' : 'unset', cursor: this.props.isSelected() ? 'default' : undefined, }} {...interactions}> - {clickableLine(this.onPointerDown, undefined, isInkMask)} + {clickableLine(this.onPointerDown, isInkMask)} {isInkMask ? null : inkLine} </svg> {!closed || (!RTFCast(this.rootDoc.text)?.Text && (!this.props.isSelected() || Doc.UserDoc().activeInkHideTextLabels)) ? null : ( @@ -458,7 +468,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { style={{ color: StrCast(this.layoutDoc.textColor, 'black'), pointerEvents: this.props.isDocumentActive?.() ? 'all' : undefined, - width: this.layoutDoc[WidthSym](), + width: this.layoutDoc[Width](), transform: `scale(${this.props.NativeDimScaling?.() || 1})`, transformOrigin: 'top left', //top: (this.props.PanelHeight() - (lineHeightGuess * fsize + 20) * (this.props.NativeDimScaling?.() || 1)) / 2, @@ -469,7 +479,7 @@ export class InkingStroke extends ViewBoxBaseComponent<FieldViewProps>() { setContentView={this.setSubContentView} // this makes the inkingStroke the "dominant" component - ie, it will show the inking UI when selected (not text) yPadding={10} xPadding={10} - fieldKey={'text'} + fieldKey="text" fontSize={fsize} dontRegisterView={true} noSidebar={true} diff --git a/src/client/views/LightboxView.tsx b/src/client/views/LightboxView.tsx index c18a89481..286d39943 100644 --- a/src/client/views/LightboxView.tsx +++ b/src/client/views/LightboxView.tsx @@ -31,7 +31,7 @@ type LightboxSavedState = { panY: Opt<number>; scale: Opt<number>; scrollTop: Opt<number>; - layoutKey: Opt<string>; + layout_fieldKey: Opt<string>; }; @observer export class LightboxView extends React.Component<LightboxViewProps> { @@ -43,7 +43,7 @@ export class LightboxView extends React.Component<LightboxViewProps> { @observable private static _layoutTemplateString: Opt<string>; @observable private static _doc: Opt<Doc>; @observable private static _docTarget: Opt<Doc>; - @observable private static _docFilters: string[] = []; // filters + @observable private static _childFilters: string[] = []; // filters private static _savedState: Opt<LightboxSavedState>; private static _history: Opt<{ doc: Doc; target?: Doc }[]> = []; @observable private static _future: Opt<Doc[]> = []; @@ -51,30 +51,30 @@ export class LightboxView extends React.Component<LightboxViewProps> { static path: { doc: Opt<Doc>; target: Opt<Doc>; history: Opt<{ doc: Doc; target?: Doc }[]>; future: Opt<Doc[]>; saved: Opt<LightboxSavedState> }[] = []; @action public static SetLightboxDoc(doc: Opt<Doc>, target?: Doc, future?: Doc[], layoutTemplate?: Doc | string) { if (this.LightboxDoc && this.LightboxDoc !== doc && this._savedState) { - if (this._savedState.panX !== undefined) this.LightboxDoc._panX = this._savedState.panX; - if (this._savedState.panY !== undefined) this.LightboxDoc._panY = this._savedState.panY; - if (this._savedState.scrollTop !== undefined) this.LightboxDoc._scrollTop = this._savedState.scrollTop; - if (this._savedState.scale !== undefined) this.LightboxDoc._viewScale = this._savedState.scale; - this.LightboxDoc.layoutKey = this._savedState.layoutKey; + if (this._savedState.panX !== undefined) this.LightboxDoc._freeform_panX = this._savedState.panX; + if (this._savedState.panY !== undefined) this.LightboxDoc._freeform_panY = this._savedState.panY; + if (this._savedState.scrollTop !== undefined) this.LightboxDoc._layout_scrollTop = this._savedState.scrollTop; + if (this._savedState.scale !== undefined) this.LightboxDoc._freeform_scale = this._savedState.scale; + this.LightboxDoc.layout_fieldKey = this._savedState.layout_fieldKey; } if (!doc) { - this._docFilters && (this._docFilters.length = 0); + this._childFilters && (this._childFilters.length = 0); this._future = this._history = []; Doc.ActiveTool = InkTool.None; MainView.Instance._exploreMode = false; } else { const l = DocUtils.MakeLinkToActiveAudio(() => doc).lastElement(); - l && (Cast(l.anchor2, Doc, null).backgroundColor = 'lightgreen'); + l && (Cast(l.link_anchor_2, Doc, null).backgroundColor = 'lightgreen'); CollectionStackedTimeline.CurrentlyPlaying?.forEach(dv => dv.ComponentView?.Pause?.()); //TabDocView.PinDoc(doc, { hidePresBox: true }); this._history ? this._history.push({ doc, target }) : (this._history = [{ doc, target }]); if (doc !== LightboxView.LightboxDoc) { this._savedState = { - layoutKey: StrCast(doc.layoutKey), - panX: Cast(doc.panX, 'number', null), - panY: Cast(doc.panY, 'number', null), - scale: Cast(doc.viewScale, 'number', null), - scrollTop: Cast(doc.scrollTop, 'number', null), + layout_fieldKey: StrCast(doc.layout_fieldKey), + panX: Cast(doc.freeform_panX, 'number', null), + panY: Cast(doc.freeform_panY, 'number', null), + scale: Cast(doc.freeform_scale, 'number', null), + scrollTop: Cast(doc.layout_scrollTop, 'number', null), }; } } @@ -91,7 +91,7 @@ export class LightboxView extends React.Component<LightboxViewProps> { this._doc = doc; this._layoutTemplate = layoutTemplate instanceof Doc ? layoutTemplate : undefined; if (doc && (typeof layoutTemplate === 'string' ? layoutTemplate : undefined)) { - doc.layoutKey = layoutTemplate; + doc.layout_fieldKey = layoutTemplate; } this._docTarget = target || doc; @@ -134,7 +134,7 @@ export class LightboxView extends React.Component<LightboxViewProps> { @action public static SetCookie(cookie: string) { if (this.LightboxDoc && cookie) { - this._docFilters = (f => (this._docFilters ? [this._docFilters.push(f) as any, this._docFilters][1] : [f]))(`cookies:${cookie}:provide`); + this._childFilters = (f => (this._childFilters ? [this._childFilters.push(f) as any, this._childFilters][1] : [f]))(`cookies:${cookie}:provide`); } } public static AddDocTab = (doc: Doc, location: OpenWhere, layoutTemplate?: Doc | string) => { @@ -142,13 +142,13 @@ export class LightboxView extends React.Component<LightboxViewProps> { return LightboxView.SetLightboxDoc( doc, undefined, - [...DocListCast(doc[Doc.LayoutFieldKey(doc)]), ...DocListCast(doc[Doc.LayoutFieldKey(doc) + '-annotations']).filter(anno => anno.annotationOn !== doc), ...(LightboxView._future ?? [])].sort( + [...DocListCast(doc[Doc.LayoutFieldKey(doc)]), ...DocListCast(doc[Doc.LayoutFieldKey(doc) + '_annotations']).filter(anno => anno.annotationOn !== doc), ...(LightboxView._future ?? [])].sort( (a: Doc, b: Doc) => NumCast(b._timecodeToShow) - NumCast(a._timecodeToShow) ), layoutTemplate ); }; - docFilters = () => LightboxView._docFilters || []; + childFilters = () => LightboxView._childFilters || []; addDocTab = LightboxView.AddDocTab; @action public static Next() { const doc = LightboxView._doc!; @@ -156,17 +156,17 @@ export class LightboxView extends React.Component<LightboxViewProps> { const targetDocView = target && DocumentManager.Instance.getLightboxDocumentView(target); if (targetDocView && target) { const l = DocUtils.MakeLinkToActiveAudio(() => targetDocView.ComponentView?.getAnchor?.(true) || target).lastElement(); - l && (Cast(l.anchor2, Doc, null).backgroundColor = 'lightgreen'); + l && (Cast(l.link_anchor_2, Doc, null).backgroundColor = 'lightgreen'); DocumentManager.Instance.showDocument(target, { willZoomCentered: true, zoomScale: 0.9 }); if (LightboxView._history?.lastElement().target !== target) LightboxView._history?.push({ doc, target }); } else { if (!target && LightboxView.path.length) { const saved = LightboxView._savedState; if (LightboxView.LightboxDoc && saved) { - LightboxView.LightboxDoc._panX = saved.panX; - LightboxView.LightboxDoc._panY = saved.panY; - LightboxView.LightboxDoc._viewScale = saved.scale; - LightboxView.LightboxDoc._scrollTop = saved.scrollTop; + LightboxView.LightboxDoc._freeform_panX = saved.panX; + LightboxView.LightboxDoc._freeform_panY = saved.panY; + LightboxView.LightboxDoc._freeform_scale = saved.scale; + LightboxView.LightboxDoc._layout_scrollTop = saved.scrollTop; } const pop = LightboxView.path.pop(); if (pop) { @@ -210,7 +210,7 @@ export class LightboxView extends React.Component<LightboxViewProps> { const coll = LightboxView._docTarget; if (coll) { const fieldKey = Doc.LayoutFieldKey(coll); - const contents = [...DocListCast(coll[fieldKey]), ...DocListCast(coll[fieldKey + '-annotations'])]; + const contents = [...DocListCast(coll[fieldKey]), ...DocListCast(coll[fieldKey + '_annotations'])]; const links = LinkManager.Links(coll) .map(link => LinkManager.getOppositeAnchor(link, coll)) .filter(doc => doc) @@ -261,8 +261,8 @@ export class LightboxView extends React.Component<LightboxViewProps> { renderDepth={0} rootSelected={returnTrue} docViewPath={returnEmptyDoclist} - docFilters={this.docFilters} - docRangeFilters={returnEmptyFilter} + childFilters={this.childFilters} + childFiltersByRanges={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} addDocument={undefined} removeDocument={undefined} @@ -305,9 +305,9 @@ export class LightboxView extends React.Component<LightboxViewProps> { title="toggle fit width" onClick={e => { e.stopPropagation(); - LightboxView.LightboxDoc!._fitWidth = !LightboxView.LightboxDoc!._fitWidth; + LightboxView.LightboxDoc!._layout_fitWidth = !LightboxView.LightboxDoc!._layout_fitWidth; }}> - <FontAwesomeIcon icon={LightboxView.LightboxDoc?._fitWidth ? 'arrows-alt-h' : 'arrows-alt-v'} size="2x" /> + <FontAwesomeIcon icon={LightboxView.LightboxDoc?._layout_fitWidth ? 'arrows-alt-h' : 'arrows-alt-v'} size="2x" /> </div> <div className="lightboxView-tabBtn" diff --git a/src/client/views/MainView.tsx b/src/client/views/MainView.tsx index 5af9a9f9a..b0e992cb6 100644 --- a/src/client/views/MainView.tsx +++ b/src/client/views/MainView.tsx @@ -10,7 +10,7 @@ import 'normalize.css'; import * as React from 'react'; import { Doc, DocListCast, Opt } from '../../fields/Doc'; import { ScriptField } from '../../fields/ScriptField'; -import { StrCast } from '../../fields/Types'; +import { DocCast, StrCast } from '../../fields/Types'; import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, returnZero, setupMoveUpEvents, Utils } from '../../Utils'; import { GoogleAuthenticationManager } from '../apis/GoogleAuthenticationManager'; import { DocServer } from '../DocServer'; @@ -81,7 +81,7 @@ export class MainView extends React.Component { @observable private _sidebarContent: any = Doc.MyLeftSidebarPanel; @observable private _leftMenuFlyoutWidth: number = 0; @computed get _hideUI() { - return this.mainDoc && this.mainDoc._viewType !== CollectionViewType.Docking; + return this.mainDoc && this.mainDoc._type_collection !== CollectionViewType.Docking; } @computed private get dashboardTabHeight() { @@ -157,27 +157,28 @@ export class MainView extends React.Component { 'dataTransition', 'viewTransition', 'treeViewOpen', - 'showSidebar', + 'layout_showSidebar', + 'carousel_index', 'itemIndex', // for changing slides in presentations - 'sidebarWidthPercent', - 'currentTimecode', - 'timelineHeightPercent', + 'layout_sidebarWidthPercent', + 'layout_currentTimecode', + 'layout_timelineHeightPercent', 'presStatus', - 'panX', - 'panY', + 'freeform_panX', + 'freeform_panY', 'overlayX', 'overlayY', - 'fitWidth', + 'layout_fitWidth', 'nativeWidth', 'nativeHeight', - 'text-scrollHeight', - 'text-height', - 'hideMinimap', - 'viewScale', - 'scrollTop', + 'text_scrollHeight', + 'text_height', + 'layout_hideMinimap', + 'freeform_scale', + 'layout_scrollTop', 'hidden', - 'curPage', - 'viewType', + 'layout_curPage', + 'type_collection', 'chromeHidden', 'currentFrame', 'width', @@ -236,7 +237,7 @@ export class MainView extends React.Component { if (pathname.length > 1 && pathname[0] === 'doc') { DocServer.GetRefField(pathname[1]).then( action(field => { - if (field instanceof Doc && field._viewType !== CollectionViewType.Docking) { + if (field instanceof Doc && field._type_collection !== CollectionViewType.Docking) { Doc.GuestTarget = field; } }) @@ -607,8 +608,8 @@ export class MainView extends React.Component { focus={emptyFunction} whenChildContentsActiveChanged={emptyFunction} bringToFront={emptyFunction} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} /> </div> @@ -636,8 +637,8 @@ export class MainView extends React.Component { focus={emptyFunction} whenChildContentsActiveChanged={emptyFunction} bringToFront={emptyFunction} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} suppressSetHeight={true} renderDepth={this._hideUI ? 0 : -1} @@ -693,7 +694,7 @@ export class MainView extends React.Component { sidebarScreenToLocal = () => new Transform(0, -this.topOfSidebarDoc, 1); mainContainerXf = () => this.sidebarScreenToLocal().translate(-this.leftScreenOffsetOfMainDocView, 0); static addDocTabFunc_impl = (doc: Doc, location: OpenWhere): boolean => { - const whereFields = doc._viewType === CollectionViewType.Docking ? [OpenWhere.dashboard] : location.split(':'); + const whereFields = doc._type_collection === CollectionViewType.Docking ? [OpenWhere.dashboard] : location.split(':'); const keyValue = whereFields[1]?.includes('KeyValue'); const whereMods: OpenWhereMod = whereFields.length > 1 ? (whereFields[1].replace('KeyValue', '') as OpenWhereMod) : OpenWhereMod.none; if (doc.dockingConfig) return DashboardView.openDashboard(doc); @@ -735,8 +736,8 @@ export class MainView extends React.Component { focus={emptyFunction} whenChildContentsActiveChanged={emptyFunction} bringToFront={emptyFunction} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} /> </div> @@ -766,8 +767,8 @@ export class MainView extends React.Component { isContentActive={returnTrue} whenChildContentsActiveChanged={emptyFunction} bringToFront={emptyFunction} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} scriptContext={this} /> @@ -853,7 +854,7 @@ export class MainView extends React.Component { this._leftMenuFlyoutWidth = this._leftMenuFlyoutWidth || 250; //setTimeout(action(() => (this._leftMenuFlyoutWidth += 0.5))); - this._sidebarContent.proto = button.target as any; + this._sidebarContent.proto = DocCast(button.target); this.LastButton = button; }); @@ -880,8 +881,8 @@ export class MainView extends React.Component { <CollectionLinearView Document={Doc.MyDockedBtns} DataDoc={undefined} - fieldKey={'data'} - dropAction={'alias'} + fieldKey="data" + dropAction="embed" setHeight={returnFalse} styleProvider={DefaultStyleProvider} rootSelected={returnTrue} @@ -902,8 +903,8 @@ export class MainView extends React.Component { renderDepth={0} focus={emptyFunction} whenChildContentsActiveChanged={emptyFunction} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} /> {['watching', 'recording'].includes(String(this.userDoc?.presentationMode) ?? '') ? <div style={{ border: '.5rem solid green', padding: '5px' }}>{StrCast(this.userDoc?.presentationMode)}</div> : <></>} @@ -911,14 +912,14 @@ export class MainView extends React.Component { ); } @computed get snapLines() { - return !SelectionManager.Views().some(dv => dv.rootDoc.showSnapLines) ? null : ( + return !SelectionManager.Views().some(dv => dv.rootDoc.freeform_snapLines) ? null : ( <div className="mainView-snapLines"> <svg style={{ width: '100%', height: '100%' }}> - {SnappingManager.horizSnapLines().map(l => ( - <line x1="0" y1={l} x2="2000" y2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={'1 1'} /> + {SnappingManager.horizSnapLines().map((l, i) => ( + <line key={i} x1="0" y1={l} x2="2000" y2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={'1 1'} /> ))} - {SnappingManager.vertSnapLines().map(l => ( - <line y1="0" x1={l} y2="2000" x2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={'1 1'} /> + {SnappingManager.vertSnapLines().map((l, i) => ( + <line key={i} y1="0" x1={l} y2="2000" x2={l} stroke="black" opacity={0.3} strokeWidth={0.5} strokeDasharray={'1 1'} /> ))} </svg> </div> diff --git a/src/client/views/MarqueeAnnotator.tsx b/src/client/views/MarqueeAnnotator.tsx index a5638f390..11336d079 100644 --- a/src/client/views/MarqueeAnnotator.tsx +++ b/src/client/views/MarqueeAnnotator.tsx @@ -1,6 +1,6 @@ import { action, observable, ObservableMap, runInAction } from 'mobx'; import { observer } from 'mobx-react'; -import { AclAdmin, AclAugment, AclEdit, DataSym, Doc, Opt } from '../../fields/Doc'; +import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, DataSym, Doc, Opt } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; import { List } from '../../fields/List'; import { NumCast } from '../../fields/Types'; @@ -8,14 +8,13 @@ import { GetEffectiveAcl } from '../../fields/util'; import { unimplementedFunction, Utils } from '../../Utils'; import { Docs, DocUtils } from '../documents/Documents'; import { DragManager } from '../util/DragManager'; +import { FollowLinkScript } from '../util/LinkFollower'; import { undoBatch, UndoManager } from '../util/UndoManager'; import './MarqueeAnnotator.scss'; import { DocumentView } from './nodes/DocumentView'; import { FormattedTextBox } from './nodes/formattedText/FormattedTextBox'; import { AnchorMenu } from './pdf/AnchorMenu'; import React = require('react'); -import { ScriptField } from '../../fields/ScriptField'; -import { FollowLinkScript } from '../util/LinkFollower'; const _global = (window /* browser */ || global) /* node */ as any; export interface MarqueeAnnotatorProps { @@ -124,9 +123,9 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { DragManager.StartAnchorAnnoDrag([ele], new DragManager.AnchorAnnoDragData(this.props.docView, sourceAnchorCreator, targetCreator), e.pageX, e.pageY, { dragComplete: e => { if (!e.aborted && e.linkDocument) { - Doc.GetProto(e.linkDocument).linkRelationship = 'cropped image'; + Doc.GetProto(e.linkDocument).link_relationship = 'cropped image'; Doc.GetProto(e.linkDocument).title = 'crop: ' + this.props.docView.rootDoc.title; - Doc.GetProto(e.linkDocument).linkDisplay = false; + Doc.GetProto(e.linkDocument).link_displayLine = false; } }, }); @@ -154,16 +153,16 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { annotationOn: this.props.rootDoc, title: 'Annotation on ' + this.props.rootDoc.title, }); - marqueeAnno.x = NumCast(this.props.docView.props.Document.panXMin) + (parseInt(anno.style.left || '0') - containerOffset[0]) / scale / NumCast(this.props.docView.props.Document._viewScale, 1); - marqueeAnno.y = NumCast(this.props.docView.props.Document.panYMin) + (parseInt(anno.style.top || '0') - containerOffset[1]) / scale / NumCast(this.props.docView.props.Document._viewScale, 1) + NumCast(this.props.scrollTop); - marqueeAnno._height = parseInt(anno.style.height || '0') / scale / NumCast(this.props.docView.props.Document._viewScale, 1); - marqueeAnno._width = parseInt(anno.style.width || '0') / scale / NumCast(this.props.docView.props.Document._viewScale, 1); + marqueeAnno.x = NumCast(this.props.docView.props.Document.freeform_panX_min) + (parseInt(anno.style.left || '0') - containerOffset[0]) / scale / NumCast(this.props.docView.props.Document._freeform_scale, 1); + marqueeAnno.y = NumCast(this.props.docView.props.Document.freeform_panY_min) + (parseInt(anno.style.top || '0') - containerOffset[1]) / scale / NumCast(this.props.docView.props.Document._freeform_scale, 1) + NumCast(this.props.scrollTop); + marqueeAnno._height = parseInt(anno.style.height || '0') / scale / NumCast(this.props.docView.props.Document._freeform_scale, 1); + marqueeAnno._width = parseInt(anno.style.width || '0') / scale / NumCast(this.props.docView.props.Document._freeform_scale, 1); anno.remove(); savedAnnoMap.clear(); return marqueeAnno; } - const textRegionAnno = Docs.Create.HTMLAnchorDocument([], { + const textRegionAnno = Docs.Create.HTMLMarkerDocument([], { annotationOn: this.props.rootDoc, text: this.props.selectionText(), backgroundColor: 'transparent', @@ -209,7 +208,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { highlight = (color: string, isLinkButton: boolean, savedAnnotations?: ObservableMap<number, HTMLDivElement[]>, addAsAnnotation?: boolean, summarize?: boolean) => { // creates annotation documents for current highlights const effectiveAcl = GetEffectiveAcl(this.props.rootDoc[DataSym]); - const annotationDoc = [AclAugment, AclEdit, AclAdmin].includes(effectiveAcl) && this.makeAnnotationDocument(color, isLinkButton, savedAnnotations); + const annotationDoc = [AclAugment, AclSelfEdit, AclEdit, AclAdmin].includes(effectiveAcl) && this.makeAnnotationDocument(color, isLinkButton, savedAnnotations); addAsAnnotation && !savedAnnotations && annotationDoc && this.props.addDocument(annotationDoc); return (annotationDoc as Doc) ?? undefined; }; @@ -244,7 +243,7 @@ export class MarqueeAnnotator extends React.Component<MarqueeAnnotatorProps> { this._top = Math.min(this._startY, this._startY + this._height); this._width = Math.abs(this._width); this._height = Math.abs(this._height); - e.stopPropagation(); + //e.stopPropagation(); // overlay documents are all 'active', yet they can be dragged. if we stop propagation, then they can be marqueed but not dragged. if we don't stop, then they will be marqueed and dragged, but the marquee will be zero width since the doc will move along with the cursor. }; onSelectEnd = (e: PointerEvent) => { diff --git a/src/client/views/MetadataEntryMenu.tsx b/src/client/views/MetadataEntryMenu.tsx index 82ec5a5b3..bcbdd3ccb 100644 --- a/src/client/views/MetadataEntryMenu.tsx +++ b/src/client/views/MetadataEntryMenu.tsx @@ -1,5 +1,5 @@ import * as React from 'react'; -import "./MetadataEntryMenu.scss"; +import './MetadataEntryMenu.scss'; import { observer } from 'mobx-react'; import { observable, action, runInAction, trace, computed, IReactionDisposer, reaction } from 'mobx'; import { KeyValueBox } from './nodes/KeyValueBox'; @@ -16,9 +16,9 @@ export interface MetadataEntryProps { } @observer -export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{ - @observable private _currentKey: string = ""; - @observable private _currentValue: string = ""; +export class MetadataEntryMenu extends React.Component<MetadataEntryProps> { + @observable private _currentKey: string = ''; + @observable private _currentValue: string = ''; private _addChildren: boolean = false; @observable _allSuggestions: string[] = []; _suggestionDispser: IReactionDisposer | undefined; @@ -32,7 +32,7 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{ if (!this.userModified) { this.previewValue(); } - } + }; previewValue = async () => { let field: Field | undefined | null = null; @@ -45,30 +45,30 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{ if (field === null) { field = v; } else if (v !== field) { - value = "multiple values"; + value = 'multiple values'; } } if (value === undefined) { if (field !== null && field !== undefined) { - value = (onProto ? "" : "= ") + Field.toScriptString(field); + value = (onProto ? '' : '= ') + Field.toScriptString(field); } else { - value = ""; + value = ''; } } const s = value; - runInAction(() => this._currentValue = s); - } + runInAction(() => (this._currentValue = s)); + }; @action onValueChange = (e: React.ChangeEvent<HTMLInputElement>) => { this._currentValue = e.target.value; - this.userModified = e.target.value.trim() !== ""; - } + this.userModified = e.target.value.trim() !== ''; + }; @undoBatch @action onValueKeyDown = async (e: React.KeyboardEvent) => { - if (e.key === "Enter") { + if (e.key === 'Enter') { e.stopPropagation(); const script = KeyValueBox.CompileKVPScript(this._currentValue); if (!script) return; @@ -95,18 +95,18 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{ this.clearInputs(); } } - } + }; @action clearInputs = () => { - this._currentKey = ""; - this._currentValue = ""; + this._currentKey = ''; + this._currentValue = ''; this.userModified = false; if (this.autosuggestRef.current) { const input: HTMLInputElement = (this.autosuggestRef.current as any).input; input && input.focus(); } - } + }; getKeySuggestions = (value: string) => { value = value.toLowerCase(); @@ -114,16 +114,18 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{ const keys = new Set<string>(); docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key))); return Array.from(keys).filter(key => key.toLowerCase().startsWith(value)); - } + }; getSuggestionValue = (suggestion: string) => suggestion; renderSuggestion = (suggestion: string) => { - return (null); - } + return null; + }; componentDidMount() { - this._suggestionDispser = reaction(() => this._currentKey, - () => this._allSuggestions = this.getKeySuggestions(this._currentKey), - { fireImmediately: true }); + this._suggestionDispser = reaction( + () => this._currentKey, + () => (this._allSuggestions = this.getKeySuggestions(this._currentKey)), + { fireImmediately: true } + ); } componentWillUnmount() { this._suggestionDispser && this._suggestionDispser(); @@ -131,49 +133,64 @@ export class MetadataEntryMenu extends React.Component<MetadataEntryProps>{ onClick = (e: React.ChangeEvent<HTMLInputElement>) => { this._addChildren = !this._addChildren; - } + }; private get considerChildOptions() { - if (!this.props.docs.every(doc => doc._viewType !== undefined)) { + if (!this.props.docs.every(doc => doc._type_collection !== undefined)) { return null; } return ( - <div style={{ display: "flex" }}> + <div style={{ display: 'flex' }}> Children: - <input type="checkbox" onChange={this.onClick} ></input> + <input type="checkbox" onChange={this.onClick}></input> </div> ); } _ref = React.createRef<HTMLInputElement>(); render() { - return (<div className="metadataEntry-outerDiv" id="metadataEntry-outer" onPointerDown={e => e.stopPropagation()}> - <div className="metadataEntry-inputArea"> - <div style={{ display: "flex", flexDirection: "row" }}> - <span>Key:</span> - <div className="metadataEntry-autoSuggester" onClick={e => this.autosuggestRef.current!.input?.focus()} > - <Autosuggest inputProps={{ value: this._currentKey, onChange: this.onKeyChange }} - getSuggestionValue={this.getSuggestionValue} - suggestions={emptyPath} - alwaysRenderSuggestions={false} - renderSuggestion={this.renderSuggestion} - onSuggestionsFetchRequested={emptyFunction} - onSuggestionsClearRequested={emptyFunction} - ref={this.autosuggestRef} /> + return ( + <div className="metadataEntry-outerDiv" id="metadataEntry-outer" onPointerDown={e => e.stopPropagation()}> + <div className="metadataEntry-inputArea"> + <div style={{ display: 'flex', flexDirection: 'row' }}> + <span>Key:</span> + <div className="metadataEntry-autoSuggester" onClick={e => this.autosuggestRef.current!.input?.focus()}> + <Autosuggest + inputProps={{ value: this._currentKey, onChange: this.onKeyChange }} + getSuggestionValue={this.getSuggestionValue} + suggestions={emptyPath} + alwaysRenderSuggestions={false} + renderSuggestion={this.renderSuggestion} + onSuggestionsFetchRequested={emptyFunction} + onSuggestionsClearRequested={emptyFunction} + ref={this.autosuggestRef} + /> + </div> </div> + <div style={{ display: 'flex', flexDirection: 'row' }}> + <span>Value:</span> + <input className="metadataEntry-input" ref={this._ref} value={this._currentValue} onClick={e => this._ref.current!.focus()} onChange={this.onValueChange} onKeyDown={this.onValueKeyDown} /> + </div> + {this.considerChildOptions} </div> - <div style={{ display: "flex", flexDirection: "row" }}> - <span>Value:</span> - <input className="metadataEntry-input" ref={this._ref} value={this._currentValue} onClick={e => this._ref.current!.focus()} onChange={this.onValueChange} onKeyDown={this.onValueKeyDown} /> + <div className="metadataEntry-keys"> + <ul> + {this._allSuggestions + .slice() + .sort() + .map(s => ( + <li + key={s} + onClick={action(() => { + this._currentKey = s; + this.previewValue(); + })}> + {s} + </li> + ))} + </ul> </div> - {this.considerChildOptions} - </div> - <div className="metadataEntry-keys" > - <ul> - {this._allSuggestions.slice().sort().map(s => <li key={s} onClick={action(() => { this._currentKey = s; this.previewValue(); })} >{s}</li>)} - </ul> </div> - </div> ); } -}
\ No newline at end of file +} diff --git a/src/client/views/OverlayView.tsx b/src/client/views/OverlayView.tsx index 9baedae6d..339507f65 100644 --- a/src/client/views/OverlayView.tsx +++ b/src/client/views/OverlayView.tsx @@ -3,7 +3,8 @@ import { observer } from 'mobx-react'; import { computedFn } from 'mobx-utils'; import * as React from 'react'; import ReactLoading from 'react-loading'; -import { Doc, DocListCast, HeightSym, WidthSym } from '../../fields/Doc'; +import { Doc, DocListCast } from '../../fields/Doc'; +import { Height, Width } from '../../fields/DocSymbols'; import { Id } from '../../fields/FieldSymbols'; import { NumCast } from '../../fields/Types'; import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents, Utils } from '../../Utils'; @@ -159,7 +160,7 @@ export class OverlayView extends React.Component { } removeOverlayDoc = (doc: Doc | Doc[]) => { - (doc instanceof Doc ? [doc] : doc).forEach(doc => Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, doc)); + (doc instanceof Doc ? [doc] : doc).forEach(Doc.RemFromMyOverlay); return true; }; @@ -177,6 +178,7 @@ export class OverlayView extends React.Component { offsety = 0; const dref = React.createRef<HTMLDivElement>(); const onPointerMove = action((e: PointerEvent, down: number[]) => { + if (e.cancelBubble) return false; // if the overlay doc processed the move event (e.g., to pan its contents), then the event should be marked as canceled since propagation can't be stopped if (e.buttons === 1) { d.overlayX = e.clientX + offsetx; d.overlayY = e.clientY + offsety; @@ -196,7 +198,7 @@ export class OverlayView extends React.Component { }); const onPointerDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, onPointerMove, emptyFunction, emptyFunction); + setupMoveUpEvents(this, e, onPointerMove, emptyFunction, emptyFunction, false); offsetx = NumCast(d.overlayX) - e.clientX; offsety = NumCast(d.overlayY) - e.clientY; }; @@ -213,8 +215,8 @@ export class OverlayView extends React.Component { bringToFront={emptyFunction} addDocument={undefined} removeDocument={this.removeOverlayDoc} - PanelWidth={d[WidthSym]} - PanelHeight={d[HeightSym]} + PanelWidth={d[Width]} + PanelHeight={d[Height]} ScreenToLocalTransform={this.docScreenToLocalXf(d)} renderDepth={1} hideDecorations={true} @@ -226,8 +228,8 @@ export class OverlayView extends React.Component { docViewPath={returnEmptyDoclist} addDocTab={DocumentViewInternal.addDocTabFunc} pinToPres={emptyFunction} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} /> </div> diff --git a/src/client/views/Palette.tsx b/src/client/views/Palette.tsx index 3ad28c418..749eb08a2 100644 --- a/src/client/views/Palette.tsx +++ b/src/client/views/Palette.tsx @@ -56,8 +56,8 @@ export default class Palette extends React.Component<PaletteProps> { styleProvider={returnEmptyString} whenChildContentsActiveChanged={emptyFunction} bringToFront={emptyFunction} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} /> <div className="palette-cover" style={{ transform: `translate(${Math.max(0, this._selectedIndex) * 50.75 + 23}px, 0px)` }}></div> diff --git a/src/client/views/PreviewCursor.tsx b/src/client/views/PreviewCursor.tsx index c7a603897..b513fe245 100644 --- a/src/client/views/PreviewCursor.tsx +++ b/src/client/views/PreviewCursor.tsx @@ -2,9 +2,8 @@ import { action, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc } from '../../fields/Doc'; -import { Cast, NumCast, StrCast } from '../../fields/Types'; +import { StrCast } from '../../fields/Types'; import { returnFalse } from '../../Utils'; -import { DocServer } from '../DocServer'; import { Docs, DocumentOptions, DocUtils } from '../documents/Documents'; import { ImageUtils } from '../util/Import & Export/ImageUtils'; import { Transform } from '../util/Transform'; @@ -57,7 +56,7 @@ export class PreviewCursor extends React.Component<{}> { title: url, _width: 500, _height: 300, - useCors: true, + data_useCors: true, x: newPoint[0], y: newPoint[1], }) @@ -66,23 +65,11 @@ export class PreviewCursor extends React.Component<{}> { } else if (plain.startsWith('__DashDocId(') || plain.startsWith('__DashCloneId(')) { const clone = plain.startsWith('__DashCloneId('); const docids = plain.split(':'); - const strs = docids[0].split(','); + const strs = docids[0].split(','); // hack! docids[0] is the top left of the selection rectangle const ptx = Number(strs[0].substring((clone ? '__DashCloneId(' : '__DashDocId(').length)); const pty = Number(strs[1].substring(0, strs[1].length - 1)); + Doc.Paste(docids.slice(1), clone, PreviewCursor._addDocument, ptx, pty, newPoint); - const batch = UndoManager.StartBatch('cloning'); - { - const toCopy = await Promise.all(docids.slice(1).map(async did => Cast(await DocServer.GetRefField(did), Doc, null))); - const docs = clone ? (await Promise.all(Doc.MakeClones(toCopy, false))).map(res => res.clone) : toCopy; - const firstx = docs.length ? NumCast(docs[0].x) + ptx - newPoint[0] : 0; - const firsty = docs.length ? NumCast(docs[0].y) + pty - newPoint[1] : 0; - docs.map(doc => { - doc.x = NumCast(doc.x) - firstx; - doc.y = NumCast(doc.y) - firsty; - }); - PreviewCursor._addDocument(docs); - } - batch.end(); e.stopPropagation(); } else { FormattedTextBox.PasteOnLoad = e; diff --git a/src/client/views/PropertiesButtons.tsx b/src/client/views/PropertiesButtons.tsx index cf808f801..11b89fd69 100644 --- a/src/client/views/PropertiesButtons.tsx +++ b/src/client/views/PropertiesButtons.tsx @@ -6,22 +6,21 @@ import { Doc, DocListCast, Opt } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; import { InkField } from '../../fields/InkField'; import { RichTextField } from '../../fields/RichTextField'; -import { ScriptField } from '../../fields/ScriptField'; import { BoolCast, ScriptCast, StrCast } from '../../fields/Types'; import { ImageField } from '../../fields/URLField'; import { Utils } from '../../Utils'; import { DocUtils } from '../documents/Documents'; import { CollectionViewType, DocumentType } from '../documents/DocumentTypes'; +import { IsFollowLinkScript } from '../util/LinkFollower'; import { LinkManager } from '../util/LinkManager'; import { SelectionManager } from '../util/SelectionManager'; -import { undoBatch } from '../util/UndoManager'; +import { undoable, undoBatch } from '../util/UndoManager'; import { Colors } from './global/globalEnums'; import { InkingStroke } from './InkingStroke'; import { DocumentView, OpenWhere } from './nodes/DocumentView'; import { pasteImageBitmap } from './nodes/WebBoxRenderer'; import './PropertiesButtons.scss'; import React = require('react'); -import { IsFollowLinkScript } from '../util/LinkFollower'; const higflyout = require('@hig/flyout'); export const { anchorPoints } = higflyout; export const Flyout = higflyout.default; @@ -51,11 +50,11 @@ export class PropertiesButtons extends React.Component<{}, {}> { <div className={`propertiesButtons-linkButton-empty toggle-${StrCast(targetDoc[property]).includes(':hover') ? 'hover' : targetDoc[property] ? 'on' : 'off'}`} onPointerDown={e => e.stopPropagation()} - onClick={undoBatch(() => { + onClick={undoable(() => { if (SelectionManager.Views().length > 1) { SelectionManager.Views().forEach(dv => (onClick ?? onPropToggle)(dv, dv.rootDoc, property)); } else if (targetDoc) (onClick ?? onPropToggle)(undefined, targetDoc, property); - })}> + }, property)}> <FontAwesomeIcon className="documentdecorations-icon" size="lg" icon={icon(BoolCast(targetDoc?.[property])) as any} /> </div> <div className="propertiesButtons-title">{label}</div> @@ -74,7 +73,7 @@ export class PropertiesButtons extends React.Component<{}, {}> { @computed get maskButton() { return this.propertyToggleBtn( 'Mask', - 'isInkMask', + 'stroke_isInkMask', on => (on ? 'Make plain ink' : 'Make highlight mask'), on => 'paint-brush', (dv, doc) => InkingStroke.toggleMask(dv?.layoutDoc || doc) @@ -91,7 +90,7 @@ export class PropertiesButtons extends React.Component<{}, {}> { @computed get clustersButton() { return this.propertyToggleBtn( 'Clusters', - '_useClusters', + '_freeform_useClusters', on => `${on ? 'Hide' : 'Show'} clusters`, on => 'braille' ); @@ -115,7 +114,7 @@ export class PropertiesButtons extends React.Component<{}, {}> { @computed get fitContentButton() { return this.propertyToggleBtn( 'View All', - '_fitContentsToBox', + '_freeform_fitContentsToBox', on => `${on ? "Don't" : 'Do'} fit content to container visible area`, on => 'object-group' ); @@ -134,22 +133,22 @@ export class PropertiesButtons extends React.Component<{}, {}> { const containerDoc = dv.rootDoc; //containerDoc.followAllLinks = // containerDoc.noShadow = - // containerDoc.disableDocBrushing = + // containerDoc.layout_disableBrushing = // containerDoc._forceActive = - //containerDoc._fitContentsToBox = + //containerDoc._freeform_fitContentsToBox = containerDoc._isLightbox = !containerDoc._isLightbox; //containerDoc._xPadding = containerDoc._yPadding = containerDoc._isLightbox ? 10 : undefined; const containerContents = DocListCast(dv.dataDoc[dv.props.fieldKey ?? Doc.LayoutFieldKey(containerDoc)]); //dv.rootDoc.onClick = ScriptField.MakeScript('{self.data = undefined; documentView.select(false)}', { documentView: 'any' }); - containerContents.forEach(doc => LinkManager.Links(doc).forEach(link => (link.linkDisplay = false))); + containerContents.forEach(doc => LinkManager.Links(doc).forEach(link => (link.link_displayLine = false))); }); } ); } - @computed get fitWidthButton() { + @computed get layout_fitWidthButton() { return this.propertyToggleBtn( 'Fit\xA0Width', - '_fitWidth', + '_layout_fitWidth', on => `${on ? "Don't" : 'Do'} fit content to width of container`, on => 'arrows-alt-h' ); @@ -157,10 +156,10 @@ export class PropertiesButtons extends React.Component<{}, {}> { @computed get captionButton() { return this.propertyToggleBtn( 'Caption', - '_showCaption', + '_layout_showCaption', on => `${on ? 'Hide' : 'Show'} caption footer`, on => 'closed-captioning', - (dv, doc) => ((dv?.rootDoc || doc)._showCaption = (dv?.rootDoc || doc)._showCaption === undefined ? 'caption' : undefined) + (dv, doc) => ((dv?.rootDoc || doc)._layout_showCaption = (dv?.rootDoc || doc)._layout_showCaption === undefined ? 'caption' : undefined) ); } @computed get chromeButton() { @@ -175,20 +174,20 @@ export class PropertiesButtons extends React.Component<{}, {}> { @computed get titleButton() { return this.propertyToggleBtn( 'Title', - '_showTitle', + '_layout_showTitle', on => 'Switch between title styles', on => 'text-width', (dv, doc) => { const tdoc = dv?.rootDoc || doc; - const newtitle = !tdoc._showTitle ? 'title' : tdoc._showTitle === 'title' ? 'title:hover' : ''; - tdoc._showTitle = newtitle; + const newtitle = !tdoc._layout_showTitle ? 'title' : tdoc._layout_showTitle === 'title' ? 'title:hover' : ''; + tdoc._layout_showTitle = newtitle; } ); } - @computed get autoHeightButton() { + @computed get layout_autoHeightButton() { return this.propertyToggleBtn( 'Auto\xA0Size', - '_autoHeight', + '_layout_autoHeight', on => `Automatical vertical sizing to show all content`, on => 'arrows-alt-v' ); @@ -196,7 +195,7 @@ export class PropertiesButtons extends React.Component<{}, {}> { @computed get gridButton() { return this.propertyToggleBtn( 'Grid', - '_backgroundGridShow', + '_freeform_backgroundGrid', on => `Display background grid in collection`, on => 'border-all' ); @@ -236,7 +235,7 @@ export class PropertiesButtons extends React.Component<{}, {}> { @computed get snapButton() { return this.propertyToggleBtn( 'Snap\xA0Lines', - 'showSnapLines', + 'freeform_snapLines', on => `Display snapping lines when objects are dragged`, on => 'th', undefined, @@ -281,11 +280,11 @@ export class PropertiesButtons extends React.Component<{}, {}> { @undoBatch handlePerspectiveChange = (e: any) => { - this.selectedDoc && (this.selectedDoc._viewType = e.target.value); + this.selectedDoc && (this.selectedDoc._type_collection = e.target.value); SelectionManager.Views() .filter(dv => dv.docView) .map(dv => dv.docView!) - .forEach(docView => (docView.layoutDoc._viewType = e.target.value)); + .forEach(docView => (docView.layoutDoc._type_collection = e.target.value)); }; @undoBatch @@ -373,7 +372,7 @@ export class PropertiesButtons extends React.Component<{}, {}> { const makeLabel = (value: string, label: string) => ( <div className="radio" key={label}> <label> - <input type="radio" value={value} checked={(this.selectedDoc?._viewType ?? 'invalid') === value} onChange={this.handlePerspectiveChange} /> + <input type="radio" value={value} checked={(this.selectedDoc?._type_collection ?? 'invalid') === value} onChange={this.handlePerspectiveChange} /> {label} </label> </div> @@ -395,9 +394,9 @@ export class PropertiesButtons extends React.Component<{}, {}> { const isMap = this.selectedDoc?.type === DocumentType.MAP; const isCollection = this.selectedDoc?.type === DocumentType.COL; //TODO: will likely need to create separate note-taking view type here - const isStacking = this.selectedDoc?._viewType === CollectionViewType.Stacking || this.selectedDoc?._viewType === CollectionViewType.NoteTaking; - const isFreeForm = this.selectedDoc?._viewType === CollectionViewType.Freeform; - const isTree = this.selectedDoc?._viewType === CollectionViewType.Tree; + const isStacking = this.selectedDoc?._type_collection === CollectionViewType.Stacking || this.selectedDoc?._type_collection === CollectionViewType.NoteTaking; + const isFreeForm = this.selectedDoc?._type_collection === CollectionViewType.Freeform; + const isTree = this.selectedDoc?._type_collection === CollectionViewType.Tree; const isTabView = this.selectedTabView; const toggle = (ele: JSX.Element | null, style?: React.CSSProperties) => ( <div className="propertiesButtons-button" style={style}> @@ -412,12 +411,12 @@ export class PropertiesButtons extends React.Component<{}, {}> { {toggle(this.captionButton)} {toggle(this.lockButton)} {toggle(this.onClickButton)} - {toggle(this.fitWidthButton)} + {toggle(this.layout_fitWidthButton)} {toggle(this.freezeThumb)} - {toggle(this.forceActiveButton, { display: !isFreeForm && !isMap ? 'none' : '' })} + {toggle(this.forceActiveButton)} {toggle(this.fitContentButton, { display: !isFreeForm && !isMap ? 'none' : '' })} {toggle(this.isLightboxButton, { display: !isFreeForm && !isMap ? 'none' : '' })} - {toggle(this.autoHeightButton, { display: !isText && !isStacking && !isTree ? 'none' : '' })} + {toggle(this.layout_autoHeightButton, { display: !isText && !isStacking && !isTree ? 'none' : '' })} {toggle(this.maskButton, { display: !isInk ? 'none' : '' })} {toggle(this.hideImageButton, { display: !isImage ? 'none' : '' })} {toggle(this.chromeButton, { display: !isCollection || isNovice ? 'none' : '' })} diff --git a/src/client/views/PropertiesDocBacklinksSelector.tsx b/src/client/views/PropertiesDocBacklinksSelector.tsx index 935d551c6..3e69bcba6 100644 --- a/src/client/views/PropertiesDocBacklinksSelector.tsx +++ b/src/client/views/PropertiesDocBacklinksSelector.tsx @@ -19,13 +19,13 @@ type PropertiesDocBacklinksSelectorProps = { @observer export class PropertiesDocBacklinksSelector extends React.Component<PropertiesDocBacklinksSelectorProps> { getOnClick = (link: Doc) => { - const linkSource = this.props.Document; - const other = LinkManager.getOppositeAnchor(link, linkSource); + const linkAnchor = this.props.Document; + const other = LinkManager.getOppositeAnchor(link, linkAnchor); const otherdoc = !other ? undefined : other.annotationOn && other.type !== DocumentType.RTF ? Cast(other.annotationOn, Doc, null) : other; if (otherdoc) { otherdoc.hidden = false; - this.props.addDocTab(Doc.IsPrototype(otherdoc) ? Doc.MakeDelegate(otherdoc) : otherdoc, OpenWhere.toggleRight); + this.props.addDocTab(Doc.IsDataProto(otherdoc) ? Doc.MakeDelegate(otherdoc) : otherdoc, OpenWhere.toggleRight); } }; diff --git a/src/client/views/PropertiesDocContextSelector.tsx b/src/client/views/PropertiesDocContextSelector.tsx index c14d7ef73..e1279c9a7 100644 --- a/src/client/views/PropertiesDocContextSelector.tsx +++ b/src/client/views/PropertiesDocContextSelector.tsx @@ -23,14 +23,14 @@ export class PropertiesDocContextSelector extends React.Component<PropertiesDocC if (!this.props.DocView) return []; const target = this.props.DocView.props.Document; const targetContext = this.props.DocView.props.docViewPath().lastElement()?.rootDoc; - const aliases = DocListCast(target.aliases); - const containerProtos = aliases.filter(alias => alias.context && alias.context instanceof Doc).reduce((set, alias) => set.add(Cast(alias.context, Doc, null)), new Set<Doc>()); - const containerSets = Array.from(containerProtos.keys()).map(container => DocListCast(container.aliases)); + const embeddings = DocListCast(target.proto_embeddings); + const containerProtos = embeddings.filter(embedding => embedding.embedContainer && embedding.embedContainer instanceof Doc).reduce((set, embedding) => set.add(Cast(embedding.embedContainer, Doc, null)), new Set<Doc>()); + const containerSets = Array.from(containerProtos.keys()).map(container => DocListCast(container.proto_embeddings)); const containers = containerSets.reduce((p, set) => { set.map(s => p.add(s)); return p; }, new Set<Doc>()); - const doclayoutSets = Array.from(containers.keys()).map(dp => DocListCast(dp.aliases)); + const doclayoutSets = Array.from(containers.keys()).map(dp => DocListCast(dp.proto_embeddings)); const doclayouts = Array.from( doclayoutSets .reduce((p, set) => { @@ -48,7 +48,7 @@ export class PropertiesDocContextSelector extends React.Component<PropertiesDocC getOnClick = (col: Doc, target: Doc) => { if (!this.props.DocView) return; - col = Doc.IsPrototype(col) ? Doc.MakeDelegate(col) : col; + col = Doc.IsDataProto(col) ? Doc.MakeDelegate(col) : col; DocFocusOrOpen(Doc.GetProto(this.props.DocView.props.Document), undefined, col); }; diff --git a/src/client/views/PropertiesView.tsx b/src/client/views/PropertiesView.tsx index fb21c2db2..b6699c3bf 100644 --- a/src/client/views/PropertiesView.tsx +++ b/src/client/views/PropertiesView.tsx @@ -6,7 +6,7 @@ import { Checkbox, Tooltip } from '@material-ui/core'; import { action, computed, Lambda, observable } from 'mobx'; import { observer } from 'mobx-react'; import { ColorState, SketchPicker } from 'react-color'; -import { AclAdmin, DataSym, Doc, Field, FieldResult, HeightSym, NumListCast, Opt, StrListCast, WidthSym } from '../../fields/Doc'; +import { AclAdmin, AclSym, DataSym, Doc, Field, FieldResult, HeightSym, HierarchyMapping, NumListCast, Opt, StrListCast, WidthSym } from '../../fields/Doc'; import { Id } from '../../fields/FieldSymbols'; import { InkField } from '../../fields/InkField'; import { List } from '../../fields/List'; @@ -20,7 +20,7 @@ import { LinkManager } from '../util/LinkManager'; import { SelectionManager } from '../util/SelectionManager'; import { SharingManager } from '../util/SharingManager'; import { Transform } from '../util/Transform'; -import { undoBatch, UndoManager } from '../util/UndoManager'; +import { undoable, undoBatch, UndoManager } from '../util/UndoManager'; import { EditableView } from './EditableView'; import { FilterPanel } from './FilterPanel'; import { Colors } from './global/globalEnums'; @@ -68,7 +68,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { return this.selectedDoc?.type === DocumentType.LINK; } @computed get dataDoc() { - return this.selectedDoc?.[DataSym]; + return this.selectedDoc?.[DocData]; } @observable layoutFields: boolean = false; @@ -115,19 +115,18 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { return this.selectedDoc?.type === DocumentType.INK; } - rtfWidth = () => (!this.selectedDoc ? 0 : Math.min(this.selectedDoc?.[WidthSym](), this.props.width - 20)); - rtfHeight = () => (!this.selectedDoc ? 0 : this.rtfWidth() <= this.selectedDoc?.[WidthSym]() ? Math.min(this.selectedDoc?.[HeightSym](), this.MAX_EMBED_HEIGHT) : this.MAX_EMBED_HEIGHT); + rtfWidth = () => (!this.selectedDoc ? 0 : Math.min(this.selectedDoc?.[Width](), this.props.width - 20)); + rtfHeight = () => (!this.selectedDoc ? 0 : this.rtfWidth() <= this.selectedDoc?.[Width]() ? Math.min(this.selectedDoc?.[Height](), this.MAX_EMBED_HEIGHT) : this.MAX_EMBED_HEIGHT); @action docWidth = () => { if (this.selectedDoc) { const layoutDoc = this.selectedDoc; - const aspect = Doc.NativeAspect(layoutDoc, undefined, !layoutDoc._fitWidth); - if (aspect) return Math.min(layoutDoc[WidthSym](), Math.min(this.MAX_EMBED_HEIGHT * aspect, this.props.width - 20)); - return Doc.NativeWidth(layoutDoc) ? Math.min(layoutDoc[WidthSym](), this.props.width - 20) : this.props.width - 20; - } else { - return 0; + const aspect = Doc.NativeAspect(layoutDoc, undefined, !layoutDoc._layout_fitWidth); + if (aspect) return Math.min(layoutDoc[Width](), Math.min(this.MAX_EMBED_HEIGHT * aspect, this.props.width - 20)); + return Doc.NativeWidth(layoutDoc) ? Math.min(layoutDoc[Width](), this.props.width - 20) : this.props.width - 20; } + return 0; }; @action @@ -140,7 +139,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { this.MAX_EMBED_HEIGHT, Doc.NativeAspect(layoutDoc, undefined, true) ? this.docWidth() / Doc.NativeAspect(layoutDoc, undefined, true) - : layoutDoc._fitWidth + : layoutDoc._layout_fitWidth ? !Doc.NativeHeight(this.dataDoc) ? NumCast(this.props.height) : Math.min((this.docWidth() * Doc.NativeHeight(layoutDoc)) / Doc.NativeWidth(layoutDoc) || NumCast(this.props.height)) @@ -198,13 +197,13 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { } @computed get noviceFields() { - const noviceReqFields = ['author', 'creationDate', 'tags', '_curPage']; - return this.editableFields(key => key.indexOf('lastModified') !== -1 || (key[0] === key[0].toUpperCase() && !key.startsWith('acl')), noviceReqFields); + const noviceReqFields = ['author', 'author_date', 'tags', '_layout_curPage']; + return this.editableFields(key => key.indexOf('modificationDate') !== -1 || (key[0] === key[0].toUpperCase() && !key.startsWith('acl')), noviceReqFields); } @undoBatch setKeyValue = (value: string) => { - const docs = SelectionManager.Views().length < 2 && this.selectedDoc ? [this.layoutFields ? Doc.Layout(this.selectedDoc) : this.dataDoc] : SelectionManager.Views().map(dv => (this.layoutFields ? dv.layoutDoc : dv.dataDoc)); + const docs = SelectionManager.Views().length < 2 && this.selectedDoc ? [this.layoutFields ? Doc.Layout(this.selectedDoc) : this.dataDoc!] : SelectionManager.Views().map(dv => (this.layoutFields ? dv.layoutDoc : dv.dataDoc)); docs.forEach(doc => { if (value.indexOf(':') !== -1) { const newVal = value[0].toUpperCase() + value.substring(1, value.length); @@ -219,7 +218,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { const tags = StrListCast(doc.tags); if (!tags.includes(value)) { tags.push(value); - doc[DataSym].tags = tags.length ? new List<string>(tags) : undefined; + doc[DocData].tags = tags.length ? new List<string>(tags) : undefined; } return true; } @@ -275,8 +274,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { PanelHeight={panelHeight} focus={emptyFunction} ScreenToLocalTransform={this.getTransform} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} addDocument={returnFalse} moveDocument={undefined} @@ -300,7 +299,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { */ @undoBatch changePermissions = (e: any, user: string) => { - const docs = (SelectionManager.Views().length < 2 ? [this.selectedDoc] : SelectionManager.Views().map(dv => dv.props.Document)).filter(doc => doc).map(doc => (this.layoutDocAcls ? doc : DocCast(doc)[DataSym])); + const docs = (SelectionManager.Views().length < 2 ? [this.selectedDoc] : SelectionManager.Views().map(dv => dv.props.Document)).filter(doc => doc).map(doc => (this.layoutDocAcls ? doc! : DocCast(doc)[DocData])); SharingManager.Instance.shareFromPropertiesSidebar(user, e.currentTarget.value as SharingPermissions, docs); }; @@ -436,7 +435,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { return null; } // all selected docs - const docs = SelectionManager.Views().length < 2 ? [this.layoutDocAcls ? docToUse : docToUse?.[DataSym]] : SelectionManager.Views().map(docView => (this.layoutDocAcls ? docView.props.Document : docView.props.Document[DataSym])); + const docs = + SelectionManager.Views().length < 2 ? [this.layoutDocAcls ? this.selectedDoc : this.selectedDoc?.[DataSym]] : SelectionManager.Views().map(docView => (this.layoutDocAcls ? docView.props.Document : docView.props.Document[DataSym])); + const target = docs[0]; // const effectiveAcls = GetEffectiveAcl(target); @@ -445,17 +446,18 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { const showAdmin = curUserAcl == 'Admin' || curUserAcl == 'Owner' // users in common between all docs - // const commonKeys: string[] = intersection(...docs.map(doc => doc?.[AclSym] && Object.keys(doc[AclSym]).filter(key => key !== 'acl-Me'))); - // const ownerSame = Doc.CurrentUserEmail !== target.author && docs.filter(doc => doc).every(doc => doc.author === docs[0].author); + const commonKeys: string[] = intersection(...docs.map(doc => doc?.[AclSym] && Object.keys(doc[AclSym]).filter(key => key !== 'acl-Me'))); + const tableEntries = []; const usersAdded: string[] = []; // all shared users being added - organized by denormalized email - SharingManager.Instance.users.forEach(eachUser => { - var userOnDashboard = true; - var permission = StrCast(target[`acl-${normalizeEmail(eachUser.user.email)}`]); - if (Doc.ActiveDashboard) { - if (Doc.ActiveDashboard['acl-' + normalizeEmail(eachUser.user.email)] == '' || Doc.ActiveDashboard['acl-' + normalizeEmail(eachUser.user.email)] == undefined) { - userOnDashboard = false; + // DocCastAsync(Doc.UserDoc().sidebarUsersDisplayed).then(sidebarUsersDisplayed => { + if (commonKeys.length) { + for (const key of commonKeys) { + const name = denormalizeEmail(key.substring(4)); + const uniform = docs.every(doc => doc?.[AclSym]?.[key] === docs[0]?.[AclSym]?.[key]); + if (name !== Doc.CurrentUserEmail && name !== target.author && name !== 'Public' && name !== 'Override' /* && sidebarUsersDisplayed![name] !== false*/) { + tableEntries.push(this.sharingItem(name, showAdmin, uniform ? HierarchyMapping.get(target[AclSym][key])!.name : '-multiple-')); } } if (userOnDashboard && !usersAdded.includes(eachUser.user.email) && eachUser.user.email != 'Public' && eachUser.user.email != target.author) { @@ -464,11 +466,19 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { } }); - usersAdded.sort(this.sortUsers); - usersAdded.map(userEmail => { - const permission = StrCast(target[`acl-${normalizeEmail(userEmail)}`]); - tableEntries.unshift(this.sharingItem(userEmail, showAdmin, permission, false)); // adds each user - }); + const ownerSame = Doc.CurrentUserEmail !== target.author && docs.filter(doc => doc).every(doc => doc.author === docs[0].author); + // shifts the current user, owner, public to the top of the doc. + // tableEntries.unshift(this.sharingItem("Override", showAdmin, docs.filter(doc => doc).every(doc => doc["acl-Override"] === docs[0]["acl-Override"]) ? (AclMap.get(target[AclSym]?.["acl-Override"]) || "None") : "-multiple-")); + if (ownerSame) tableEntries.unshift(this.sharingItem(StrCast(target.author), showAdmin, 'Owner')); + tableEntries.unshift(this.sharingItem('Public', showAdmin, docs.filter(doc => doc).every(doc => doc['acl-Public'] === target['acl-Public']) ? target['acl-Public'] || SharingPermissions.None : '-multiple-')); + tableEntries.unshift( + this.sharingItem( + 'Me', + showAdmin, + docs.filter(doc => doc).every(doc => doc.author === Doc.CurrentUserEmail) ? 'Owner' : effectiveAcls.every(acl => acl === effectiveAcls[0]) ? HierarchyMapping.get(effectiveAcls[0])!.name : '-multiple-', + !ownerSame + ) + ); // add current user var userEmail = Doc.CurrentUserEmail; @@ -500,9 +510,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { } @action - toggleCheckbox = () => { - this.layoutFields = !this.layoutFields; - }; + toggleCheckbox = () => (this.layoutFields = !this.layoutFields); @computed get editableTitle() { const titles = new Set<string>(); @@ -600,17 +608,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { marginLeft: title === '∠:' ? '39px' : '', }}> <div className="inputBox-title"> {title} </div> - <input - className="inputBox-input" - type="text" - value={value} - onChange={e => { - setter(e.target.value); - }} - onKeyPress={e => { - e.stopPropagation(); - }} - /> + <input className="inputBox-input" type="text" value={value} onChange={e => setter(e.target.value)} onKeyPress={e => e.stopPropagation()} /> <div className="inputBox-button"> <div className="inputBox-button-up" key="up2" onPointerDown={undoBatch(action(() => this.upDownButtons('up', key)))}> <FontAwesomeIcon icon="caret-up" color="white" size="sm" /> @@ -634,55 +632,50 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @action upDownButtons = (dirs: string, field: string) => { + const selDoc = this.selectedDoc; + if (!selDoc) return; + //prettier-ignore switch (field) { - case 'Xps': - this.selectedDoc && (this.selectedDoc.x = NumCast(this.selectedDoc?.x) + (dirs === 'up' ? 10 : -10)); - break; - case 'Yps': - this.selectedDoc && (this.selectedDoc.y = NumCast(this.selectedDoc?.y) + (dirs === 'up' ? 10 : -10)); - break; - case 'stk': - this.selectedDoc && (this.selectedDoc.strokeWidth = NumCast(this.selectedDoc?.strokeWidth) + (dirs === 'up' ? 0.1 : -0.1)); - break; + case 'Xps': selDoc.x = NumCast(this.selectedDoc?.x) + (dirs === 'up' ? 10 : -10); break; + case 'Yps': selDoc.y = NumCast(this.selectedDoc?.y) + (dirs === 'up' ? 10 : -10); break; + case 'stk': selDoc.stroke_width = NumCast(this.selectedDoc?.stroke_width) + (dirs === 'up' ? 0.1 : -0.1); break; case 'wid': - const oldWidth = NumCast(this.selectedDoc?._width); - const oldHeight = NumCast(this.selectedDoc?._height); - const oldX = NumCast(this.selectedDoc?.x); - const oldY = NumCast(this.selectedDoc?.y); - this.selectedDoc && (this.selectedDoc._width = oldWidth + (dirs === 'up' ? 10 : -10)); - const doc = this.selectedDoc; - if (doc?.type === DocumentType.INK && doc.x && doc.y && doc._height && doc._width) { - const ink = Cast(doc.data, InkField)?.inkData; + const oldWidth = NumCast(selDoc._width); + const oldHeight = NumCast(selDoc._height); + const oldX = NumCast(selDoc.x); + const oldY = NumCast(selDoc.y); + selDoc._width = oldWidth + (dirs === 'up' ? 10 : -10); + if (selDoc.type === DocumentType.INK && selDoc.x && selDoc.y && selDoc._height && selDoc._width) { + const ink = Cast(selDoc.data, InkField)?.inkData; if (ink) { const newPoints: { X: number; Y: number }[] = []; for (var j = 0; j < ink.length; j++) { // (new x — oldx) + (oldxpoint * newWidt)/oldWidth - const newX = NumCast(doc.x) - oldX + (ink[j].X * NumCast(doc._width)) / oldWidth; - const newY = NumCast(doc.y) - oldY + (ink[j].Y * NumCast(doc._height)) / oldHeight; + const newX = NumCast(selDoc.x) - oldX + (ink[j].X * NumCast(selDoc._width)) / oldWidth; + const newY = NumCast(selDoc.y) - oldY + (ink[j].Y * NumCast(selDoc._height)) / oldHeight; newPoints.push({ X: newX, Y: newY }); } - doc.data = new InkField(newPoints); + selDoc.data = new InkField(newPoints); } } break; case 'hgt': - const oWidth = NumCast(this.selectedDoc?._width); - const oHeight = NumCast(this.selectedDoc?._height); - const oX = NumCast(this.selectedDoc?.x); - const oY = NumCast(this.selectedDoc?.y); - this.selectedDoc && (this.selectedDoc._height = oHeight + (dirs === 'up' ? 10 : -10)); - const docu = this.selectedDoc; - if (docu?.type === DocumentType.INK && docu.x && docu.y && docu._height && docu._width) { - const ink = Cast(docu.data, InkField)?.inkData; + const oWidth = NumCast(selDoc._width); + const oHeight = NumCast(selDoc._height); + const oX = NumCast(selDoc.x); + const oY = NumCast(selDoc.y); + selDoc._height = oHeight + (dirs === 'up' ? 10 : -10); + if (selDoc.type === DocumentType.INK && selDoc.x && selDoc.y && selDoc._height && selDoc._width) { + const ink = Cast(selDoc.data, InkField)?.inkData; if (ink) { const newPoints: { X: number; Y: number }[] = []; for (var j = 0; j < ink.length; j++) { // (new x — oldx) + (oldxpoint * newWidt)/oldWidth - const newX = NumCast(docu.x) - oX + (ink[j].X * NumCast(docu._width)) / oWidth; - const newY = NumCast(docu.y) - oY + (ink[j].Y * NumCast(docu._height)) / oHeight; + const newX = NumCast(selDoc.x) - oX + (ink[j].X * NumCast(selDoc._width)) / oWidth; + const newY = NumCast(selDoc.y) - oY + (ink[j].Y * NumCast(selDoc._height)) / oHeight; newPoints.push({ X: newX, Y: newY }); } - docu.data = new InkField(newPoints); + selDoc.data = new InkField(newPoints); } } break; @@ -722,21 +715,11 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { return this.inputBoxDuo( 'hgt', this.shapeHgt, - (val: string) => { - if (!isNaN(Number(val))) { - this.shapeHgt = val; - } - return true; - }, + undoable((val: string) => !isNaN(Number(val)) && (this.shapeHgt = val), 'set height'), 'H:', 'wid', this.shapeWid, - (val: string) => { - if (!isNaN(Number(val))) { - this.shapeWid = val; - } - return true; - }, + undoable((val: string) => !isNaN(Number(val)) && (this.shapeWid = val), 'set width'), 'W:' ); } @@ -744,21 +727,11 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { return this.inputBoxDuo( 'Xps', this.shapeXps, - (val: string) => { - if (val !== '0' && !isNaN(Number(val))) { - this.shapeXps = val; - } - return true; - }, + undoable((val: string) => val !== '0' && !isNaN(Number(val)) && (this.shapeXps = val), 'set x coord'), 'X:', 'Yps', this.shapeYps, - (val: string) => { - if (val !== '0' && !isNaN(Number(val))) { - this.shapeYps = val; - } - return true; - }, + undoable((val: string) => val !== '0' && !isNaN(Number(val)) && (this.shapeYps = val), 'set y coord'), 'Y:' ); } @@ -766,36 +739,24 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { @observable private _fillBtn = false; @observable private _lineBtn = false; - private _lastFill = '#D0021B'; - private _lastLine = '#D0021B'; private _lastDash: any = '2'; @computed get colorFil() { - const ccol = this.getField('fillColor') || ''; - ccol && (this._lastFill = ccol); - return ccol; + return StrCast(this.selectedDoc?.fillColor); } @computed get colorStk() { - const ccol = this.getField('color') || ''; - ccol && (this._lastLine = ccol); - return ccol; + return StrCast(this.selectedDoc?.color); } set colorFil(value) { - value && (this._lastFill = value); this.selectedDoc && (this.selectedDoc.fillColor = value ? value : undefined); } set colorStk(value) { - value && (this._lastLine = value); this.selectedDoc && (this.selectedDoc.color = value ? value : undefined); } - colorButton(value: string, type: string, setter: () => {}) { - // return <div className="properties-flyout" onPointerEnter={e => this.changeScrolling(false)} - // onPointerLeave={e => this.changeScrolling(true)}> - // <Flyout anchorPoint={anchorPoints.LEFT_TOP} - // content={type === "fill" ? this.fillPicker : this.linePicker}> + colorButton(value: string, type: string, setter: () => void) { return ( - <div className="color-button" key="color" onPointerDown={undoBatch(action(e => setter()))}> + <div className="color-button" key="color" onPointerDown={action(e => setter())}> <div className="color-button-preview" style={{ @@ -808,31 +769,17 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { {value === '' || value === 'transparent' ? <p style={{ fontSize: 25, color: 'red', marginTop: -14 }}>☒</p> : ''} </div> ); - // </Flyout> - // </div>; } - @undoBatch - @action - switchStk = (color: ColorState) => { - const val = String(color.hex); - this.colorStk = val; - return true; - }; - @undoBatch - @action - switchFil = (color: ColorState) => { - const val = String(color.hex); - this.colorFil = val; - return true; - }; - - colorPicker(setter: (color: string) => {}, type: string) { + colorPicker(color: string, setter: (color: string) => void) { return ( <SketchPicker - onChange={type === 'stk' ? this.switchStk : this.switchFil} + onChange={undoable( + action((color: ColorState) => setter(color.hex)), + 'set stroke color property' + )} presetColors={['#D0021B', '#F5A623', '#F8E71C', '#8B572A', '#7ED321', '#417505', '#9013FE', '#4A90E2', '#50E3C2', '#B8E986', '#000000', '#4A4A4A', '#9B9B9B', '#FFFFFF', '#f1efeb', 'transparent']} - color={type === 'stk' ? this.colorStk : this.colorFil} + color={color} /> ); } @@ -841,22 +788,20 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { return this.colorButton(this.colorFil, 'fill', () => { this._fillBtn = !this._fillBtn; this._lineBtn = false; - return true; }); } @computed get lineButton() { return this.colorButton(this.colorStk, 'line', () => { this._lineBtn = !this._lineBtn; this._fillBtn = false; - return true; }); } @computed get fillPicker() { - return this.colorPicker((color: string) => (this.colorFil = color), 'fil'); + return this.colorPicker(this.colorFil, (color: string) => (this.colorFil = color)); } @computed get linePicker() { - return this.colorPicker((color: string) => (this.colorStk = color), 'stk'); + return this.colorPicker(this.colorStk, (color: string) => (this.colorStk = color)); } @computed get strokeAndFill() { @@ -878,49 +823,36 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { ); } - @computed get solidStk() { - return this.selectedDoc?.color && (!this.selectedDoc?.strokeDash || this.selectedDoc?.strokeDash === '0') ? true : false; - } @computed get dashdStk() { - return this.selectedDoc?.strokeDash || ''; - } - @computed get unStrokd() { - return this.selectedDoc?.color ? true : false; + return this.selectedDoc?.stroke_dash || ''; } @computed get widthStk() { - return this.getField('strokeWidth') || '1'; + return this.getField('stroke_width') || '1'; } @computed get markScal() { - return Number(this.getField('strokeMakerScale') || '1'); + return Number(this.getField('stroke_markerScale') || '1'); } @computed get markHead() { - return this.getField('strokeStartMarker') || ''; + return this.getField('stroke_startMarker') || ''; } @computed get markTail() { - return this.getField('strokeEndMarker') || ''; - } - set solidStk(value) { - this.dashdStk = ''; - this.unStrokd = !value; + return this.getField('stroke_endMarker') || ''; } set dashdStk(value) { - value && (this._lastDash = value) && (this.unStrokd = false); - this.selectedDoc && (this.selectedDoc.strokeDash = value ? this._lastDash : undefined); + value && (this._lastDash = value); + this.selectedDoc && (this.selectedDoc.stroke_dash = value ? this._lastDash : undefined); } set markScal(value) { - this.selectedDoc && (this.selectedDoc.strokeMarkerScale = Number(value)); + this.selectedDoc && (this.selectedDoc.stroke_markerScale = Number(value)); } set widthStk(value) { - this.selectedDoc && (this.selectedDoc.strokeWidth = Number(value)); - } - set unStrokd(value) { - this.colorStk = value ? '' : this._lastLine; + this.selectedDoc && (this.selectedDoc.stroke_width = Number(value)); } set markHead(value) { - this.selectedDoc && (this.selectedDoc.strokeStartMarker = value); + this.selectedDoc && (this.selectedDoc.stroke_startMarker = value); } set markTail(value) { - this.selectedDoc && (this.selectedDoc.strokeEndMarker = value); + this.selectedDoc && (this.selectedDoc.stroke_endMarker = value); } @computed get stkInput() { @@ -1086,26 +1018,26 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { } /** - * Updates this.filterDoc's currentFilter and saves the docFilters on the currentFilter + * Updates this.filterDoc's currentFilter and saves the childFilters on the currentFilter */ updateFilterDoc = (doc: Doc) => { if (this.selectedDoc) { if (doc === this.selectedDoc.currentFilter) return; // causes problems if you try to reapply the same doc - const savedDocFilters = doc._docFiltersList; - const currentDocFilters = this.selectedDoc._docFilters; - this.selectedDoc._docFilters = new List<string>(); - (this.selectedDoc.currentFilter as Doc)._docFiltersList = currentDocFilters; + const savedDocFilters = doc._childFiltersList; + const currentDocFilters = this.selectedDoc._childFilters; + this.selectedDoc._childFilters = new List<string>(); + (this.selectedDoc.currentFilter as Doc)._childFiltersList = currentDocFilters; this.selectedDoc.currentFilter = doc; - doc._docFiltersList = new List<string>(); - this.selectedDoc._docFilters = savedDocFilters; + doc._childFiltersList = new List<string>(); + this.selectedDoc._childFilters = savedDocFilters; - const savedDocRangeFilters = doc._docRangeFiltersList; - const currentDocRangeFilters = this.selectedDoc._docRangeFilters; - this.selectedDoc._docRangeFilters = new List<string>(); - (this.selectedDoc.currentFilter as Doc)._docRangeFiltersList = currentDocRangeFilters; + const savedDocRangeFilters = doc._childFiltersByRangesList; + const currentDocRangeFilters = this.selectedDoc._childFiltersByRanges; + this.selectedDoc._childFiltersByRanges = new List<string>(); + (this.selectedDoc.currentFilter as Doc)._childFiltersByRangesList = currentDocRangeFilters; this.selectedDoc.currentFilter = doc; - doc._docRangeFiltersList = new List<string>(); - this.selectedDoc._docRangeFilters = savedDocRangeFilters; + doc._childFiltersByRangesList = new List<string>(); + this.selectedDoc._childFiltersByRanges = savedDocRangeFilters; } }; @@ -1218,50 +1150,51 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { } @computed get description() { - return Field.toString(LinkManager.currentLink?.description as any as Field); + return Field.toString(LinkManager.currentLink?.link_description as any as Field); } @computed get relationship() { - return StrCast(LinkManager.currentLink?.linkRelationship); + return StrCast(LinkManager.currentLink?.link_relationship); } @observable private relationshipButtonColor: string = ''; // @action - // handleDescriptionChange = (e: React.ChangeEvent<HTMLInputElement>) => { this.description = e.target.value; } - // handleRelationshipChange = (e: React.ChangeEvent<HTMLInputElement>) => { this.relationship = e.target.value; } + // handleDescriptionChange = (e: React.ChangeEvent<HTMLInputElement>) => { this.link_description = e.target.value; } + // handleRelationshipChange = (e: React.ChangeEvent<HTMLInputElement>) => { this.link_relationship = e.target.value; } - @undoBatch - handleDescriptionChange = action((value: string) => { - if (LinkManager.currentLink && this.selectedDoc) { - this.setDescripValue(value); - return true; - } - }); - - @undoBatch - handleLinkRelationshipChange = action((value: string) => { - if (LinkManager.currentLink && this.selectedDoc) { - this.setLinkRelationshipValue(value); - return true; - } - }); + handleDescriptionChange = undoable( + action((value: string) => { + if (LinkManager.currentLink && this.selectedDoc) { + this.setDescripValue(value); + } + }), + 'change link description' + ); + + handlelinkRelationshipChange = undoable( + action((value: string) => { + if (LinkManager.currentLink && this.selectedDoc) { + this.setlinkRelationshipValue(value); + } + }), + 'change link relationship' + ); @undoBatch setDescripValue = action((value: string) => { if (LinkManager.currentLink) { - Doc.GetProto(LinkManager.currentLink).description = value; - return true; + Doc.GetProto(LinkManager.currentLink).link_description = value; } }); @undoBatch - setLinkRelationshipValue = action((value: string) => { + setlinkRelationshipValue = action((value: string) => { if (LinkManager.currentLink) { - const prevRelationship = LinkManager.currentLink.linkRelationship as string; - LinkManager.currentLink.linkRelationship = value; - Doc.GetProto(LinkManager.currentLink).linkRelationship = value; - const linkRelationshipList = StrListCast(Doc.UserDoc().linkRelationshipList); - const linkRelationshipSizes = NumListCast(Doc.UserDoc().linkRelationshipSizes); - const linkColorList = StrListCast(Doc.UserDoc().linkColorList); + const prevRelationship = StrCast(LinkManager.currentLink.link_relationship); + LinkManager.currentLink.link_relationship = value; + Doc.GetProto(LinkManager.currentLink).link_relationship = value; + const linkRelationshipList = StrListCast(Doc.UserDoc().link_relationshipList); + const linkRelationshipSizes = NumListCast(Doc.UserDoc().link_relationshipSizes); + const linkColorList = StrListCast(Doc.UserDoc().link_ColorList); // if the relationship does not exist in the list, add it and a corresponding unique randomly generated color if (!linkRelationshipList?.includes(value)) { @@ -1295,8 +1228,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { } }); - @undoBatch - changeFollowBehavior = action((follow: Opt<string>) => this.sourceAnchor && (this.sourceAnchor.followLinkLocation = follow)); + changeFollowBehavior = undoable((loc: Opt<string>) => this.sourceAnchor && (this.sourceAnchor.followLinkLocation = loc), 'change follow behavior'); @undoBatch changeAnimationBehavior = action((behavior: string) => this.sourceAnchor && (this.sourceAnchor.followLinkAnimEffect = behavior)); @@ -1331,13 +1263,13 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { }; onSelectOutRelationship = () => { - this.setLinkRelationshipValue(this.relationship); + this.setlinkRelationshipValue(this.relationship); document.getElementById('link_relationship_input')?.blur(); }; onRelationshipKey = (e: React.KeyboardEvent<HTMLInputElement>) => { if (e.key === 'Enter') { - this.setLinkRelationshipValue(this.relationship); + this.setlinkRelationshipValue(this.relationship); document.getElementById('link_relationship_input')?.blur(); } }; @@ -1350,7 +1282,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { const ldoc = LinkManager.currentLink; const lanch = this.selectedDocumentView?.anchorViewDoc ?? LinkManager.currentLinkAnchor; if (ldoc && lanch) return LinkManager.getOppositeAnchor(ldoc, lanch) ?? lanch; - return ldoc ? DocCast(ldoc.anchor2) : ldoc; + return ldoc ? DocCast(ldoc.link_anchor_2) : ldoc; } @computed get sourceAnchor() { @@ -1381,10 +1313,10 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <input autoComplete={'off'} id="link_relationship_input" - value={StrCast(this.selectedDoc?.linkRelationship)} + value={StrCast(LinkManager.currentLink?.link_relationship)} onKeyDown={this.onRelationshipKey} onBlur={this.onSelectOutRelationship} - onChange={e => this.handleLinkRelationshipChange(e.currentTarget.value)} + onChange={e => this.handlelinkRelationshipChange(e.currentTarget.value)} className="text" type="text" /> @@ -1398,7 +1330,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { autoComplete="off" style={{ textAlign: 'left' }} id="link_description_input" - value={StrCast(this.selectedDoc?.description)} + value={StrCast(LinkManager.currentLink?.link_description)} onKeyDown={this.onDescriptionKey} onBlur={this.onSelectOutDesc} onChange={e => this.handleDescriptionChange(e.currentTarget.value)} @@ -1473,8 +1405,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <div className="propertiesView-input inline"> <p>Show link</p> <button - style={{ background: !LinkManager.currentLink?.linkDisplay ? '' : '#4476f7', borderRadius: 3 }} - onPointerDown={e => this.toggleLinkProp(e, 'linkDisplay')} + style={{ background: !LinkManager.currentLink?.link_displayLine ? '' : '#4476f7', borderRadius: 3 }} + onPointerDown={e => this.toggleLinkProp(e, 'link_displayLine')} onClick={e => e.stopPropagation()} className="propertiesButton"> <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" /> @@ -1483,8 +1415,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <div className="propertiesView-input inline" style={{ marginLeft: 10 }}> <p>Auto-move anchors</p> <button - style={{ background: !LinkManager.currentLink?.linkAutoMove ? '' : '#4476f7', borderRadius: 3 }} - onPointerDown={e => this.toggleLinkProp(e, 'linkAutoMove')} + style={{ background: !LinkManager.currentLink?.link_autoMoveAnchors ? '' : '#4476f7', borderRadius: 3 }} + onPointerDown={e => this.toggleLinkProp(e, 'link_autoMoveAnchors')} onClick={e => e.stopPropagation()} className="propertiesButton"> <FontAwesomeIcon className="fa-icon" icon={faAnchor as IconLookup} size="lg" /> @@ -1493,8 +1425,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <div className="propertiesView-input inline" style={{ marginLeft: 10 }}> <p>Display arrow</p> <button - style={{ background: !LinkManager.currentLink?.linkDisplayArrow ? '' : '#4476f7', borderRadius: 3 }} - onPointerDown={e => this.toggleLinkProp(e, 'linkDisplayArrow')} + style={{ background: !LinkManager.currentLink?.link_displayArrow ? '' : '#4476f7', borderRadius: 3 }} + onPointerDown={e => this.toggleLinkProp(e, 'link_displayArrow')} onClick={e => e.stopPropagation()} className="propertiesButton"> <FontAwesomeIcon className="fa-icon" icon={faArrowRight as IconLookup} size="lg" /> @@ -1524,7 +1456,9 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <select style={{ width: '100%', gridColumn: 2 }} onChange={e => this.changeAnimationBehavior(e.currentTarget.value)} value={StrCast(this.sourceAnchor?.followLinkAnimEffect, 'default')}> <option value="default">Default</option> {[PresEffect.None, PresEffect.Zoom, PresEffect.Lightspeed, PresEffect.Fade, PresEffect.Flip, PresEffect.Rotate, PresEffect.Bounce, PresEffect.Roll].map(effect => ( - <option value={effect.toString()}>{effect.toString()}</option> + <option key={effect.toString()} value={effect.toString()}> + {effect.toString()} + </option> ))} </select> <div className="effectDirection" style={{ marginLeft: '10px', display: 'grid', width: 40, height: 36, gridColumn: 3, gridTemplateRows: '12px 12px 12px' }}> @@ -1634,7 +1568,7 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <div className="propertiesView-input inline" style={{ display: 'grid', gridTemplateColumns: '78px calc(100% - 108px) 50px' }}> <p>Zoom %</p> <div className="ribbon-property" style={{ display: !targZoom ? 'none' : 'inline-flex' }}> - <input className="presBox-input" style={{ width: '100%' }} type="number" value={zoom} /> + <input className="presBox-input" style={{ width: '100%' }} readOnly={true} type="number" value={zoom} /> <div className="ribbon-propertyUpDown" style={{ display: 'flex', flexDirection: 'column' }}> <div className="ribbon-propertyUpDownItem" onClick={undoBatch(() => this.setZoom(String(zoom), 0.1))}> <FontAwesomeIcon icon={'caret-up'} /> @@ -1686,8 +1620,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { </div> ); } - if (this.isPres) { - const selectedItem: boolean = PresBox.Instance?.selectedArray.size > 0; + if (this.isPres && PresBox.Instance) { + const selectedItem: boolean = PresBox.Instance.selectedArray.size > 0; const type = [DocumentType.AUDIO, DocumentType.VID].includes(DocCast(PresBox.Instance.activeItem?.annotationOn)?.type as any as DocumentType) ? (DocCast(PresBox.Instance.activeItem?.annotationOn)?.type as any as DocumentType) : PresBox.targetRenderedDoc(PresBox.Instance.activeItem)?.type; @@ -1699,8 +1633,8 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { <div className="propertiesView-name" style={{ borderBottom: 0 }}> {this.editableTitle} <div className="propertiesView-presSelected"> - <div className="propertiesView-selectedCount">{PresBox.Instance?.selectedArray.size} selected</div> - <div className="propertiesView-selectedList">{PresBox.Instance?.listOfSelected}</div> + <div className="propertiesView-selectedCount">{PresBox.Instance.selectedArray.size} selected</div> + <div className="propertiesView-selectedList">{PresBox.Instance.listOfSelected}</div> </div> </div> {!selectedItem ? null : ( @@ -1750,19 +1684,6 @@ export class PropertiesView extends React.Component<PropertiesViewProps> { {this.openSlideOptions ? <div className="propertiesView-presTrails-content">{PresBox.Instance.mediaOptionsDropdown}</div> : null} </div> )} - {/* <div className="propertiesView-presTrails"> - <div className="propertiesView-presTrails-title" - onPointerDown={action(() => { this.openAddSlide = !this.openAddSlide; })} - style={{ backgroundColor: this.openAddSlide ? "black" : "" }}> - <FontAwesomeIcon icon={"plus"} /> Add new slide - <div className="propertiesView-presTrails-title-icon"> - <FontAwesomeIcon icon={this.openAddSlide ? "caret-down" : "caret-right"} size="lg" color="white" /> - </div> - </div> - {this.openAddSlide ? <div className="propertiesView-presTrails-content"> - {PresBox.Instance.newDocumentDropdown} - </div> : null} - </div> */} </div> ); } diff --git a/src/client/views/ScriptingRepl.scss b/src/client/views/ScriptingRepl.scss index 778e9c445..adc82238e 100644 --- a/src/client/views/ScriptingRepl.scss +++ b/src/client/views/ScriptingRepl.scss @@ -31,6 +31,9 @@ border-radius: 25%; padding: 2px; } +.documentIcon-outerDiv:hover { + opacity: 0.3; +} .scriptingObject-icon { padding: 3px; @@ -48,4 +51,4 @@ .scriptingObject-leaf { margin-left: 15px; -}
\ No newline at end of file +} diff --git a/src/client/views/ScriptingRepl.tsx b/src/client/views/ScriptingRepl.tsx index 4fecfa4d9..5dfe10def 100644 --- a/src/client/views/ScriptingRepl.tsx +++ b/src/client/views/ScriptingRepl.tsx @@ -10,34 +10,49 @@ import { OverlayView } from './OverlayView'; import './ScriptingRepl.scss'; @observer -export class ScriptingObjectDisplay extends React.Component<{ scrollToBottom: () => void, value: { [key: string]: any }, name?: string }> { +export class ScriptingObjectDisplay extends React.Component<{ scrollToBottom: () => void; value: { [key: string]: any }; name?: string }> { @observable collapsed = true; @action toggle = () => { this.collapsed = !this.collapsed; this.props.scrollToBottom(); - } + }; render() { const val = this.props.value; const proto = Object.getPrototypeOf(val); const name = (proto && proto.constructor && proto.constructor.name) || String(val); - const title = this.props.name ? <><b>{this.props.name} : </b>{name}</> : name; + const title = this.props.name ? ( + <> + <b>{this.props.name} : </b> + {name} + </> + ) : ( + name + ); if (this.collapsed) { return ( <div className="scriptingObject-collapsed"> - <span onClick={this.toggle} className="scriptingObject-icon scriptingObject-iconCollapsed"><FontAwesomeIcon icon="caret-right" size="sm" /></span>{title} (+{Object.keys(val).length}) + <span onClick={this.toggle} className="scriptingObject-icon scriptingObject-iconCollapsed"> + <FontAwesomeIcon icon="caret-right" size="sm" /> + </span> + {title} (+{Object.keys(val).length}) </div> ); } else { return ( <div className="scriptingObject-open"> <div> - <span onClick={this.toggle} className="scriptingObject-icon"><FontAwesomeIcon icon="caret-down" size="sm" /></span>{title} + <span onClick={this.toggle} className="scriptingObject-icon"> + <FontAwesomeIcon icon="caret-down" size="sm" /> + </span> + {title} </div> <div className="scriptingObject-fields"> - {Object.keys(val).map(key => <ScriptingValueDisplay {...this.props} name={key} />)} + {Object.keys(val).map(key => ( + <ScriptingValueDisplay {...this.props} name={key} /> + ))} </div> </div> ); @@ -46,18 +61,32 @@ export class ScriptingObjectDisplay extends React.Component<{ scrollToBottom: () } @observer -export class ScriptingValueDisplay extends React.Component<{ scrollToBottom: () => void, value: any, name?: string }> { +export class ScriptingValueDisplay extends React.Component<{ scrollToBottom: () => void; value: any; name?: string }> { render() { const val = this.props.name ? this.props.value[this.props.name] : this.props.value; - if (typeof val === "object") { + if (typeof val === 'object') { return <ScriptingObjectDisplay scrollToBottom={this.props.scrollToBottom} value={val} name={this.props.name} />; - } else if (typeof val === "function") { - const name = "[Function]"; - const title = this.props.name ? <><b>{this.props.name} : </b>{name}</> : name; + } else if (typeof val === 'function') { + const name = '[Function]'; + const title = this.props.name ? ( + <> + <b>{this.props.name} : </b> + {name} + </> + ) : ( + name + ); return <div className="scriptingObject-leaf">{title}</div>; } else { const name = String(val); - const title = this.props.name ? <><b>{this.props.name} : </b>{name}</> : name; + const title = this.props.name ? ( + <> + <b>{this.props.name} : </b> + {name} + </> + ) : ( + name + ); return <div className="scriptingObject-leaf">{title}</div>; } } @@ -65,11 +94,11 @@ export class ScriptingValueDisplay extends React.Component<{ scrollToBottom: () @observer export class ScriptingRepl extends React.Component { - @observable private commands: { command: string, result: any }[] = []; + @observable private commands: { command: string; result: any }[] = []; private commandsHistory: string[] = []; - @observable private commandString: string = ""; - private commandBuffer: string = ""; + @observable private commandString: string = ''; + private commandBuffer: string = ''; @observable private historyIndex: number = -1; @@ -82,7 +111,7 @@ export class ScriptingRepl extends React.Component { transformer: context => { const knownVars: { [name: string]: number } = {}; const usedDocuments: number[] = []; - ScriptingGlobals.getGlobals().forEach((global: any) => knownVars[global] = 1); + ScriptingGlobals.getGlobals().forEach((global: any) => (knownVars[global] = 1)); return root => { function visit(node: ts.Node) { let skip = false; @@ -105,7 +134,7 @@ export class ScriptingRepl extends React.Component { const m = parseInt(match[1]); usedDocuments.push(m); } else { - return ts.createPropertyAccess(ts.createIdentifier("args"), node); + return ts.createPropertyAccess(ts.createIdentifier('args'), node); } } } @@ -114,20 +143,20 @@ export class ScriptingRepl extends React.Component { } return ts.visitNode(root, visit); }; - } + }, }; - } + }; @action onKeyDown = (e: React.KeyboardEvent) => { let stopProp = true; switch (e.key) { - case "Enter": { + case 'Enter': { e.stopPropagation(); const docGlobals: { [name: string]: any } = {}; - Array.from(DocumentManager.Instance.DocumentViews).forEach((dv, i) => docGlobals[`d${i}`] = dv.props.Document); + DocumentManager.Instance.DocumentViews.forEach((dv, i) => (docGlobals[`d${i}`] = dv.props.Document)); const globals = ScriptingGlobals.makeMutableGlobalsCopy(docGlobals); - const script = CompileScript(this.commandString, { typecheck: false, addReturn: true, editable: true, params: { args: "any" }, transformer: this.getTransformer(), globals }); + const script = CompileScript(this.commandString, { typecheck: false, addReturn: true, editable: true, params: { args: 'any' }, transformer: this.getTransformer(), globals }); if (!script.compiled) { this.commands.push({ command: this.commandString, result: script.errors }); return; @@ -139,13 +168,13 @@ export class ScriptingRepl extends React.Component { this.maybeScrollToBottom(); - this.commandString = ""; - this.commandBuffer = ""; + this.commandString = ''; + this.commandBuffer = ''; this.historyIndex = -1; } break; } - case "ArrowUp": { + case 'ArrowUp': { if (this.historyIndex < this.commands.length - 1) { this.historyIndex++; if (this.historyIndex === 0) { @@ -155,12 +184,12 @@ export class ScriptingRepl extends React.Component { } break; } - case "ArrowDown": { + case 'ArrowDown': { if (this.historyIndex >= 0) { this.historyIndex--; if (this.historyIndex === -1) { this.commandString = this.commandBuffer; - this.commandBuffer = ""; + this.commandBuffer = ''; } else { this.commandString = this.commandsHistory[this.commands.length - 1 - this.historyIndex]; } @@ -176,25 +205,25 @@ export class ScriptingRepl extends React.Component { e.stopPropagation(); e.preventDefault(); } - } + }; @action onChange = (e: React.ChangeEvent<HTMLInputElement>) => { this.commandString = e.target.value; - } + }; private shouldScroll: boolean = false; private maybeScrollToBottom = () => { const ele = this.commandsRef.current; - if (ele && ele.scrollTop === (ele.scrollHeight - ele.offsetHeight)) { + if (ele && ele.scrollTop === ele.scrollHeight - ele.offsetHeight) { this.shouldScroll = true; this.forceUpdate(); } - } + }; private scrollToBottom() { const ele = this.commandsRef.current; - ele && ele.scroll({ behavior: "auto", top: ele.scrollHeight }); + ele && ele.scroll({ behavior: 'auto', top: ele.scrollHeight }); } componentDidUpdate() { @@ -206,15 +235,11 @@ export class ScriptingRepl extends React.Component { overlayDisposer?: () => void; onFocus = () => { - if (this.overlayDisposer) { - this.overlayDisposer(); - } + this.overlayDisposer?.(); this.overlayDisposer = OverlayView.Instance.addElement(<DocumentIconContainer />, { x: 0, y: 0 }); - } + }; - onBlur = () => { - this.overlayDisposer?.(); - } + onBlur = () => this.overlayDisposer?.(); render() { return ( @@ -229,14 +254,8 @@ export class ScriptingRepl extends React.Component { ); })} </div> - <input - className="scriptingRepl-commandInput" - onFocus={this.onFocus} - onBlur={this.onBlur} - value={this.commandString} - onChange={this.onChange} - onKeyDown={this.onKeyDown}></input> + <input className="scriptingRepl-commandInput" onFocus={this.onFocus} onBlur={this.onBlur} value={this.commandString} onChange={this.onChange} onKeyDown={this.onKeyDown}></input> </div> ); } -}
\ No newline at end of file +} diff --git a/src/client/views/SidebarAnnos.scss b/src/client/views/SidebarAnnos.scss index a0506cb3a..d7de2b641 100644 --- a/src/client/views/SidebarAnnos.scss +++ b/src/client/views/SidebarAnnos.scss @@ -4,22 +4,26 @@ overflow: auto; flex-flow: row; flex-wrap: wrap; - .sidebarAnnos-filterTag, .sidebarAnnos-filterTag-active, - .sidebarAnnos-filterUser, .sidebarAnnos-filterUser-active { + .sidebarAnnos-filterTag, + .sidebarAnnos-filterTag-active, + .sidebarAnnos-filterUser, + .sidebarAnnos-filterUser-active { font-weight: bold; font-size: 10px; padding-left: 5; padding-right: 5; box-shadow: black 1px 1px 3px; border-radius: 5; - margin: 2; + margin: 2; height: 15; background-color: lightgrey; } - .sidebarAnnos-filterUser, .sidebarAnnos-filterUser-active { + .sidebarAnnos-filterUser, + .sidebarAnnos-filterUser-active { border-radius: 15px; } + .sidebarAnnos-filterUser-active, .sidebarAnnos-filterTag-active { background-color: white; } -}
\ No newline at end of file +} diff --git a/src/client/views/SidebarAnnos.tsx b/src/client/views/SidebarAnnos.tsx index b0aab968e..db273cc88 100644 --- a/src/client/views/SidebarAnnos.tsx +++ b/src/client/views/SidebarAnnos.tsx @@ -49,17 +49,25 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { ); return keys; } + @computed get allHashtags() { + const keys = new Set<string>(); + DocListCast(this.props.rootDoc[this.sidebarKey]).forEach(doc => StrListCast(doc.tags).forEach(tag => keys.add(tag))); + return Array.from(keys.keys()) + .filter(key => key[0]) + .filter(key => !key.startsWith('_') && (key[0] === '#' || key[0] === key[0].toUpperCase())) + .sort(); + } @computed get allUsers() { const keys = new Set<string>(); DocListCast(this.props.rootDoc[this.sidebarKey]).forEach(doc => keys.add(StrCast(doc.author))); return Array.from(keys.keys()).sort(); } get filtersKey() { - return '_' + this.sidebarKey + '-docFilters'; + return '_' + this.sidebarKey + '_childFilters'; } anchorMenuClick = (anchor: Doc) => { - const startup = StrListCast(this.props.rootDoc.docFilters) + const startup = StrListCast(this.props.rootDoc.childFilters) .map(filter => filter.split(':')[0]) .join(' '); const target = Docs.Create.TextDocument(startup, { @@ -67,17 +75,17 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { annotationOn: this.props.rootDoc, _width: 200, _height: 50, - _fitWidth: true, - _autoHeight: true, - _fontSize: StrCast(Doc.UserDoc().fontSize), - _fontFamily: StrCast(Doc.UserDoc().fontFamily), + _layout_fitWidth: true, + _layout_autoHeight: true, + _text_fontSize: StrCast(Doc.UserDoc().fontSize), + _text_fontFamily: StrCast(Doc.UserDoc().fontFamily), }); FormattedTextBox.SelectOnLoad = target[Id]; FormattedTextBox.DontSelectInitialText = true; - const link = DocUtils.MakeLink(anchor, target, { linkRelationship: 'inline comment:comment on' }); - link && (link.linkDisplay = false); + const link = DocUtils.MakeLink(anchor, target, { link_relationship: 'inline comment:comment on' }); + link && (link.link_displayLine = false); - const taggedContent = this.docFilters() + const taggedContent = this.childFilters() .filter(data => data.split(':')[0]) .map(data => { const key = data.split(':')[0]; @@ -141,7 +149,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { return target; }; makeDocUnfiltered = (doc: Doc) => { - if (DocListCast(this.props.rootDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.unrendered ? DocCast(doc.annotationOn) : doc, anno))) { + if (DocListCast(this.props.rootDoc[this.sidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) { if (this.props.layoutDoc[this.filtersKey]) { this.props.layoutDoc[this.filtersKey] = new List<string>(); } @@ -151,7 +159,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { }; get sidebarKey() { - return this.props.fieldKey + '-sidebar'; + return this.props.fieldKey + '_sidebar'; } filtersHeight = () => 38; screenToLocalTransform = () => @@ -169,19 +177,19 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { addDocument = (doc: Doc | Doc[]) => this.props.sidebarAddDocument(doc, this.sidebarKey); moveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.props.moveDocument(doc, targetCollection, addDocument, this.sidebarKey); removeDocument = (doc: Doc | Doc[]) => this.props.removeDocument(doc, this.sidebarKey); - docFilters = () => [...StrListCast(this.props.layoutDoc._docFilters), ...StrListCast(this.props.layoutDoc[this.filtersKey])]; - showTitle = () => 'title'; + childFilters = () => [...StrListCast(this.props.layoutDoc._childFilters), ...StrListCast(this.props.layoutDoc[this.filtersKey])]; + layout_showTitle = () => 'title'; setHeightCallback = (height: number) => this.props.setHeight?.(height + this.filtersHeight()); sortByLinkAnchorY = (a: Doc, b: Doc) => { - const ay = LinkManager.Links(a).length && DocCast(LinkManager.Links(a)[0].anchor1).y; - const by = LinkManager.Links(b).length && DocCast(LinkManager.Links(b)[0].anchor1).y; + const ay = LinkManager.Links(a).length && DocCast(LinkManager.Links(a)[0].link_anchor_1).y; + const by = LinkManager.Links(b).length && DocCast(LinkManager.Links(b)[0].link_anchor_1).y; return NumCast(ay) - NumCast(by); }; render() { const renderTag = (tag: string) => { - const active = StrListCast(this.props.rootDoc[this.filtersKey]).includes(`${tag}:${tag}:check`); + const active = StrListCast(this.props.rootDoc[this.filtersKey]).includes(`tags:${tag}:check`); return ( - <div key={tag} className={`sidebarAnnos-filterTag${active ? '-active' : ''}`} onClick={e => Doc.setDocFilter(this.props.rootDoc, tag, tag, 'check', true, this.sidebarKey, e.shiftKey)}> + <div key={tag} className={`sidebarAnnos-filterTag${active ? '-active' : ''}`} onClick={e => Doc.setDocFilter(this.props.rootDoc, 'tags', tag, 'check', true, this.sidebarKey, e.shiftKey)}> {tag} </div> ); @@ -208,7 +216,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { style={{ position: 'absolute', pointerEvents: this.props.isContentActive() ? 'all' : undefined, - top: this.props.rootDoc.type !== DocumentType.RTF && StrCast(this.props.rootDoc._showTitle) === 'title' ? 15 : 0, + top: this.props.rootDoc.type !== DocumentType.RTF && StrCast(this.props.rootDoc._layout_showTitle) === 'title' ? 15 : 0, right: 0, background: this.props.styleProvider?.(this.props.rootDoc, this.props, StyleProp.WidgetColor), width: `100%`, @@ -216,6 +224,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { }}> <div className="sidebarAnnos-tagList" style={{ height: this.filtersHeight() }} onWheel={e => e.stopPropagation()}> {this.allUsers.map(renderUsers)} + {this.allHashtags.map(renderTag)} {Array.from(this.allMetadata.keys()) .sort() .map(key => renderMeta(key, this.allMetadata.get(key)))} @@ -229,13 +238,13 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { ref={this._stackRef} PanelHeight={this.panelHeight} PanelWidth={this.panelWidth} - docFilters={this.docFilters} + childFilters={this.childFilters} sortFunc={this.sortByLinkAnchorY} setHeight={this.setHeightCallback} isAnnotationOverlay={false} select={emptyFunction} NativeDimScaling={returnOne} - //childShowTitle={this.showTitle} + //childlayout_showTitle={this.layout_showTitle} isAnyChildContentActive={returnFalse} childDocumentsActive={this.props.isContentActive} whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} @@ -245,7 +254,7 @@ export class SidebarAnnos extends React.Component<FieldViewProps & ExtraProps> { addDocument={this.addDocument} ScreenToLocalTransform={this.screenToLocalTransform} renderDepth={this.props.renderDepth + 1} - viewType={CollectionViewType.Stacking} + type_collection={CollectionViewType.Stacking} fieldKey={this.sidebarKey} pointerEvents={returnAll} /> diff --git a/src/client/views/StyleProvider.tsx b/src/client/views/StyleProvider.tsx index 192a501f2..2cca2c5a9 100644 --- a/src/client/views/StyleProvider.tsx +++ b/src/client/views/StyleProvider.tsx @@ -18,13 +18,12 @@ import { TreeSort } from './collections/TreeView'; import { Colors } from './global/globalEnums'; import { InkingStroke } from './InkingStroke'; import { MainView } from './MainView'; -import { DocumentViewProps, OpenWhere } from './nodes/DocumentView'; +import { DocumentViewProps } from './nodes/DocumentView'; import { FieldViewProps } from './nodes/FieldView'; import { KeyValueBox } from './nodes/KeyValueBox'; import { SliderBox } from './nodes/SliderBox'; import './StyleProvider.scss'; import React = require('react'); -import { SchemaRowBox } from './collections/collectionSchema/SchemaRowBox'; export enum StyleProp { TreeViewIcon = 'treeViewIcon', @@ -37,13 +36,13 @@ export enum StyleProp { BackgroundColor = 'backgroundColor', // background color of a document view FillColor = 'fillColor', // fill color of an ink stroke or shape WidgetColor = 'widgetColor', // color to display UI widgets on a document view -- used for the sidebar divider dragger on a text note - HideLinkButton = 'hideLinkButton', // hides the blue-dot link button. used when a document acts like a button + HideLinkBtn = 'hideLinkButton', // hides the blue-dot link button. used when a document acts like a button PointerEvents = 'pointerEvents', // pointer events for DocumentView -- inherits pointer events if not specified Decorations = 'decorations', // additional decoration to display above a DocumentView -- currently only used to display a Lock for making things background HeaderMargin = 'headerMargin', // margin at top of documentview, typically for displaying a title -- doc contents will start below that - ShowCaption = 'showCaption', + ShowCaption = 'layout_showCaption', TitleHeight = 'titleHeight', // Height of Title area - ShowTitle = 'showTitle', // whether to display a title on a Document (optional :hover suffix) + ShowTitle = 'layout_showTitle', // whether to display a title on a Document (optional :hover suffix) JitterRotation = 'jitterRotation', // whether documents should be randomly rotated BorderPath = 'customBorder', // border path for document view FontSize = 'fontSize', // size of text font @@ -84,20 +83,22 @@ export function wavyBorderPath(pw: number, ph: number, inset: number = 0.05) { // a preliminary implementation of a dash style sheet for setting rendering properties of documents nested within a Tab // export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps>, property: string): any { - const remoteDocHeader = 'author;creationDate;noMargin'; + const remoteDocHeader = 'author;author_date;noMargin'; const docProps = testDocProps(props) ? props : undefined; const selected = property.includes(':selected'); const isCaption = property.includes(':caption'); const isAnchor = property.includes(':anchor'); const isAnnotated = property.includes(':annotated'); const isOpen = property.includes(':open'); + const isEmpty = property.includes(':empty'); const boxBackground = property.includes(':box'); - const fieldKey = props?.fieldKey ? props.fieldKey + '-' : isCaption ? 'caption-' : ''; + const fieldKey = props?.fieldKey ? props.fieldKey + '_' : isCaption ? 'caption_' : ''; const lockedPosition = () => doc && BoolCast(doc._lockedPosition); const backgroundCol = () => props?.styleProvider?.(doc, props, StyleProp.BackgroundColor); const opacity = () => props?.styleProvider?.(doc, props, StyleProp.Opacity); - const showTitle = () => props?.styleProvider?.(doc, props, StyleProp.ShowTitle); + const layout_showTitle = () => props?.styleProvider?.(doc, props, StyleProp.ShowTitle); const random = (min: number, max: number, x: number, y: number) => /* min should not be equal to max */ min + (((Math.abs(x * y) * 9301 + 49297) % 233280) / 233280) * (max - min); + // prettier-ignore switch (property.split(':')[0]) { case StyleProp.TreeViewIcon: const img = ImageCast(doc?.icon, ImageCast(doc?.data)); @@ -106,17 +107,16 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps const url = doc?.icon ? img.url.href : img.url.href.replace(ext, '_s' + ext); return <img src={url} width={20} height={15} style={{ margin: 'auto', display: 'block', objectFit: 'contain' }} />; } - return Doc.toIcon(doc, isOpen); - + return Doc.toIcon(doc, isEmpty ? undefined : isOpen); case StyleProp.TreeViewSortings: const allSorts: { [key: string]: { color: string; label: string } | undefined } = {}; allSorts[TreeSort.Down] = { color: 'blue', label: '↓' }; allSorts[TreeSort.Up] = { color: 'crimson', label: '↑' }; - if (doc?._viewType === CollectionViewType.Freeform) allSorts[TreeSort.Zindex] = { color: 'green', label: 'z' }; + if (doc?._type_collection === CollectionViewType.Freeform) allSorts[TreeSort.Zindex] = { color: 'green', label: 'z' }; allSorts[TreeSort.None] = { color: 'darkgray', label: '\u00A0\u00A0\u00A0' }; return allSorts; case StyleProp.Highlighting: - if (doc && !doc.disableDocBrushing && !props?.disableDocBrushing) { + if (doc && !doc.layout_disableBrushing && !props?.disableBrushing) { const selected = SelectionManager.Views().some(dv => dv.rootDoc === doc); const highlightIndex = Doc.isBrushedHighlightedDegree(doc) || (selected ? Doc.DocBrushStatus.selfBrushed : 0); const highlightColor = ['transparent', 'rgb(68, 118, 247)', selected ? 'black' : 'rgb(68, 118, 247)', 'orange', 'lightBlue'][highlightIndex]; @@ -131,39 +131,33 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps } } return undefined; - case StyleProp.DocContents: - return undefined; - case StyleProp.WidgetColor: - return isAnnotated ? Colors.LIGHT_BLUE : darkScheme() ? 'lightgrey' : 'dimgrey'; - case StyleProp.Opacity: - return props?.LayoutTemplateString?.includes(KeyValueBox.name) ? 1 : Cast(doc?._opacity, 'number', Cast(doc?.opacity, 'number', null)); - case StyleProp.HideLinkButton: - return props?.hideLinkButton || (!selected && doc?.hideLinkButton); - case StyleProp.FontSize: - return StrCast(doc?.[fieldKey + 'fontSize'], StrCast(doc?._fontSize, StrCast(Doc.UserDoc().fontSize))); - case StyleProp.FontFamily: - return StrCast(doc?.[fieldKey + 'fontFamily'], StrCast(doc?._fontFamily, StrCast(Doc.UserDoc().fontFamily))); - case StyleProp.FontWeight: - return StrCast(doc?.[fieldKey + 'fontWeight'], StrCast(doc?._fontWeight, StrCast(Doc.UserDoc().fontWeight))); + case StyleProp.DocContents:return undefined; + case StyleProp.WidgetColor:return isAnnotated ? Colors.LIGHT_BLUE : darkScheme() ? 'lightgrey' : 'dimgrey'; + case StyleProp.Opacity: return props?.LayoutTemplateString?.includes(KeyValueBox.name) ? 1 : Cast(doc?._opacity, "number", Cast(doc?.opacity, 'number', null)); + case StyleProp.HideLinkBtn:return props?.hideLinkButton || (!selected && doc?.layout_hideLinkButton); + case StyleProp.FontSize: return StrCast(doc?.[fieldKey + 'fontSize'], StrCast(doc?._text_fontSize, StrCast(Doc.UserDoc().fontSize))); + case StyleProp.FontFamily: return StrCast(doc?.[fieldKey + 'fontFamily'], StrCast(doc?._text_fontFamily, StrCast(Doc.UserDoc().fontFamily))); + case StyleProp.FontWeight: return StrCast(doc?.[fieldKey + 'fontWeight'], StrCast(doc?._text_fontWeight, StrCast(Doc.UserDoc().fontWeight))); + case StyleProp.FillColor: return StrCast(doc?._fillColor, StrCast(doc?.fillColor, 'transparent')); + case StyleProp.ShowCaption:return doc?._type_collection === CollectionViewType.Carousel || props?.hideCaptions ? undefined : StrCast(doc?._layout_showCaption); + case StyleProp.TitleHeight:return 15; case StyleProp.ShowTitle: return ( (doc && !doc.presentationTargetDoc && !props?.LayoutTemplateString?.includes(KeyValueBox.name) && - props?.showTitle?.() !== '' && + props?.layout_showTitle?.() !== '' && StrCast( - doc._showTitle, - props?.showTitle?.() || + doc._layout_showTitle, + props?.layout_showTitle?.() || (!Doc.IsSystem(doc) && [DocumentType.COL, DocumentType.FUNCPLOT, DocumentType.LABEL, DocumentType.RTF, DocumentType.IMG, DocumentType.VID].includes(doc.type as any) ? doc.author === Doc.CurrentUserEmail - ? StrCast(Doc.UserDoc().showTitle) + ? StrCast(Doc.UserDoc().layout_showTitle) : remoteDocHeader : '') )) || '' ); - case StyleProp.FillColor: - return Cast(doc?._fillColor, 'string', Cast(doc?.fillColor, 'string', 'transparent')); case StyleProp.Color: if (MainView.Instance.LastButton === doc) return Colors.DARK_GRAY; const docColor: Opt<string> = StrCast(doc?.[fieldKey + 'color'], StrCast(doc?._color)); @@ -173,9 +167,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps if (!backColor) return undefined; return lightOrDark(backColor); case StyleProp.BorderRounding: - return StrCast(doc?.[fieldKey + 'borderRounding'], StrCast(doc?.borderRounding, doc?._viewType === CollectionViewType.Pile ? '50%' : '')); - case StyleProp.TitleHeight: - return 15; + return StrCast(doc?.[fieldKey + 'borderRounding'], StrCast(doc?.layout_borderRounding, doc?._type_collection === CollectionViewType.Pile ? '50%' : '')); case StyleProp.BorderPath: const borderPath = Doc.IsComicStyle(doc) && props?.renderDepth && @@ -194,68 +186,40 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps }; case StyleProp.JitterRotation: return Doc.IsComicStyle(doc) ? random(-1, 1, NumCast(doc?.x), NumCast(doc?.y)) * ((props?.PanelWidth() || 0) > (props?.PanelHeight() || 0) ? 5 : 10) : 0; - case StyleProp.ShowCaption: - return doc?._viewType === CollectionViewType.Carousel || props?.hideCaptions ? undefined : StrCast(doc?._showCaption); case StyleProp.HeaderMargin: - return ([CollectionViewType.Stacking, CollectionViewType.NoteTaking, CollectionViewType.Masonry, CollectionViewType.Tree].includes(doc?._viewType as any) || - (doc?.type === DocumentType.RTF && !showTitle()?.includes('noMargin')) || + return ([CollectionViewType.Stacking, CollectionViewType.NoteTaking, CollectionViewType.Masonry, CollectionViewType.Tree].includes(doc?._type_collection as any) || + (doc?.type === DocumentType.RTF && !layout_showTitle()?.includes('noMargin')) || doc?.type === DocumentType.LABEL) && - showTitle() && - !StrCast(doc?.showTitle).includes(':hover') + layout_showTitle() && + !StrCast(doc?.layout_showTitle).includes(':hover') ? 15 : 0; case StyleProp.BackgroundColor: { if (MainView.Instance.LastButton === doc) return Colors.LIGHT_GRAY; - let docColor: Opt<string> = StrCast(doc?.[fieldKey + 'backgroundColor'], StrCast(doc?._backgroundColor, isCaption ? 'rgba(0,0,0,0.4)' : '')); + let docColor: Opt<string> = StrCast(doc?.[fieldKey + '_backgroundColor'], StrCast(doc?._backgroundColor, isCaption ? 'rgba(0,0,0,0.4)' : '')); + // prettier-ignore switch (doc?.type) { - case DocumentType.PRESELEMENT: - docColor = docColor || (darkScheme() ? '' : ''); - break; - case DocumentType.PRES: - docColor = docColor || (darkScheme() ? 'transparent' : 'transparent'); - break; - case DocumentType.FONTICON: - docColor = boxBackground ? undefined : docColor || Colors.DARK_GRAY; - break; - case DocumentType.RTF: - docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY); - break; - case DocumentType.FILTER: - docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : 'rgba(105, 105, 105, 0.432)'); - break; - case DocumentType.INK: - docColor = doc?.isInkMask ? 'rgba(0,0,0,0.7)' : undefined; - break; - case DocumentType.SLIDER: - break; - case DocumentType.EQUATION: - docColor = docColor || 'transparent'; - break; - case DocumentType.LABEL: - docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY); - break; - case DocumentType.BUTTON: - docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY); - break; - case DocumentType.LINKANCHOR: - docColor = isAnchor ? Colors.LIGHT_BLUE : 'transparent'; - break; - case DocumentType.LINK: - docColor = (isAnchor ? docColor : '') || 'transparent'; - break; + case DocumentType.SLIDER: break; + case DocumentType.PRESELEMENT: docColor = docColor || (darkScheme() ? '' : ''); break; + case DocumentType.PRES: docColor = docColor || (darkScheme() ? 'transparent' : 'transparent'); break; + case DocumentType.FONTICON: docColor = boxBackground ? undefined : docColor || Colors.DARK_GRAY; break; + case DocumentType.RTF: docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY); break; + case DocumentType.FILTER: docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : 'rgba(105, 105, 105, 0.432)'); break; + case DocumentType.INK: docColor = doc?.stroke_isInkMask ? 'rgba(0,0,0,0.7)' : undefined; break; + case DocumentType.EQUATION: docColor = docColor || 'transparent'; break; + case DocumentType.LABEL: docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY); break; + case DocumentType.BUTTON: docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY); break; + case DocumentType.LINKANCHOR: docColor = isAnchor ? Colors.LIGHT_BLUE : 'transparent'; break; + case DocumentType.LINK: docColor = (isAnchor ? docColor : '') || 'transparent'; break; case DocumentType.IMG: case DocumentType.WEB: case DocumentType.PDF: case DocumentType.MAP: case DocumentType.SCREENSHOT: - case DocumentType.VID: - docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY); - break; + case DocumentType.VID: docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY); break; case DocumentType.COL: if (StrCast(Doc.LayoutField(doc)).includes(SliderBox.name)) break; - docColor = - docColor || - (Doc.IsSystem(doc) + docColor = docColor || (Doc.IsSystem(doc) ? darkScheme() ? Colors.DARK_GRAY : Colors.LIGHT_GRAY // system docs (seen in treeView) get a grayish background @@ -265,48 +229,44 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps ? undefined : Cast((props?.renderDepth || 0) > 0 ? Doc.UserDoc().activeCollectionNestedBackground : Doc.UserDoc().activeCollectionBackground, 'string') ?? (darkScheme() ? Colors.BLACK : 'linear-gradient(#065fff, #85c1f9)')); break; - //if (doc._viewType !== CollectionViewType.Freeform && doc._viewType !== CollectionViewType.Time) return "rgb(62,62,62)"; - default: - docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.WHITE); - break; + //if (doc._type_collection !== CollectionViewType.Freeform && doc._type_collection !== CollectionViewType.Time) return "rgb(62,62,62)"; + default: docColor = docColor || (darkScheme() ? Colors.DARK_GRAY : Colors.WHITE); } - if (docColor && !doc) docColor = DashColor(docColor).fade(0.5).toString(); - return docColor; + return (docColor && !doc) ? DashColor(docColor).fade(0.5).toString() : docColor; } case StyleProp.BoxShadow: { if (!doc || opacity() === 0 || doc.noShadow) return undefined; // if it's not visible, then no shadow) - if (doc.boxShadow === 'standard') return Shadows.STANDARD_SHADOW; + if (doc.layout_boxShadow === 'standard') return Shadows.STANDARD_SHADOW; if (IsFollowLinkScript(doc?.onClick) && LinkManager.Links(doc).length && ![DocumentType.LINK, DocumentType.INK].includes(doc.type as any)) return StrCast(doc?._linkButtonShadow, 'lightblue 0em 0em 1em'); switch (doc?.type) { case DocumentType.COL: return StrCast( - doc?.boxShadow, - doc?._viewType === CollectionViewType.Pile + doc?.layout_borderRounding, + doc?._type_collection === CollectionViewType.Pile ? '4px 4px 10px 2px' : lockedPosition() || doc?._isGroup || docProps?.LayoutTemplateString ? undefined // groups have no drop shadow -- they're supposed to be "invisible". LayoutString's imply collection is being rendered as something else (e.g., title of a Slide) - : `${darkScheme() ? Colors.DARK_GRAY : Colors.MEDIUM_GRAY} ${StrCast(doc.boxShadow, '0.2vw 0.2vw 0.8vw')}` + : `${darkScheme() ? Colors.DARK_GRAY : Colors.MEDIUM_GRAY} ${StrCast(doc.layout_borderRounding, '0.2vw 0.2vw 0.8vw')}` ); case DocumentType.LABEL: if (doc?.annotationOn !== undefined) return 'black 2px 2px 1px'; default: return doc.z - ? `#9c9396 ${StrCast(doc?.boxShadow, '10px 10px 0.9vw')}` // if it's a floating doc, give it a big shadow - : props?.docViewPath().lastElement()?.rootDoc._useClusters - ? `${backgroundCol()} ${StrCast(doc.boxShadow, `0vw 0vw ${(lockedPosition() ? 100 : 50) / (docProps?.NativeDimScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent + ? `#9c9396 ${StrCast(doc?.layout_boxShadow, '10px 10px 0.9vw')}` // if it's a floating doc, give it a big shadow + : props?.docViewPath().lastElement()?.rootDoc._freeform_useClusters + ? `${backgroundCol()} ${StrCast(doc.layout_boxShadow, `0vw 0vw ${(lockedPosition() ? 100 : 50) / (docProps?.NativeDimScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent : NumCast(doc.group, -1) !== -1 - ? `gray ${StrCast(doc.boxShadow, `0vw 0vw ${(lockedPosition() ? 100 : 50) / (docProps?.NativeDimScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent + ? `gray ${StrCast(doc.layout_boxShadow, `0vw 0vw ${(lockedPosition() ? 100 : 50) / (docProps?.NativeDimScaling?.() || 1)}px`)}` // if it's just in a cluster, make the shadown roughly match the cluster border extent : lockedPosition() ? undefined // if it's a background & has a cluster color, make the shadow spread really big - : StrCast(doc.boxShadow, ''); + : StrCast(doc.layout_boxShadow, ''); } } case StyleProp.PointerEvents: const isInk = doc && StrCast(Doc.Layout(doc).layout).includes(InkingStroke.name) && !props?.LayoutTemplateString; if (docProps?.DocumentView?.().ComponentView?.overridePointerEvents?.() !== undefined) return docProps?.DocumentView?.().ComponentView?.overridePointerEvents?.(); - if (MainView.Instance._exploreMode || doc?.unrendered) return isInk ? 'visiblePainted' : 'all'; - if (doc?.pointerEvents) return StrCast(doc.pointerEvents); + if (MainView.Instance._exploreMode || doc?.layout_unrendered) return isInk ? 'visiblePainted' : 'all'; if (props?.contentPointerEvents) return StrCast(props.contentPointerEvents); if (props?.pointerEvents?.() === 'none') return 'none'; if (opacity() === 0) return 'none'; @@ -314,7 +274,7 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps return undefined; // fixes problem with tree view elements getting pointer events when the tree view is not active case StyleProp.Decorations: const lock = () => { - if (props?.docViewPath().lastElement()?.rootDoc?._viewType === CollectionViewType.Freeform) { + if (props?.docViewPath().lastElement()?.rootDoc?._type_collection === CollectionViewType.Freeform) { return doc?.pointerEvents !== 'none' ? null : ( <div className="styleProvider-lock" onClick={() => toggleLockedPosition(doc)}> <FontAwesomeIcon icon={'lock'} style={{ color: 'red' }} size="lg" /> @@ -324,9 +284,9 @@ export function DefaultStyleProvider(doc: Opt<Doc>, props: Opt<DocumentViewProps }; const filter = () => { const showFilterIcon = - StrListCast(doc?._docFilters).length || StrListCast(doc?._docRangeFilters).length + StrListCast(doc?._childFilters).length || StrListCast(doc?._childFiltersByRanges).length ? '#18c718bd' //'hasFilter' - : docProps?.docFilters?.().filter(f => Utils.IsRecursiveFilter(f) && f !== Utils.noDragsDocFilter).length || docProps?.docRangeFilters().length + : docProps?.childFilters?.().filter(f => Utils.IsRecursiveFilter(f) && f !== Utils.noDragsDocFilter).length || docProps?.childFiltersByRanges().length ? 'orange' //'inheritsFilter' : undefined; return !showFilterIcon ? null : ( @@ -378,13 +338,9 @@ export function DashboardToggleButton(doc: Doc, field: string, onIcon: IconProp, */ export function DashboardStyleProvider(doc: Opt<Doc>, props: Opt<FieldViewProps | DocumentViewProps>, property: string) { if (doc && property.split(':')[0] === StyleProp.Decorations) { - return doc._viewType === CollectionViewType.Docking ? null : ( - <> - {DashboardToggleButton(doc, 'hidden', 'eye-slash', 'eye', () => { - DocFocusOrOpen(doc, { toggleTarget: true, willZoomCentered: true, zoomScale: 0 }, DocCast(doc?.context ?? doc?.annotationOn)); - })} - </> - ); + return doc._type_collection === CollectionViewType.Docking + ? null + : DashboardToggleButton(doc, 'hidden', 'eye-slash', 'eye', () => DocFocusOrOpen(doc, { toggleTarget: true, willZoomCentered: true, zoomScale: 0 }, DocCast(doc?.embedContainer ?? doc?.annotationOn))); } return DefaultStyleProvider(doc, props, property); } diff --git a/src/client/views/TemplateMenu.tsx b/src/client/views/TemplateMenu.tsx index 1c816aa05..62f47ba71 100644 --- a/src/client/views/TemplateMenu.tsx +++ b/src/client/views/TemplateMenu.tsx @@ -91,12 +91,12 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { } templateIsUsed = (selDoc: Doc, templateDoc: Doc) => { const template = StrCast(templateDoc.dragFactory ? Cast(templateDoc.dragFactory, Doc, null)?.title : templateDoc.title); - return StrCast(selDoc.layoutKey) === 'layout_' + template ? 'check' : 'unchecked'; + return StrCast(selDoc.layout_fieldKey) === 'layout_' + template ? 'check' : 'unchecked'; }; render() { TraceMobx(); const firstDoc = this.props.docViews[0].props.Document; - const templateName = StrCast(firstDoc.layoutKey, 'layout').replace('layout_', ''); + const templateName = StrCast(firstDoc.layout_fieldKey, 'layout').replace('layout_', ''); const noteTypes = DocListCast(Cast(Doc.UserDoc()['template-notes'], Doc, null)?.data); const addedTypes = DocListCast(Cast(Doc.UserDoc()['template-clickFuncs'], Doc, null)?.data); const templateMenu: Array<JSX.Element> = []; @@ -116,8 +116,8 @@ export class TemplateMenu extends React.Component<TemplateMenuProps> { styleProvider={DefaultStyleProvider} setHeight={returnFalse} docViewPath={returnEmptyDoclist} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} rootSelected={returnFalse} onCheckedClick={this.scriptField} diff --git a/src/client/views/UndoStack.scss b/src/client/views/UndoStack.scss new file mode 100644 index 000000000..ab21e6d7e --- /dev/null +++ b/src/client/views/UndoStack.scss @@ -0,0 +1,29 @@ +.undoStack-outerContainer { + height: 100%; + display: flex; + flex-direction: column; + position: relative; + pointer-events: all; + padding-left: 4px; +} + +.undoStack-resultContainer { + border-radius: 5px; +} + +.undoStack-commandInput { + width: 100%; +} + +.undoStack-commandResult, +.undoStack-commandString { + overflow-wrap: break-word; +} + +.undoStack-commandsContainer { + background-color: whitesmoke; + flex: 1 1 auto; + overflow-y: scroll; + height: 30px; + border-radius: 5px; +} diff --git a/src/client/views/UndoStack.tsx b/src/client/views/UndoStack.tsx new file mode 100644 index 000000000..f5af09e5b --- /dev/null +++ b/src/client/views/UndoStack.tsx @@ -0,0 +1,47 @@ +import { action, observable } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { UndoManager } from '../util/UndoManager'; +import './UndoStack.scss'; + +interface UndoStackProps { + width?: number; + height?: number; + inline?: boolean; +} +@observer +export class UndoStack extends React.Component<UndoStackProps> { + @observable static HideInline: boolean; + @observable static Expand: boolean; + render() { + return this.props.inline && UndoStack.HideInline ? null : ( + <div + className="undoStack-outerContainer" + style={{ width: this.props.width, height: this.props.height ? (UndoStack.Expand ? 4 : 1) * this.props.height : undefined, top: UndoStack.Expand && this.props.height ? -this.props.height * 3 : undefined }} + onClick={action(e => (UndoStack.Expand = !UndoStack.Expand))} + onDoubleClick={action(e => (UndoStack.Expand = UndoStack.HideInline = false))}> + <div className="undoStack-commandsContainer" ref={r => r?.scroll({ behavior: 'auto', top: r?.scrollHeight + 20 })} style={{ background: UndoManager.batchCounter.get() ? 'yellow' : undefined }}> + <div className="undoStack-resultContainer" key={0}> + <div className="undoStack-commandString" style={{ fontWeight: 'bold', textAlign: 'center' }}> + Undo/Redo Stack + </div> + </div> + {UndoManager.undoStackNames.map((name, i) => ( + <div className="undoStack-resultContainer" key={i}> + <div className="undoStack-commandString">{name.replace(/[^\.]*\./, '')}</div> + </div> + ))} + {Array.from(UndoManager.redoStackNames) + .reverse() + .map((name, i) => ( + <div className="undoStack-resultContainer" key={i}> + <div className="undoStack-commandString" style={{ fontWeight: 'bold', color: 'red' }}> + {name.replace(/[^\.]*\./, '')} + </div> + </div> + ))} + </div> + </div> + ); + } +} diff --git a/src/client/views/animationtimeline/Timeline.tsx b/src/client/views/animationtimeline/Timeline.tsx index 7a393a4f7..adc97bbb4 100644 --- a/src/client/views/animationtimeline/Timeline.tsx +++ b/src/client/views/animationtimeline/Timeline.tsx @@ -76,7 +76,7 @@ export class Timeline extends React.Component<FieldViewProps> { private get children(): Doc[] { const annotatedDoc = [DocumentType.IMG, DocumentType.VID, DocumentType.PDF, DocumentType.MAP].includes(StrCast(this.props.Document.type) as any); if (annotatedDoc) { - return DocListCast(this.props.Document[Doc.LayoutFieldKey(this.props.Document) + '-annotations']); + return DocListCast(this.props.Document[Doc.LayoutFieldKey(this.props.Document) + '_annotations']); } return DocListCast(this.props.Document[this.props.fieldKey]); } @@ -290,15 +290,15 @@ export class Timeline extends React.Component<FieldViewProps> { }; resetView(doc: Doc) { - doc._panX = doc._customOriginX ?? 0; - doc._panY = doc._customOriginY ?? 0; - doc._viewScale = doc._customOriginScale ?? 1; + doc._freeform_panX = doc._customOriginX ?? 0; + doc._freeform_panY = doc._customOriginY ?? 0; + doc._freeform_scale = doc._customOriginScale ?? 1; } setView(doc: Doc) { - doc._customOriginX = doc._panX; - doc._customOriginY = doc._panY; - doc._customOriginScale = doc._viewScale; + doc._customOriginX = doc._freeform_panX; + doc._customOriginY = doc._freeform_panY; + doc._customOriginScale = doc._freeform_scale; } /** * zooming mechanism (increment and spacing changes) diff --git a/src/client/views/animationtimeline/Track.tsx b/src/client/views/animationtimeline/Track.tsx index 25c2e68e7..2349ba786 100644 --- a/src/client/views/animationtimeline/Track.tsx +++ b/src/client/views/animationtimeline/Track.tsx @@ -1,15 +1,15 @@ -import { action, computed, intercept, observable, reaction, runInAction } from "mobx"; -import { observer } from "mobx-react"; -import * as React from "react"; -import { Doc, DocListCast, Opt, DocListCastAsync } from "../../../fields/Doc"; -import { Copy } from "../../../fields/FieldSymbols"; -import { List } from "../../../fields/List"; -import { ObjectField } from "../../../fields/ObjectField"; -import { listSpec } from "../../../fields/Schema"; -import { Cast, NumCast } from "../../../fields/Types"; -import { Transform } from "../../util/Transform"; -import { Keyframe, KeyframeFunc, RegionData } from "./Keyframe"; -import "./Track.scss"; +import { action, computed, intercept, observable, reaction, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { Doc, DocListCast, Opt, DocListCastAsync } from '../../../fields/Doc'; +import { Copy } from '../../../fields/FieldSymbols'; +import { List } from '../../../fields/List'; +import { ObjectField } from '../../../fields/ObjectField'; +import { listSpec } from '../../../fields/Schema'; +import { Cast, NumCast } from '../../../fields/Types'; +import { Transform } from '../../util/Transform'; +import { Keyframe, KeyframeFunc, RegionData } from './Keyframe'; +import './Track.scss'; interface IProps { node: Doc; @@ -32,27 +32,22 @@ export class Track extends React.Component<IProps> { @observable private _newKeyframe: boolean = false; private readonly MAX_TITLE_HEIGHT = 75; @observable private _trackHeight = 0; - private primitiveWhitelist = [ - "x", - "y", - "_width", - "_height", - "opacity", - "_scrollTop" - ]; - private objectWhitelist = [ - "data" - ]; + private primitiveWhitelist = ['x', 'y', '_width', '_height', 'opacity', '_layout_scrollTop']; + private objectWhitelist = ['data']; - @computed private get regions() { return DocListCast(this.props.node.regions); } - @computed private get time() { return NumCast(KeyframeFunc.convertPixelTime(this.props.currentBarX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement)); } + @computed private get regions() { + return DocListCast(this.props.node.regions); + } + @computed private get time() { + return NumCast(KeyframeFunc.convertPixelTime(this.props.currentBarX, 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement)); + } async componentDidMount() { const regions = await DocListCastAsync(this.props.node.regions); if (!regions) this.props.node.regions = new List<Doc>(); //if there is no region, then create new doc to store stuff - //these two lines are exactly same from timeline.tsx + //these two lines are exactly same from timeline.tsx const relativeHeight = window.innerHeight / 20; - runInAction(() => this._trackHeight = relativeHeight < this.MAX_TITLE_HEIGHT ? relativeHeight : this.MAX_TITLE_HEIGHT); //for responsiveness + runInAction(() => (this._trackHeight = relativeHeight < this.MAX_TITLE_HEIGHT ? relativeHeight : this.MAX_TITLE_HEIGHT)); //for responsiveness this._timelineVisibleReaction = this.timelineVisibleReaction(); this._currentBarXReaction = this.currentBarXReaction(); if (DocListCast(this.props.node.regions).length === 0) this.createRegion(this.time); @@ -71,7 +66,6 @@ export class Track extends React.Component<IProps> { } //////////////////////////////// - getLastRegionTime = () => { let lastTime: number = 0; let lastRegion: Opt<Doc>; @@ -83,11 +77,11 @@ export class Track extends React.Component<IProps> { } }); return lastRegion ? lastTime + NumCast(lastRegion.duration) : 0; - } + }; /** * keyframe save logic. Needs to be changed so it's more efficient - * + * */ @action saveKeyframe = async () => { @@ -97,16 +91,18 @@ export class Track extends React.Component<IProps> { if (this._newKeyframe) { DocListCast(this.saveStateRegion?.keyframes).forEach((kf, index) => { this.copyDocDataToKeyFrame(kf); - kf.opacity = (index === 0 || index === 3) ? 0.1 : 1; + kf.opacity = index === 0 || index === 3 ? 0.1 : 1; }); this._newKeyframe = false; } if (!kf) return; - if (kf.type === KeyframeFunc.KeyframeType.default) { // only save for non-fades + if (kf.type === KeyframeFunc.KeyframeType.default) { + // only save for non-fades this.copyDocDataToKeyFrame(kf); const leftkf = KeyframeFunc.calcMinLeft(this.saveStateRegion!, this.time, kf); // lef keyframe, if it exists - const rightkf = KeyframeFunc.calcMinRight(this.saveStateRegion!, this.time, kf); //right keyframe, if it exists - if (leftkf?.type === KeyframeFunc.KeyframeType.fade) { //replicating this keyframe to fades + const rightkf = KeyframeFunc.calcMinRight(this.saveStateRegion!, this.time, kf); //right keyframe, if it exists + if (leftkf?.type === KeyframeFunc.KeyframeType.fade) { + //replicating this keyframe to fades const edge = KeyframeFunc.calcMinLeft(this.saveStateRegion!, this.time, leftkf); edge && this.copyDocDataToKeyFrame(edge); leftkf && this.copyDocDataToKeyFrame(leftkf); @@ -124,8 +120,7 @@ export class Track extends React.Component<IProps> { keyframes[kfIndex] = kf; this.saveStateKf = undefined; this.saveStateRegion = undefined; - } - + }; /** * autocreates keyframe @@ -136,23 +131,27 @@ export class Track extends React.Component<IProps> { intercept(this.props.node, change => { return change; }); - return reaction(() => { - return [...this.primitiveWhitelist.map(key => this.props.node[key]), ...objects]; - }, (changed, reaction) => { - //check for region - const region = this.findRegion(this.time); - if (region !== undefined) { //if region at scrub time exist - const r = region as RegionData; //for some region is returning undefined... which is not the case - if (DocListCast(r.keyframes).find(kf => kf.time === this.time) === undefined) { //basically when there is no additional keyframe at that timespot - this.makeKeyData(r, this.time, KeyframeFunc.KeyframeType.default); + return reaction( + () => { + return [...this.primitiveWhitelist.map(key => this.props.node[key]), ...objects]; + }, + (changed, reaction) => { + //check for region + const region = this.findRegion(this.time); + if (region !== undefined) { + //if region at scrub time exist + const r = region as RegionData; //for some region is returning undefined... which is not the case + if (DocListCast(r.keyframes).find(kf => kf.time === this.time) === undefined) { + //basically when there is no additional keyframe at that timespot + this.makeKeyData(r, this.time, KeyframeFunc.KeyframeType.default); + } } - } - }, { fireImmediately: false }); - } - - + }, + { fireImmediately: false } + ); + }; - // @observable private _storedState:(Doc | undefined) = undefined; + // @observable private _storedState:(Doc | undefined) = undefined; // /** // * reverting back to previous state before editing on AT // */ @@ -161,54 +160,62 @@ export class Track extends React.Component<IProps> { // if (this._storedState) this.applyKeys(this._storedState); // } - /** * Reaction when scrubber bar changes * made into function so it's easier to dispose later - */ + */ @action currentBarXReaction = () => { - return reaction(() => this.props.currentBarX, () => { - const regiondata = this.findRegion(this.time); - if (regiondata) { - this.props.node.hidden = false; - // if (!this._autoKfReaction) { - // // this._autoKfReaction = this.autoCreateKeyframe(); - // } - this.timeChange(); - } else { - this.props.node.hidden = true; - this.props.node.opacity = 0; - //if (this._autoKfReaction) this._autoKfReaction(); + return reaction( + () => this.props.currentBarX, + () => { + const regiondata = this.findRegion(this.time); + if (regiondata) { + this.props.node.hidden = false; + // if (!this._autoKfReaction) { + // // this._autoKfReaction = this.autoCreateKeyframe(); + // } + this.timeChange(); + } else { + this.props.node.hidden = true; + this.props.node.opacity = 0; + //if (this._autoKfReaction) this._autoKfReaction(); + } } - }); - } + ); + }; /** * when timeline is visible, reaction is ran so states are reverted */ @action timelineVisibleReaction = () => { - return reaction(() => { - return this.props.timelineVisible; - }, isVisible => { - if (isVisible) { - this.regions.filter(region => !region.hasData).forEach(region => { - for (let i = 0; i < 4; i++) { - this.copyDocDataToKeyFrame(DocListCast(region.keyframes)[i]); - if (i === 0 || i === 3) { //manually inputing fades - DocListCast(region.keyframes)[i].opacity = 0.1; - } - } - }); - } else { - //this.revertState(); + return reaction( + () => { + return this.props.timelineVisible; + }, + isVisible => { + if (isVisible) { + this.regions + .filter(region => !region.hasData) + .forEach(region => { + for (let i = 0; i < 4; i++) { + this.copyDocDataToKeyFrame(DocListCast(region.keyframes)[i]); + if (i === 0 || i === 3) { + //manually inputing fades + DocListCast(region.keyframes)[i].opacity = 0.1; + } + } + }); + } else { + //this.revertState(); + } } - }); - } + ); + }; - @observable private saveStateKf: (Doc | undefined) = undefined; - @observable private saveStateRegion: (Doc | undefined) = undefined; + @observable private saveStateKf: Doc | undefined = undefined; + @observable private saveStateRegion: Doc | undefined = undefined; /**w * when scrubber position changes. Need to edit the logic @@ -222,9 +229,9 @@ export class Track extends React.Component<IProps> { } const regiondata = await this.findRegion(Math.round(this.time)); //finds a region that the scrubber is on if (regiondata) { - const leftkf: (Doc | undefined) = await KeyframeFunc.calcMinLeft(regiondata, this.time); // lef keyframe, if it exists - const rightkf: (Doc | undefined) = await KeyframeFunc.calcMinRight(regiondata, this.time); //right keyframe, if it exists - const currentkf: (Doc | undefined) = await this.calcCurrent(regiondata); //if the scrubber is on top of the keyframe + const leftkf: Doc | undefined = await KeyframeFunc.calcMinLeft(regiondata, this.time); // lef keyframe, if it exists + const rightkf: Doc | undefined = await KeyframeFunc.calcMinRight(regiondata, this.time); //right keyframe, if it exists + const currentkf: Doc | undefined = await this.calcCurrent(regiondata); //if the scrubber is on top of the keyframe if (currentkf) { await this.applyKeys(currentkf); this.saveStateKf = currentkf; @@ -233,10 +240,10 @@ export class Track extends React.Component<IProps> { await this.interpolate(leftkf, rightkf); } } - } + }; /** - * applying changes (when saving the keyframe) + * applying changes (when saving the keyframe) * need to change the logic here */ @action @@ -249,73 +256,73 @@ export class Track extends React.Component<IProps> { this.props.node[key] = stored instanceof ObjectField ? stored[Copy]() : stored; } }); - } - + }; /** * calculating current keyframe, if the scrubber is right on the keyframe */ @action calcCurrent = (region: Doc) => { - let currentkf: (Doc | undefined) = undefined; + let currentkf: Doc | undefined = undefined; const keyframes = DocListCast(region.keyframes!); - keyframes.forEach((kf) => { + keyframes.forEach(kf => { if (NumCast(kf.time) === Math.round(this.time)) currentkf = kf; }); return currentkf; - } - + }; /** - * basic linear interpolation function + * basic linear interpolation function */ @action interpolate = async (left: Doc, right: Doc) => { this.primitiveWhitelist.forEach(key => { - if (left[key] && right[key] && typeof (left[key]) === "number" && typeof (right[key]) === "number") { //if it is number, interpolate + if (left[key] && right[key] && typeof left[key] === 'number' && typeof right[key] === 'number') { + //if it is number, interpolate const dif = NumCast(right[key]) - NumCast(left[key]); const deltaLeft = this.time - NumCast(left.time); const ratio = deltaLeft / (NumCast(right.time) - NumCast(left.time)); - this.props.node[key] = NumCast(left[key]) + (dif * ratio); - } else { // case data + this.props.node[key] = NumCast(left[key]) + dif * ratio; + } else { + // case data const stored = left[key]; this.props.node[key] = stored instanceof ObjectField ? stored[Copy]() : stored; } }); - } + }; /** * finds region that corresponds to specific time (is there a region at this time?) * linear O(n) (maybe possible to optimize this with other Data structures?) */ findRegion = (time: number) => { - return this.regions?.find(rd => (time >= NumCast(rd.position) && time <= (NumCast(rd.position) + NumCast(rd.duration)))); - } - + return this.regions?.find(rd => time >= NumCast(rd.position) && time <= NumCast(rd.position) + NumCast(rd.duration)); + }; /** - * double click on track. Signalling keyframe creation. + * double click on track. Signalling keyframe creation. */ @action onInnerDoubleClick = (e: React.MouseEvent) => { const inner = this._inner.current!; const offsetX = Math.round((e.clientX - inner.getBoundingClientRect().left) * this.props.transform.Scale); - this.createRegion(KeyframeFunc.convertPixelTime(offsetX, "mili", "time", this.props.tickSpacing, this.props.tickIncrement)); - } - + this.createRegion(KeyframeFunc.convertPixelTime(offsetX, 'mili', 'time', this.props.tickSpacing, this.props.tickIncrement)); + }; /** - * creates a region (KEYFRAME.TSX stuff). + * creates a region (KEYFRAME.TSX stuff). */ @action createRegion = (time: number) => { - if (this.findRegion(time) === undefined) { //check if there is a region where double clicking (prevents phantom regions) + if (this.findRegion(time) === undefined) { + //check if there is a region where double clicking (prevents phantom regions) const regiondata = KeyframeFunc.defaultKeyframe(); //create keyframe data regiondata.position = time; //set position const rightRegion = KeyframeFunc.findAdjacentRegion(KeyframeFunc.Direction.right, regiondata, this.regions); - if (rightRegion && rightRegion.position - regiondata.position <= 4000) { //edge case when there is less than default 4000 duration space between this and right region + if (rightRegion && rightRegion.position - regiondata.position <= 4000) { + //edge case when there is less than default 4000 duration space between this and right region regiondata.duration = rightRegion.position - regiondata.position; } if (this.regions.length === 0 || !rightRegion || (rightRegion && rightRegion.position - regiondata.position >= NumCast(regiondata.fadeIn) + NumCast(regiondata.fadeOut))) { @@ -325,27 +332,30 @@ export class Track extends React.Component<IProps> { return regiondata; } } - } + }; @action - makeKeyData = (regiondata: RegionData, time: number, type: KeyframeFunc.KeyframeType = KeyframeFunc.KeyframeType.default) => { //Kfpos is mouse offsetX, representing time + makeKeyData = (regiondata: RegionData, time: number, type: KeyframeFunc.KeyframeType = KeyframeFunc.KeyframeType.default) => { + //Kfpos is mouse offsetX, representing time const trackKeyFrames = DocListCast(regiondata.keyframes); const existingkf = trackKeyFrames.find(TK => TK.time === time); if (existingkf) return existingkf; - //else creates a new doc. + //else creates a new doc. const newKeyFrame: Doc = new Doc(); newKeyFrame.time = time; newKeyFrame.type = type; this.copyDocDataToKeyFrame(newKeyFrame); //assuming there are already keyframes (for keeping keyframes in order, sorted by time) if (trackKeyFrames.length === 0) regiondata.keyframes!.push(newKeyFrame); - trackKeyFrames.map(kf => NumCast(kf.time)).forEach((kfTime, index) => { - if ((kfTime < time && index === trackKeyFrames.length - 1) || (kfTime < time && time < NumCast(trackKeyFrames[index + 1].time))) { - regiondata.keyframes!.splice(index + 1, 0, newKeyFrame); - } - }); + trackKeyFrames + .map(kf => NumCast(kf.time)) + .forEach((kfTime, index) => { + if ((kfTime < time && index === trackKeyFrames.length - 1) || (kfTime < time && time < NumCast(trackKeyFrames[index + 1].time))) { + regiondata.keyframes!.splice(index + 1, 0, newKeyFrame); + } + }); return newKeyFrame; - } + }; @action copyDocDataToKeyFrame = (doc: Doc) => { @@ -353,7 +363,7 @@ export class Track extends React.Component<IProps> { const originalVal = this.props.node[key]; doc[key] = originalVal instanceof ObjectField ? originalVal[Copy]() : originalVal; }); - } + }; /** * UI sstuff here. Not really much to change @@ -362,10 +372,13 @@ export class Track extends React.Component<IProps> { return ( <div className="track-container"> <div className="track"> - <div className="inner" ref={this._inner} style={{ height: `${this._trackHeight}px` }} + <div + className="inner" + ref={this._inner} + style={{ height: `${this._trackHeight}px` }} onDoubleClick={this.onInnerDoubleClick} onPointerOver={() => Doc.BrushDoc(this.props.node)} - onPointerOut={() => Doc.UnBrushDoc(this.props.node)} > + onPointerOut={() => Doc.UnBrushDoc(this.props.node)}> {this.regions?.map((region, i) => { return <Keyframe key={`${i}`} {...this.props} RegionData={region} makeKeyData={this.makeKeyData} />; })} @@ -374,4 +387,4 @@ export class Track extends React.Component<IProps> { </div> ); } -}
\ No newline at end of file +} diff --git a/src/client/views/collections/CollectionCarousel3DView.scss b/src/client/views/collections/CollectionCarousel3DView.scss index 5c8b491eb..6bd1d9f5f 100644 --- a/src/client/views/collections/CollectionCarousel3DView.scss +++ b/src/client/views/collections/CollectionCarousel3DView.scss @@ -1,3 +1,4 @@ +@import '../global/globalCssVariables'; .collectionCarousel3DView-outer { height: 100%; position: relative; @@ -7,8 +8,8 @@ .carousel-wrapper { display: flex; position: absolute; - top: 15%; - height: 60%; + top: $CAROUSEL3D_TOP * 1%; + height: ($CAROUSEL3D_SIDE_SCALE * 100) * 1%; align-items: center; transition: transform 0.3s cubic-bezier(0.455, 0.03, 0.515, 0.955); @@ -17,10 +18,17 @@ flex: 1; transition: opacity 0.3s linear, transform 0.5s cubic-bezier(0.455, 0.03, 0.515, 0.955); pointer-events: none; + opacity: 0.5; + z-index: 1; + transform: scale($CAROUSEL3D_SIDE_SCALE); + user-select: none; } .collectionCarousel3DView-item-active { pointer-events: unset; + opacity: 1; + z-index: 2; + transform: scale($CAROUSEL3D_CENTER_SCALE); } } @@ -105,4 +113,4 @@ .carousel3DView-back-scroll:hover, .carousel3DView-fwd-scroll:hover { background: lightgray; -}
\ No newline at end of file +} diff --git a/src/client/views/collections/CollectionCarousel3DView.tsx b/src/client/views/collections/CollectionCarousel3DView.tsx index a266c9207..d94e552b4 100644 --- a/src/client/views/collections/CollectionCarousel3DView.tsx +++ b/src/client/views/collections/CollectionCarousel3DView.tsx @@ -7,6 +7,8 @@ import { Id } from '../../../fields/FieldSymbols'; import { NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { returnFalse, returnZero, Utils } from '../../../Utils'; import { DragManager } from '../../util/DragManager'; +import { SelectionManager } from '../../util/SelectionManager'; +import { CAROUSEL3D_CENTER_SCALE, CAROUSEL3D_SIDE_SCALE, CAROUSEL3D_TOP } from '../global/globalCssVariables.scss'; import { DocumentView } from '../nodes/DocumentView'; import { StyleProp } from '../StyleProvider'; import './CollectionCarousel3DView.scss'; @@ -32,27 +34,34 @@ export class CollectionCarousel3DView extends CollectionSubView() { } }; + centerScale = Number(CAROUSEL3D_CENTER_SCALE); panelWidth = () => this.props.PanelWidth() / 3; - panelHeight = () => this.props.PanelHeight() * 0.6; + panelHeight = () => this.props.PanelHeight() * Number(CAROUSEL3D_SIDE_SCALE); onChildDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick); isContentActive = () => this.props.isSelected() || this.props.isContentActive() || this.props.isAnyChildContentActive(); isChildContentActive = () => (this.isContentActive() ? true : false); + childScreenToLocal = () => + this.props // document's left is the panel shifted by the doc's index * panelWidth/#docs. But it scales by centerScale around its center, so it's left moves left by the distance of the left from the center (panelwidth/2) * the scale delta (centerScale-1) + .ScreenToLocalTransform() // the top behaves the same way ecept it's shifted by the 'top' amount specified for the panel in css and then by the scale factor. + .translate(-this.panelWidth() + ((this.centerScale - 1) * this.panelWidth()) / 2, -((Number(CAROUSEL3D_TOP) / 100) * this.props.PanelHeight()) + ((this.centerScale - 1) * this.panelHeight()) / 2) + .scale(1 / this.centerScale); @computed get content() { - const currentIndex = NumCast(this.layoutDoc._itemIndex); + const currentIndex = NumCast(this.layoutDoc._carousel_index); const displayDoc = (childPair: { layout: Doc; data: Doc }) => { return ( <DocumentView {...this.props} NativeWidth={returnZero} NativeHeight={returnZero} - suppressSetHeight={true} + //suppressSetHeight={true} onDoubleClick={this.onChildDoubleClick} renderDepth={this.props.renderDepth + 1} LayoutTemplate={this.props.childLayoutTemplate} LayoutTemplateString={this.props.childLayoutString} Document={childPair.layout} DataDoc={childPair.data} + ScreenToLocalTransform={this.childScreenToLocal} isContentActive={this.isChildContentActive} isDocumentActive={this.props.childDocumentsActive?.() || this.Document._childDocumentsActive ? this.props.isDocumentActive : this.isContentActive} PanelWidth={this.panelWidth} @@ -64,10 +73,7 @@ export class CollectionCarousel3DView extends CollectionSubView() { return this.childLayoutPairs.map((childPair, index) => { return ( - <div - key={childPair.layout[Id]} - className={`collectionCarousel3DView-item${index === currentIndex ? '-active' : ''} ${index}`} - style={index === currentIndex ? { opacity: '1', transform: 'scale(1.3)', width: this.panelWidth() } : { opacity: '0.5', transform: 'scale(0.6)', userSelect: 'none', width: this.panelWidth() }}> + <div key={childPair.layout[Id]} className={`collectionCarousel3DView-item${index === currentIndex ? '-active' : ''} ${index}`} style={{ width: this.panelWidth() }}> {displayDoc(childPair)} </div> ); @@ -75,7 +81,8 @@ export class CollectionCarousel3DView extends CollectionSubView() { } changeSlide = (direction: number) => { - this.layoutDoc._itemIndex = (NumCast(this.layoutDoc._itemIndex) + direction + this.childLayoutPairs.length) % this.childLayoutPairs.length; + SelectionManager.DeselectAll(); + this.layoutDoc._carousel_index = (NumCast(this.layoutDoc._carousel_index) + direction + this.childLayoutPairs.length) % this.childLayoutPairs.length; }; onArrowClick = (e: React.MouseEvent, direction: number) => { @@ -114,10 +121,10 @@ export class CollectionCarousel3DView extends CollectionSubView() { return ( <div className="arrow-buttons"> <div key="back" className="carousel3DView-back" style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }} onClick={e => this.onArrowClick(e, -1)}> - <FontAwesomeIcon icon={'angle-left'} size={'2x'} /> + <FontAwesomeIcon icon="angle-left" size={'2x'} /> </div> <div key="fwd" className="carousel3DView-fwd" style={{ background: `${StrCast(this.props.Document.backgroundColor)}` }} onClick={e => this.onArrowClick(e, 1)}> - <FontAwesomeIcon icon={'angle-right'} size={'2x'} /> + <FontAwesomeIcon icon="angle-right" size={'2x'} /> </div> {this.autoScrollButton} </div> @@ -139,13 +146,14 @@ export class CollectionCarousel3DView extends CollectionSubView() { } @computed get dots() { - return this.childLayoutPairs.map((_child, index) => <div key={Utils.GenerateGuid()} className={`dot${index === NumCast(this.layoutDoc._itemIndex) ? '-active' : ''}`} onClick={() => (this.layoutDoc._itemIndex = index)} />); + return this.childLayoutPairs.map((_child, index) => <div key={Utils.GenerateGuid()} className={`dot${index === NumCast(this.layoutDoc._carousel_index) ? '-active' : ''}`} onClick={() => (this.layoutDoc._carousel_index = index)} />); + } + @computed get translateX() { + const index = NumCast(this.layoutDoc._carousel_index); + return this.panelWidth() * (1 - index); } render() { - const index = NumCast(this.layoutDoc._itemIndex); - const translateX = this.panelWidth() * (1 - index); - return ( <div className="collectionCarousel3DView-outer" @@ -154,7 +162,7 @@ export class CollectionCarousel3DView extends CollectionSubView() { background: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor), color: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color), }}> - <div className="carousel-wrapper" style={{ transform: `translateX(${translateX}px)` }}> + <div className="carousel-wrapper" style={{ transform: `translateX(${this.translateX}px)` }}> {this.content} </div> {this.props.Document._chromeHidden ? null : this.buttons} diff --git a/src/client/views/collections/CollectionCarouselView.tsx b/src/client/views/collections/CollectionCarouselView.tsx index 0e4556eb4..0eb61a0b2 100644 --- a/src/client/views/collections/CollectionCarouselView.tsx +++ b/src/client/views/collections/CollectionCarouselView.tsx @@ -4,7 +4,7 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, Opt } from '../../../fields/Doc'; import { NumCast, ScriptCast, StrCast } from '../../../fields/Types'; -import { emptyFunction, returnFalse, returnZero } from '../../../Utils'; +import { returnFalse, returnZero, StopEvent } from '../../../Utils'; import { DragManager } from '../../util/DragManager'; import { DocumentView, DocumentViewProps } from '../nodes/DocumentView'; import { FormattedTextBox } from '../nodes/formattedText/FormattedTextBox'; @@ -30,27 +30,27 @@ export class CollectionCarouselView extends CollectionSubView() { advance = (e: React.MouseEvent) => { e.stopPropagation(); - this.layoutDoc._itemIndex = (NumCast(this.layoutDoc._itemIndex) + 1) % this.childLayoutPairs.length; + this.layoutDoc._carousel_index = (NumCast(this.layoutDoc._carousel_index) + 1) % this.childLayoutPairs.length; }; goback = (e: React.MouseEvent) => { e.stopPropagation(); - this.layoutDoc._itemIndex = (NumCast(this.layoutDoc._itemIndex) - 1 + this.childLayoutPairs.length) % this.childLayoutPairs.length; + this.layoutDoc._carousel_index = (NumCast(this.layoutDoc._carousel_index) - 1 + this.childLayoutPairs.length) % this.childLayoutPairs.length; }; captionStyleProvider = (doc: Doc | undefined, captionProps: Opt<DocumentViewProps>, property: string): any => { // first look for properties on the document in the carousel, then fallback to properties on the container const childValue = doc?.['caption-' + property] ? this.props.styleProvider?.(doc, captionProps, property) : undefined; return childValue ?? this.props.styleProvider?.(this.layoutDoc, captionProps, property); }; - panelHeight = () => this.props.PanelHeight() - (StrCast(this.layoutDoc._showCaption) ? 50 : 0); + panelHeight = () => this.props.PanelHeight() - (StrCast(this.layoutDoc._layout_showCaption) ? 50 : 0); onContentDoubleClick = () => ScriptCast(this.layoutDoc.onChildDoubleClick); onContentClick = () => ScriptCast(this.layoutDoc.onChildClick); @computed get content() { - const index = NumCast(this.layoutDoc._itemIndex); + const index = NumCast(this.layoutDoc._carousel_index); const curDoc = this.childLayoutPairs?.[index]; const captionProps = { ...this.props, fieldKey: 'caption', setHeight: undefined }; - const marginX = NumCast(this.layoutDoc['caption-xMargin']); - const marginY = NumCast(this.layoutDoc['caption-yMargin']); - const showCaptions = StrCast(this.layoutDoc._showCaption); + const marginX = NumCast(this.layoutDoc['caption_xMargin']); + const marginY = NumCast(this.layoutDoc['caption_yMargin']); + const show_captions = StrCast(this.layoutDoc._layout_showCaption); return !(curDoc?.layout instanceof Doc) ? null : ( <> <div className="collectionCarouselView-image" key="image"> @@ -60,7 +60,7 @@ export class CollectionCarouselView extends CollectionSubView() { NativeHeight={returnZero} onDoubleClick={this.onContentDoubleClick} onClick={this.onContentClick} - hideCaptions={showCaptions ? true : false} + hideCaptions={show_captions ? true : false} renderDepth={this.props.renderDepth + 1} LayoutTemplate={this.props.childLayoutTemplate} LayoutTemplateString={this.props.childLayoutString} @@ -73,14 +73,15 @@ export class CollectionCarouselView extends CollectionSubView() { <div className="collectionCarouselView-caption" key="caption" + onWheel={StopEvent} style={{ - display: showCaptions ? undefined : 'none', + display: show_captions ? undefined : 'none', borderRadius: this.props.styleProvider?.(this.layoutDoc, captionProps, StyleProp.BorderRounding), marginRight: marginX, marginLeft: marginX, width: `calc(100% - ${marginX * 2}px)`, }}> - <FormattedTextBox key={index} {...captionProps} fieldKey={showCaptions} styleProvider={this.captionStyleProvider} Document={curDoc.layout} DataDoc={undefined} /> + <FormattedTextBox key={index} {...captionProps} allowScroll={true} fieldKey={show_captions} styleProvider={this.captionStyleProvider} Document={curDoc.layout} DataDoc={undefined} /> </div> </> ); diff --git a/src/client/views/collections/CollectionDockingView.tsx b/src/client/views/collections/CollectionDockingView.tsx index bb1f788d4..e9cc2c894 100644 --- a/src/client/views/collections/CollectionDockingView.tsx +++ b/src/client/views/collections/CollectionDockingView.tsx @@ -6,7 +6,7 @@ import { Doc, DocListCast, Opt } from '../../../fields/Doc'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; -import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types'; +import { ImageCast, NumCast, StrCast } from '../../../fields/Types'; import { ImageField } from '../../../fields/URLField'; import { inheritParentAcls } from '../../../fields/util'; import { emptyFunction, incrementTitleCopy } from '../../../Utils'; @@ -20,14 +20,15 @@ import { SelectionManager } from '../../util/SelectionManager'; import { undoBatch, UndoManager } from '../../util/UndoManager'; import { DashboardView } from '../DashboardView'; import { LightboxView } from '../LightboxView'; +import { OpenWhere, OpenWhereMod } from '../nodes/DocumentView'; +import { OverlayView } from '../OverlayView'; +import { ScriptingRepl } from '../ScriptingRepl'; +import { UndoStack } from '../UndoStack'; import './CollectionDockingView.scss'; import { CollectionFreeFormView } from './collectionFreeForm'; import { CollectionSubView, SubCollectionViewProps } from './CollectionSubView'; import { TabDocView } from './TabDocView'; import React = require('react'); -import { OpenWhere, OpenWhereMod } from '../nodes/DocumentView'; -import { OverlayView } from '../OverlayView'; -import { ScriptingRepl } from '../ScriptingRepl'; const _global = (window /* browser */ || global) /* node */ as any; @observer @@ -85,6 +86,7 @@ export class CollectionDockingView extends CollectionSubView() { tabItemDropped = () => DragManager.CompleteWindowDrag?.(false); tabDragStart = (proxy: any, finishDrag?: (aborted: boolean) => void) => { + this._flush = this._flush ?? UndoManager.StartBatch('tab move'); const dashDoc = proxy?._contentItem?.tab?.DashDoc as Doc; dashDoc && (DragManager.DocDragData = new DragManager.DocumentDragData([proxy._contentItem.tab.DashDoc])); DragManager.CompleteWindowDrag = (aborted: boolean) => { @@ -92,12 +94,12 @@ export class CollectionDockingView extends CollectionSubView() { proxy._dragListener.AbortDrag(); if (this._flush) { this._flush.cancel(); // cancel the undo change being logged - this._flush = undefined; this.setupGoldenLayout(); // restore golden layout to where it was before the drag (this is a no-op when using StartOtherDrag because the proxy dragged item was never in the golden layout) } DragManager.CompleteWindowDrag = undefined; } finishDrag?.(aborted); + setTimeout(this.endUndoBatch, 100); }; }; @undoBatch @@ -127,12 +129,12 @@ export class CollectionDockingView extends CollectionSubView() { SelectionManager.DeselectAll(); const instance = CollectionDockingView.Instance; if (instance) { - if (doc._viewType === CollectionViewType.Docking && doc.layoutKey === 'layout') { + if (doc._type_collection === CollectionViewType.Docking && doc.layout_fieldKey === 'layout') { return DashboardView.openDashboard(doc); } const newItemStackConfig = { type: 'stack', - content: [CollectionDockingView.makeDocumentConfig(Doc.MakeAlias(doc))], + content: [CollectionDockingView.makeDocumentConfig(Doc.MakeEmbedding(doc))], }; const docconfig = instance._goldenLayout.root.layoutManager.createContentItem(newItemStackConfig, instance._goldenLayout); instance._goldenLayout.root.contentItems[0].addChild(docconfig); @@ -180,10 +182,9 @@ export class CollectionDockingView extends CollectionSubView() { // // Creates a split on any side of the docking view based on the passed input pullSide and then adds the Document to the requested side // - @undoBatch @action public static AddSplit(document: Doc, pullSide: OpenWhereMod, stack?: any, panelName?: string, keyValue?: boolean) { - if (document?._viewType === CollectionViewType.Docking) return DashboardView.openDashboard(document); + if (document?._type_collection === CollectionViewType.Docking) return DashboardView.openDashboard(document); if (!CollectionDockingView.Instance) return false; const tab = Array.from(CollectionDockingView.Instance.tabMap).find(tab => tab.DashDoc === document && !keyValue); if (tab) { @@ -195,6 +196,8 @@ export class CollectionDockingView extends CollectionSubView() { if (!instance) return false; const docContentConfig = CollectionDockingView.makeDocumentConfig(document, panelName, undefined, keyValue); + CollectionDockingView.Instance._flush = CollectionDockingView.Instance._flush ?? UndoManager.StartBatch('Add Split'); + setTimeout(CollectionDockingView.Instance.endUndoBatch, 100); if (!pullSide && stack) { stack.addChild(docContentConfig, undefined); setTimeout(() => stack.setActiveContentItem(stack.contentItems[stack.contentItems.length - 1])); @@ -370,16 +373,30 @@ export class CollectionDockingView extends CollectionSubView() { !LightboxView.LightboxDoc && cur && this._goldenLayout?.updateSize(cur.getBoundingClientRect().width, cur.getBoundingClientRect().height); }; + endUndoBatch = () => { + const json = JSON.stringify(this._goldenLayout.toConfig()); + const matches = json.match(/\"documentId\":\"[a-z0-9-]+\"/g); + const docids = matches?.map(m => m.replace('"documentId":"', '').replace('"', '')); + const docs = !docids + ? [] + : docids + .map(id => DocServer.GetCachedRefField(id)) + .filter(f => f) + .map(f => f as Doc); + const changesMade = this.props.Document.dockingConfig !== json; + if (changesMade) { + this.props.Document.dockingConfig = json; + this.props.Document.data = new List<Doc>(docs); + } + this._flush?.end(); + this._flush = undefined; + }; + @action onPointerUp = (e: MouseEvent): void => { window.removeEventListener('pointerup', this.onPointerUp); - const flush = this._flush; - this._flush = undefined; - if (flush) { - DragManager.CompleteWindowDrag = undefined; - if (!this.stateChanged()) flush.cancel(); - else flush.end(); - } + DragManager.CompleteWindowDrag = undefined; + setTimeout(this.endUndoBatch, 100); }; @action @@ -393,10 +410,8 @@ export class CollectionDockingView extends CollectionSubView() { window.addEventListener('mouseup', this.onPointerUp); if (!htmlTarget.closest('*.lm_content') && (htmlTarget.closest('*.lm_tab') || htmlTarget.closest('*.lm_stack'))) { const className = typeof htmlTarget.className === 'string' ? htmlTarget.className : ''; - if (!className.includes('lm_close') && !className.includes('lm_maximise')) { - this._flush = UndoManager.StartBatch('golden layout edit'); - DocServer.UPDATE_SERVER_CACHE(); - } + if (className.includes('lm_maximise')) this._flush = UndoManager.StartBatch('tab maximize'); + else if (!className.includes('lm_close')) DocServer.UPDATE_SERVER_CACHE(); } } if (!InteractionUtils.IsType(e, InteractionUtils.TOUCHTYPE) && !InteractionUtils.IsType(e, InteractionUtils.PENTYPE) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { @@ -411,8 +426,8 @@ export class CollectionDockingView extends CollectionSubView() { const _height = Number(getComputedStyle(content).height.replace('px', '')); return CollectionFreeFormView.UpdateIcon(this.layoutDoc[Id] + '-icon' + new Date().getTime(), content, _width, _height, _width, _height, 0, 1, true, this.layoutDoc[Id] + '-icon', (iconFile, _nativeWidth, _nativeHeight) => { const proto = this.dataDoc; // Cast(img.proto, Doc, null)!; - proto['thumb-nativeWidth'] = _width; - proto['thumb-nativeHeight'] = _height; + proto['thumb_nativeWidth'] = _width; + proto['thumb_nativeHeight'] = _height; proto.thumb = new ImageField(iconFile); }); } @@ -434,11 +449,11 @@ export class CollectionDockingView extends CollectionSubView() { .map(f => f as Doc); const newtabs = origtabs.map(origtab => { const origtabdocs = DocListCast(origtab.data); - const newtab = origtabdocs.length ? Doc.MakeCopy(origtab, true, undefined, true) : Doc.MakeAlias(origtab); - const newtabdocs = origtabdocs.map(origtabdoc => Doc.MakeAlias(origtabdoc)); + const newtab = origtabdocs.length ? Doc.MakeCopy(origtab, true, undefined, true) : Doc.MakeEmbedding(origtab); + const newtabdocs = origtabdocs.map(origtabdoc => Doc.MakeEmbedding(origtabdoc)); if (newtabdocs.length) { Doc.GetProto(newtab).data = new List<Doc>(newtabdocs); - newtabdocs.forEach(ntab => (ntab.context = newtab)); + newtabdocs.forEach(ntab => (ntab.embedContainer = newtab)); } json = json.replace(origtab[Id], newtab[Id]); return newtab; @@ -452,25 +467,12 @@ export class CollectionDockingView extends CollectionSubView() { stateChanged = () => { this._ignoreStateChange = JSON.stringify(this._goldenLayout.toConfig()); const json = JSON.stringify(this._goldenLayout.toConfig()); - const matches = json.match(/\"documentId\":\"[a-z0-9-]+\"/g); - const docids = matches?.map(m => m.replace('"documentId":"', '').replace('"', '')); - const docs = !docids - ? [] - : docids - .map(id => DocServer.GetCachedRefField(id)) - .filter(f => f) - .map(f => f as Doc); const changesMade = this.props.Document.dockingConfig !== json; - if (changesMade && !this._flush) { - UndoManager.RunInBatch(() => { - this.props.Document.dockingConfig = json; - this.props.Document.data = new List<Doc>(docs); - }, 'state changed'); - } return changesMade; }; tabDestroyed = (tab: any) => { + this._flush = this._flush ?? UndoManager.StartBatch('tab movement'); if (tab.DashDoc && ![DocumentType.KVP, DocumentType.PRES].includes(tab.DashDoc?.type)) { Doc.AddDocToList(Doc.MyHeaderBar, 'data', tab.DashDoc); Doc.AddDocToList(Doc.MyRecentlyClosed, 'data', tab.DashDoc, undefined, true, true); @@ -498,8 +500,8 @@ export class CollectionDockingView extends CollectionSubView() { const docToAdd = Docs.Create.FreeformDocument([], { _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), - _backgroundGridShow: true, - _fitWidth: true, + _freeform_backgroundGrid: true, + _layout_fitWidth: true, title: `Untitled Tab ${NumCast(dashboard['pane-count'])}`, }); this.props.Document.isShared && inheritParentAcls(this.props.Document, docToAdd); @@ -541,8 +543,8 @@ export class CollectionDockingView extends CollectionSubView() { const docToAdd = Docs.Create.FreeformDocument([], { _width: this.props.PanelWidth(), _height: this.props.PanelHeight(), - _fitWidth: true, - _backgroundGridShow: true, + _layout_fitWidth: true, + _freeform_backgroundGrid: true, title: `Untitled Tab ${NumCast(dashboard['pane-count'])}`, }); this.props.Document.isShared && inheritParentAcls(this.props.Document, docToAdd); @@ -586,8 +588,12 @@ ScriptingGlobals.add( case OpenWhere.addRight: return CollectionDockingView.AddSplit(doc, OpenWhereMod.right); case OpenWhere.overlay: - if (doc === 'repl') OverlayView.Instance.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200, title: 'Scripting REPL' }); - else Doc.AddDocToList(Doc.MyOverlayDocs, undefined, doc); + // prettier-ignore + switch (doc) { + case '<ScriptingRepl />': return OverlayView.Instance.addWindow(<ScriptingRepl />, { x: 300, y: 100, width: 200, height: 200, title: 'Scripting REPL' }); + case "<UndoStack>": return OverlayView.Instance.addWindow(<UndoStack />, { x: 300, y: 100, width: 200, height: 200, title: 'Scripting REPL' }); + } + Doc.AddToMyOverlay(doc); } }, 'opens up document in location specified', diff --git a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx index befd89e41..64f9c6a87 100644 --- a/src/client/views/collections/CollectionMasonryViewFieldRow.tsx +++ b/src/client/views/collections/CollectionMasonryViewFieldRow.tsx @@ -2,7 +2,8 @@ import React = require('react'); import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; -import { DataSym, Doc, DocListCast } from '../../../fields/Doc'; +import { Doc, DocListCast } from '../../../fields/Doc'; +import { DocData } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { PastelSchemaPalette, SchemaHeaderField } from '../../../fields/SchemaHeaderField'; import { ScriptField } from '../../../fields/ScriptField'; @@ -44,7 +45,7 @@ interface CMVFieldRowProps { @observer export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowProps> { @observable private _background = 'inherit'; - @observable private _createAliasSelected: boolean = false; + @observable private _createEmbeddingSelected: boolean = false; @observable private heading: string = ''; @observable private color: string = '#f1efeb'; @observable private collapsed: boolean = false; @@ -95,7 +96,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr @undoBatch rowDrop = action((e: Event, de: DragManager.DropEvent) => { - this._createAliasSelected = false; + this._createEmbeddingSelected = false; if (de.complete.docDragData) { this.props.parent.Document.dropConverter instanceof ScriptField && this.props.parent.Document.dropConverter.script.run({ dragData: de.complete.docDragData }); const key = this.props.pivotField; @@ -117,12 +118,12 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr @action headingChanged = (value: string, shiftDown?: boolean) => { - this._createAliasSelected = false; + this._createEmbeddingSelected = false; const key = this.props.pivotField; const castedValue = this.getValue(value); if (castedValue) { - if (this.props.parent.columnHeaders) { - if (this.props.parent.columnHeaders.map(i => i.heading).indexOf(castedValue.toString()) > -1) { + if (this.props.parent.colHeaderData) { + if (this.props.parent.colHeaderData.map(i => i.heading).indexOf(castedValue.toString()) > -1) { return false; } } @@ -135,7 +136,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr @action changeColumnColor = (color: string) => { - this._createAliasSelected = false; + this._createEmbeddingSelected = false; this._color = color; }; @@ -143,53 +144,53 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr @action pointerLeaveRow = () => { - this._createAliasSelected = false; + this._createEmbeddingSelected = false; this._background = 'inherit'; }; @action addDocument = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => { if (!value && !forceEmptyNote) return false; - this._createAliasSelected = false; + this._createEmbeddingSelected = false; const key = this.props.pivotField; - const newDoc = Docs.Create.TextDocument('', { _autoHeight: true, _width: 200, _fitWidth: true, title: value }); + const newDoc = Docs.Create.TextDocument('', { _layout_autoHeight: true, _width: 200, _layout_fitWidth: true, title: value }); const onLayoutDoc = this.onLayoutDoc(key); FormattedTextBox.SelectOnLoad = newDoc[Id]; FormattedTextBox.SelectOnLoadChar = value; - (onLayoutDoc ? newDoc : newDoc[DataSym])[key] = this.getValue(this.props.heading); + (onLayoutDoc ? newDoc : newDoc[DocData])[key] = this.getValue(this.props.heading); const docs = this.props.parent.childDocList; return docs ? (docs.splice(0, 0, newDoc) ? true : false) : this.props.parent.props.addDocument?.(newDoc) || false; // should really extend addDocument to specify insertion point (at beginning of list) }; deleteRow = undoBatch( action(() => { - this._createAliasSelected = false; + this._createEmbeddingSelected = false; const key = this.props.pivotField; this.props.docList.forEach(d => Doc.SetInPlace(d, key, undefined, true)); - if (this.props.parent.columnHeaders && this.props.headingObject) { - const index = this.props.parent.columnHeaders.indexOf(this.props.headingObject); - this.props.parent.columnHeaders.splice(index, 1); + if (this.props.parent.colHeaderData && this.props.headingObject) { + const index = this.props.parent.colHeaderData.indexOf(this.props.headingObject); + this.props.parent.colHeaderData.splice(index, 1); } }) ); @action collapseSection = (e: any) => { - this._createAliasSelected = false; + this._createEmbeddingSelected = false; this.toggleVisibility(); e.stopPropagation(); }; headerMove = (e: PointerEvent) => { - const alias = Doc.MakeAlias(this.props.Document); + const embedding = Doc.MakeEmbedding(this.props.Document); const key = this.props.pivotField; let value = this.getValue(this.heading); value = typeof value === 'string' ? `"${value}"` : value; const script = `return doc.${key} === ${value}`; const compiled = CompileScript(script, { params: { doc: Doc.name } }); if (compiled.compiled) { - alias.viewSpecScript = new ScriptField(compiled); - DragManager.StartDocumentDrag([this._headerRef.current!], new DragManager.DocumentDragData([alias]), e.clientX, e.clientY); + embedding.viewSpecScript = new ScriptField(compiled); + DragManager.StartDocumentDrag([this._headerRef.current!], new DragManager.DocumentDragData([embedding]), e.clientX, e.clientY); } return true; }; @@ -198,7 +199,7 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr headerDown = (e: React.PointerEvent<HTMLDivElement>) => { if (e.button === 0 && !e.ctrlKey) { setupMoveUpEvents(this, e, this.headerMove, emptyFunction, e => !this.props.chromeHidden && this.collapseSection(e)); - this._createAliasSelected = false; + this._createEmbeddingSelected = false; } }; @@ -242,16 +243,16 @@ export class CollectionMasonryViewFieldRow extends React.Component<CMVFieldRowPr ); }; - toggleAlias = action(() => (this._createAliasSelected = true)); + toggleEmbedding = action(() => (this._createEmbeddingSelected = true)); toggleVisibility = () => (this._collapsed = !this.collapsed); renderMenu = () => { - const selected = this._createAliasSelected; + const selected = this._createEmbeddingSelected; return ( <div className="collectionStackingView-optionPicker"> <div className="optionOptions"> - <div className={'optionPicker' + (selected === true ? ' active' : '')} onClick={this.toggleAlias}> - Create Alias + <div className={'optionPicker' + (selected === true ? ' active' : '')} onClick={this.toggleEmbedding}> + Create Embedding </div> <div className={'optionPicker' + (selected === true ? ' active' : '')} onClick={this.deleteRow}> Delete diff --git a/src/client/views/collections/CollectionMenu.tsx b/src/client/views/collections/CollectionMenu.tsx index 2154016bd..fc3f713ce 100644 --- a/src/client/views/collections/CollectionMenu.tsx +++ b/src/client/views/collections/CollectionMenu.tsx @@ -14,7 +14,7 @@ import { ObjectField } from '../../../fields/ObjectField'; import { RichTextField } from '../../../fields/RichTextField'; import { listSpec } from '../../../fields/Schema'; import { ScriptField } from '../../../fields/ScriptField'; -import { BoolCast, Cast, NumCast, StrCast } from '../../../fields/Types'; +import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../fields/Types'; import { GestureUtils } from '../../../pen-gestures/GestureUtils'; import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnTrue, setupMoveUpEvents, Utils } from '../../../Utils'; import { Docs } from '../../documents/Documents'; @@ -108,8 +108,8 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> { <CollectionLinearView Document={selDoc} DataDoc={undefined} - fieldKey={'data'} - dropAction={'alias'} + fieldKey="data" + dropAction="embed" setHeight={returnFalse} styleProvider={DefaultStyleProvider} rootSelected={returnTrue} @@ -130,8 +130,8 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> { renderDepth={0} focus={emptyFunction} whenChildContentsActiveChanged={emptyFunction} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} /> </div> @@ -172,7 +172,7 @@ export class CollectionMenu extends AntimodeMenu<CollectionMenuProps> { // [<CollectionViewBaseChrome key="chrome" // docView={this.SelectedCollection} // fieldKey={this.SelectedCollection.LayoutFieldKey} - // type={StrCast(this.SelectedCollection?.props.Document._viewType, CollectionViewType.Invalid) as CollectionViewType} />, + // type={StrCast(this.SelectedCollection?.props.Document._type_collection, CollectionViewType.Invalid) as CollectionViewType} />, // prop, // /*button*/]); } @@ -246,47 +246,47 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu _viewCommand = { params: ['target'], title: 'bookmark view', - script: "self.target._panX = self['target-panX']; self.target._panY = self['target-panY']; self.target._viewScale = self['target-viewScale']; gotoFrame(self.target, self['target-currentFrame']);", + script: "self.target._freeform_panX = self['target-freeform_panX']; self.target._freeform_panY = self['target-freeform_panY']; self.target._freeform_scale = self['target_freeform_scale']; gotoFrame(self.target, self['target-currentFrame']);", immediate: undoBatch((source: Doc[]) => { - this.target._panX = 0; - this.target._panY = 0; - this.target._viewScale = 1; + this.target._freeform_panX = 0; + this.target._freeform_panY = 0; + this.target._freeform_scale = 1; this.target._currentFrame = this.target._currentFrame === undefined ? undefined : 0; }), initialize: (button: Doc) => { - button['target-panX'] = this.target._panX; - button['target-panY'] = this.target._panY; - button['target-viewScale'] = this.target._viewScale; + button['target-panX'] = this.target._freeform_panX; + button['target-panY'] = this.target._freeform_panY; + button['target-_ayout_scale'] = this.target._freeform_scale; button['target-currentFrame'] = this.target._currentFrame; }, }; _clusterCommand = { params: ['target'], title: 'fit content', - script: 'self.target._fitContentsToBox = !self.target._fitContentsToBox;', - immediate: undoBatch((source: Doc[]) => (this.target._fitContentsToBox = !this.target._fitContentsToBox)), + script: 'self.target._freeform_fitContentsToBox = !self.target._freeform_fitContentsToBox;', + immediate: undoBatch((source: Doc[]) => (this.target._freeform_fitContentsToBox = !this.target._freeform_fitContentsToBox)), initialize: emptyFunction, }; _fitContentCommand = { params: ['target'], title: 'toggle clusters', - script: 'self.target._useClusters = !self.target._useClusters;', - immediate: undoBatch((source: Doc[]) => (this.target._useClusters = !this.target._useClusters)), + script: 'self.target._freeform_useClusters = !self.target._freeform_useClusters;', + immediate: undoBatch((source: Doc[]) => (this.target._freeform_useClusters = !this.target._freeform_useClusters)), initialize: emptyFunction, }; _saveFilterCommand = { params: ['target'], title: 'save filter', - script: `self.target._docFilters = compareLists(self['target-docFilters'],self.target._docFilters) ? undefined : copyField(self['target-docFilters']); + script: `self.target._childFilters = compareLists(self['target-childFilters'],self.target._childFilters) ? undefined : copyField(self['target-childFilters']); self.target._searchFilterDocs = compareLists(self['target-searchFilterDocs'],self.target._searchFilterDocs) ? undefined: copyField(self['target-searchFilterDocs']);`, immediate: undoBatch((source: Doc[]) => { - this.target._docFilters = undefined; + this.target._childFilters = undefined; this.target._searchFilterDocs = undefined; }), initialize: (button: Doc) => { const activeDash = Doc.ActiveDashboard; if (activeDash) { - button['target-docFilters'] = (Doc.MySearcher._docFilters || activeDash._docFilters) instanceof ObjectField ? ObjectField.MakeCopy((Doc.MySearcher._docFilters || activeDash._docFilters) as any as ObjectField) : undefined; + button['target-childFilters'] = (Doc.MySearcher._childFilters || activeDash._childFilters) instanceof ObjectField ? ObjectField.MakeCopy((Doc.MySearcher._childFilters || activeDash._childFilters) as any as ObjectField) : undefined; button['target-searchFilterDocs'] = activeDash._searchFilterDocs instanceof ObjectField ? ObjectField.MakeCopy(activeDash._searchFilterDocs as any as ObjectField) : undefined; } }, @@ -347,9 +347,9 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu @undoBatch viewChanged = (e: React.ChangeEvent) => { - const target = this.document !== Doc.MyLeftSidebarPanel ? this.document : (this.document.proto as Doc); + const target = this.document !== Doc.MyLeftSidebarPanel ? this.document : DocCast(this.document.proto); //@ts-ignore - target._viewType = e.target.selectedOptions[0].value; + target._type_collection = e.target.selectedOptions[0].value; }; commandChanged = (e: React.ChangeEvent) => { @@ -440,8 +440,8 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu const c = { params: ['target'], title: vtype, - script: `this.target._viewType = '${StrCast(this.props.type)}'`, - immediate: (source: Doc[]) => (this.document._viewType = Doc.getDocTemplate(source?.[0])), + script: `this.target._type_collection = '${StrCast(this.props.type)}'`, + immediate: (source: Doc[]) => (this.document._type_collection = Doc.getDocTemplate(source?.[0])), initialize: emptyFunction, }; DragManager.StartButtonDrag([this._viewRef.current!], c.script, StrCast(c.title), { target: this.document }, c.params, c.initialize, e.clientX, e.clientY); @@ -548,8 +548,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu @undoBatch @action startRecording = () => { - const doc = Docs.Create.ScreenshotDocument({ title: 'screen recording', _fitWidth: true, _width: 400, _height: 200, mediaState: 'pendingRecording' }); - //Doc.AddDocToList(Doc.MyOverlayDocs, undefined, doc); + const doc = Docs.Create.ScreenshotDocument({ title: 'screen recording', _layout_fitWidth: true, _width: 400, _height: 200, mediaState: 'pendingRecording' }); CollectionDockingView.AddSplit(doc, OpenWhereMod.right); }; @@ -568,29 +567,29 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu } @undoBatch - onAlias = () => { + onEmbed = () => { if (this.selectedDoc && this.selectedDocumentView) { // const copy = Doc.MakeCopy(this.selectedDocumentView.props.Document, true); // copy.x = NumCast(this.selectedDoc.x) + NumCast(this.selectedDoc._width); // copy.y = NumCast(this.selectedDoc.y) + 30; // this.selectedDocumentView.props.addDocument?.(copy); - const alias = Doc.MakeAlias(this.selectedDoc); - alias.x = NumCast(this.selectedDoc.x) + NumCast(this.selectedDoc._width); - alias.y = NumCast(this.selectedDoc.y) + 30; - this.selectedDocumentView.props.addDocument?.(alias); + const embedding = Doc.MakeEmbedding(this.selectedDoc); + embedding.x = NumCast(this.selectedDoc.x) + NumCast(this.selectedDoc._width); + embedding.y = NumCast(this.selectedDoc.y) + 30; + this.selectedDocumentView.props.addDocument?.(embedding); } }; - onAliasButtonDown = (e: React.PointerEvent): void => { - setupMoveUpEvents(this, e, this.onAliasButtonMoved, emptyFunction, emptyFunction); + onEmbedButtonDown = (e: React.PointerEvent): void => { + setupMoveUpEvents(this, e, this.onEmbedButtonMoved, emptyFunction, emptyFunction); }; @undoBatch - onAliasButtonMoved = (e: PointerEvent) => { + onEmbedButtonMoved = (e: PointerEvent) => { const contentDiv = this.selectedDocumentView?.ContentDiv; if (contentDiv && this.selectedDoc) { const dragData = new DragManager.DocumentDragData([this.selectedDoc]); const offset = [e.clientX - contentDiv.getBoundingClientRect().x, e.clientY - contentDiv.getBoundingClientRect().y]; - dragData.defaultDropAction = 'alias'; + dragData.defaultDropAction = 'embed'; dragData.canEmbed = true; DragManager.StartDocumentDrag([contentDiv], dragData, e.clientX, e.clientY, { offsetX: offset[0], @@ -603,11 +602,11 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu }; @computed - get aliasButton() { + get embedButton() { const targetDoc = this.selectedDoc; return !targetDoc || targetDoc.type === DocumentType.PRES ? null : ( - <Tooltip title={<div className="dash-tooltip">{'Tap or Drag to create an alias'}</div>} placement="top"> - <button className="antimodeMenu-button" onPointerDown={this.onAliasButtonDown} onClick={this.onAlias} style={{ cursor: 'drag' }}> + <Tooltip title={<div className="dash-tooltip">{'Tap or Drag to create an embedding'}</div>} placement="top"> + <button className="antimodeMenu-button" onPointerDown={this.onEmbedButtonDown} onClick={this.onEmbed} style={{ cursor: 'drag' }}> <FontAwesomeIcon className="colMenu-icon" icon="copy" size="lg" /> </button> </Tooltip> @@ -639,8 +638,8 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu key="float" style={{ backgroundColor: this.props.docView.layoutDoc.z ? '121212' : undefined, - pointerEvents: this.props.docView.props.docViewPath().lastElement()?.rootDoc?._viewType !== CollectionViewType.Freeform ? 'none' : undefined, - color: this.props.docView.props.docViewPath().lastElement()?.rootDoc?._viewType !== CollectionViewType.Freeform ? 'dimgrey' : undefined, + pointerEvents: this.props.docView.props.docViewPath().lastElement()?.rootDoc?._type_collection !== CollectionViewType.Freeform ? 'none' : undefined, + color: this.props.docView.props.docViewPath().lastElement()?.rootDoc?._type_collection !== CollectionViewType.Freeform ? 'dimgrey' : undefined, }} onClick={undoBatch(() => this.props.docView.props.CollectionFreeFormDocumentView?.().float())}> <FontAwesomeIcon icon={['fab', 'buffer']} size={'lg'} /> @@ -657,7 +656,7 @@ export class CollectionViewBaseChrome extends React.Component<CollectionViewMenu <div className="collectionViewBaseChrome"> {this.notACollection || this.props.type === CollectionViewType.Invalid ? null : this.viewModes} <div className="collectionMenu-divider" key="divider1"></div> - {this.aliasButton} + {this.embedButton} {/* {this.pinButton} */} {this.toggleOverlayButton} <div className="collectionMenu-divider" key="divider2"></div> @@ -691,7 +690,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView return this.props.docView.props.Document; } @computed get dataField() { - return this.document[this.props.docView.LayoutFieldKey + (this.props.isOverlay ? '-annotations' : '')]; + return this.document[this.props.docView.LayoutFieldKey + (this.props.isOverlay ? '_annotations' : '')]; } @computed get childDocs() { return DocListCast(this.dataField); @@ -798,7 +797,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView if (doc.type === DocumentType.INK) { switch (field) { case 'width': - doc.strokeWidth = Number(value); + doc.stroke_width = Number(value); break; case 'color': doc.color = String(value); @@ -807,7 +806,7 @@ export class CollectionFreeFormViewChrome extends React.Component<CollectionView doc.fillColor = String(value); break; case 'dash': - doc.strokeDash = value; + doc.stroke_dash = value; } } }) @@ -1005,16 +1004,13 @@ export class CollectionStackingViewChrome extends React.Component<CollectionView if (Doc.noviceMode) { if (docs instanceof Doc) { - const keys = Object.keys(docs).filter(key => key.indexOf('title') >= 0 || key.indexOf('author') >= 0 || key.indexOf('creationDate') >= 0 || key.indexOf('lastModified') >= 0 || (key[0].toUpperCase() === key[0] && key[0] !== '_')); + const keys = Object.keys(docs).filter(key => key.indexOf('title') >= 0 || key.indexOf('author') >= 0 || key.indexOf('author_date') >= 0 || key.indexOf('modificationDate') >= 0 || (key[0].toUpperCase() === key[0] && key[0] !== '_')); return keys.filter(key => key.toLowerCase().indexOf(val) > -1); - } else { - const keys = new Set<string>(); - docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key))); - const noviceKeys = Array.from(keys).filter( - key => key.indexOf('title') >= 0 || key.indexOf('author') >= 0 || key.indexOf('creationDate') >= 0 || key.indexOf('lastModified') >= 0 || (key[0]?.toUpperCase() === key[0] && key[0] !== '_') - ); - return noviceKeys.filter(key => key.toLowerCase().indexOf(val) > -1); } + const keys = new Set<string>(); + docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key))); + const noviceKeys = Array.from(keys).filter(key => key.indexOf('title') >= 0 || key.indexOf('author') >= 0 || key.indexOf('author_date') >= 0 || key.indexOf('modificationDate') >= 0 || (key[0]?.toUpperCase() === key[0] && key[0] !== '_')); + return noviceKeys.filter(key => key.toLowerCase().indexOf(val) > -1); } if (docs instanceof Doc) { @@ -1125,13 +1121,13 @@ export class CollectionNoteTakingViewChrome extends React.Component<CollectionVi if (Doc.UserDoc().noviceMode) { if (docs instanceof Doc) { - const keys = Object.keys(docs).filter(key => key.indexOf('title') >= 0 || key.indexOf('author') >= 0 || key.indexOf('creationDate') >= 0 || key.indexOf('lastModified') >= 0 || (key[0].toUpperCase() === key[0] && key[0] !== '_')); + const keys = Object.keys(docs).filter(key => key.indexOf('title') >= 0 || key.indexOf('author') >= 0 || key.indexOf('author_date') >= 0 || key.indexOf('modificationDate') >= 0 || (key[0].toUpperCase() === key[0] && key[0] !== '_')); return keys.filter(key => key.toLowerCase().indexOf(val) > -1); } else { const keys = new Set<string>(); docs.forEach(doc => Doc.allKeys(doc).forEach(key => keys.add(key))); const noviceKeys = Array.from(keys).filter( - key => key.indexOf('title') >= 0 || key.indexOf('author') >= 0 || key.indexOf('creationDate') >= 0 || key.indexOf('lastModified') >= 0 || (key[0]?.toUpperCase() === key[0] && key[0] !== '_') + key => key.indexOf('title') >= 0 || key.indexOf('author') >= 0 || key.indexOf('author_date') >= 0 || key.indexOf('modificationDate') >= 0 || (key[0]?.toUpperCase() === key[0] && key[0] !== '_') ); return noviceKeys.filter(key => key.toLowerCase().indexOf(val) > -1); } @@ -1235,9 +1231,9 @@ export class CollectionSchemaViewChrome extends React.Component<CollectionViewMe const dividerWidth = 4; const borderWidth = Number(COLLECTION_BORDER_WIDTH); const panelWidth = this.props.docView.props.PanelWidth(); - const previewWidth = NumCast(this.document.schemaPreviewWidth); + const previewWidth = NumCast(this.document.schema_previewWidth); const tableWidth = panelWidth - 2 * borderWidth - dividerWidth - previewWidth; - this.document.schemaPreviewWidth = previewWidth === 0 ? Math.min(tableWidth / 3, 200) : 0; + this.document.schema_previewWidth = previewWidth === 0 ? Math.min(tableWidth / 3, 200) : 0; }; @undoBatch @@ -1254,7 +1250,7 @@ export class CollectionSchemaViewChrome extends React.Component<CollectionViewMe }; render() { - const previewWidth = NumCast(this.document.schemaPreviewWidth); + const previewWidth = NumCast(this.document.schema_previewWidth); const textWrapped = Cast(this.document.textwrappedSchemaRows, listSpec('string'), []).length > 0; return ( diff --git a/src/client/views/collections/CollectionNoteTakingView.tsx b/src/client/views/collections/CollectionNoteTakingView.tsx index 99d4d0bee..675f23970 100644 --- a/src/client/views/collections/CollectionNoteTakingView.tsx +++ b/src/client/views/collections/CollectionNoteTakingView.tsx @@ -2,7 +2,8 @@ import React = require('react'); import { CursorProperty } from 'csstype'; import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; -import { DataSym, Doc, Field, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; +import { Doc, Field, Opt } from '../../../fields/Doc'; +import { DocData, Height, Width } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; @@ -49,12 +50,12 @@ export class CollectionNoteTakingView extends CollectionSubView() { return BoolCast(this.layoutDoc.chromeHidden); } // columnHeaders returns the list of SchemaHeaderFields currently being used by the layout doc to render the columns - @computed get columnHeaders() { - const columnHeaders = Cast(this.dataDoc.columnHeaders, listSpec(SchemaHeaderField), null); - const needsUnsetCategory = this.childDocs.some(d => !d[this.notetakingCategoryField] && !columnHeaders?.find(sh => sh.heading === 'unset')); - if (needsUnsetCategory || columnHeaders === undefined || columnHeaders.length === 0) { + @computed get colHeaderData() { + const colHeaderData = Cast(this.dataDoc[this.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null); + const needsUnsetCategory = this.childDocs.some(d => !d[this.notetakingCategoryField] && !colHeaderData?.find(sh => sh.heading === 'unset')); + if (needsUnsetCategory || colHeaderData === undefined || colHeaderData.length === 0) { setTimeout(() => { - const columnHeaders = Array.from(Cast(this.dataDoc.columnHeaders, listSpec(SchemaHeaderField), null) ?? []); + const columnHeaders = Array.from(Cast(this.dataDoc[this.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null) ?? []); const needsUnsetCategory = this.childDocs.some(d => !d[this.notetakingCategoryField] && !columnHeaders?.find(sh => sh.heading === 'unset')); if (needsUnsetCategory || columnHeaders.length === 0) { columnHeaders.push(new SchemaHeaderField('unset', undefined, undefined, 1)); @@ -62,7 +63,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { } }); } - return columnHeaders ?? ([] as SchemaHeaderField[]); + return colHeaderData ?? ([] as SchemaHeaderField[]); } @computed get headerMargin() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin); @@ -78,7 +79,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { } // numGroupColumns returns the number of columns @computed get numGroupColumns() { - return this.columnHeaders.length; + return this.colHeaderData.length; } // PanelWidth returns the size of the total available space the view occupies @computed get PanelWidth() { @@ -117,7 +118,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { // (2) documentView gets unmounted as you remove it from the list @computed get Sections() { TraceMobx(); - const columnHeaders = this.columnHeaders; + const columnHeaders = this.colHeaderData; // filter out the currently dragged docs from the child docs, since we will insert them later const docs = this.childDocs.filter(d => !DragManager.docsBeingDragged.includes(d)); const sections = new Map<SchemaHeaderField, Doc[]>(columnHeaders.map(sh => [sh, []] as [SchemaHeaderField, []])); @@ -149,9 +150,10 @@ export class CollectionNoteTakingView extends CollectionSubView() { componentDidMount() { super.componentDidMount?.(); document.addEventListener('pointerup', this.removeDocDragHighlight, true); - this._disposers.autoHeight = reaction( - () => this.layoutDoc._autoHeight, - autoHeight => autoHeight && this.props.setHeight?.(Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), this.headerMargin + Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace('px', '')))))) + this._disposers.layout_autoHeight = reaction( + () => this.layoutDoc._layout_autoHeight, + layout_autoHeight => + layout_autoHeight && this.props.setHeight?.(Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), this.headerMargin + Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace('px', '')))))) ); } @@ -199,13 +201,17 @@ export class CollectionNoteTakingView extends CollectionSubView() { }; styleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string) => { - if (property === StyleProp.BoxShadow && doc && DragManager.docsBeingDragged.includes(doc)) { - return `#9c9396 ${StrCast(doc?.boxShadow, '10px 10px 0.9vw')}`; - } - if (property === StyleProp.Opacity && doc) { - if (this.props.childOpacity) { - return this.props.childOpacity(); - } + switch (property) { + case StyleProp.BoxShadow: + if (doc && DragManager.docsBeingDragged.includes(doc)) { + return `#9c9396 ${StrCast(doc?.layout_boxShadow, '10px 10px 0.9vw')}`; + } + break; + case StyleProp.Opacity: + if (doc && this.props.childOpacity) { + return this.props.childOpacity(); + } + break; } return this.props.styleProvider?.(doc, props, property); }; @@ -224,13 +230,13 @@ export class CollectionNoteTakingView extends CollectionSubView() { ref={r => (dref = r || undefined)} Document={doc} pointerEvents={this.blockPointerEventsWhenDragging} - DataDoc={dataDoc || (!Doc.AreProtosEqual(doc[DataSym], doc) && doc[DataSym])} + DataDoc={dataDoc ?? (!Doc.AreProtosEqual(doc[DocData], doc) ? doc[DocData] : undefined)} renderDepth={this.props.renderDepth + 1} PanelWidth={width} PanelHeight={height} styleProvider={this.styleProvider} docViewPath={this.props.docViewPath} - fitWidth={this.props.childFitWidth} + layout_fitWidth={this.props.childLayoutFitWidth} isContentActive={emptyFunction} onKey={this.onKeyDown} //TODO: change this from a prop to a parameter passed into a function @@ -238,22 +244,22 @@ export class CollectionNoteTakingView extends CollectionSubView() { isDocumentActive={this.isContentActive} LayoutTemplate={this.props.childLayoutTemplate} LayoutTemplateString={this.props.childLayoutString} - NativeWidth={this.props.childIgnoreNativeSize ? returnZero : this.props.childFitWidth?.(doc) || (doc._fitWidth && !Doc.NativeWidth(doc)) ? width : undefined} // explicitly ignore nativeWidth/height if childIgnoreNativeSize is set- used by PresBox - NativeHeight={this.props.childIgnoreNativeSize ? returnZero : this.props.childFitWidth?.(doc) || (doc._fitWidth && !Doc.NativeHeight(doc)) ? height : undefined} + NativeWidth={this.props.childIgnoreNativeSize ? returnZero : this.props.childLayoutFitWidth?.(doc) || (doc._layout_fitWidth && !Doc.NativeWidth(doc)) ? width : undefined} // explicitly ignore nativeWidth/height if childIgnoreNativeSize is set- used by PresBox + NativeHeight={this.props.childIgnoreNativeSize ? returnZero : this.props.childLayoutFitWidth?.(doc) || (doc._layout_fitWidth && !Doc.NativeHeight(doc)) ? height : undefined} dontCenter={this.props.childIgnoreNativeSize ? 'xy' : undefined} dontRegisterView={dataDoc ? true : BoolCast(this.layoutDoc.childDontRegisterViews, this.props.dontRegisterView)} rootSelected={this.rootSelected} - showTitle={this.props.childShowTitle} + layout_showTitle={this.props.childlayout_showTitle} dropAction={StrCast(this.layoutDoc.childDropAction) as dropActionType} onClick={this.onChildClickHandler} onDoubleClick={this.onChildDoubleClickHandler} ScreenToLocalTransform={noteTakingDocTransform} focus={this.focusDocument} - docFilters={this.childDocFilters} + childFilters={this.childDocFilters} hideDecorationTitle={this.props.childHideDecorationTitle?.()} hideResizeHandles={this.props.childHideResizeHandles?.()} hideTitle={this.props.childHideTitle?.()} - docRangeFilters={this.childDocRangeFilters} + childFiltersByRanges={this.childDocRangeFilters} searchFilterDocs={this.searchFilterDocs} addDocument={this.props.addDocument} moveDocument={this.props.moveDocument} @@ -280,10 +286,10 @@ export class CollectionNoteTakingView extends CollectionSubView() { // if a note doc. Otherwise, returns the normal width (for graphs, images, etc...) getDocWidth(d: Doc) { const heading = !d[this.notetakingCategoryField] ? 'unset' : Field.toString(d[this.notetakingCategoryField] as Field); - const existingHeader = this.columnHeaders.find(sh => sh.heading === heading); + const existingHeader = this.colHeaderData.find(sh => sh.heading === heading); const existingWidth = existingHeader?.width ? existingHeader.width : 0; const maxWidth = existingWidth > 0 ? existingWidth * this.availableWidth : this.maxColWidth; - const width = d.fitWidth ? maxWidth : d[WidthSym](); + const width = d.layout_fitWidth ? maxWidth : d[Width](); return Math.min(maxWidth - CollectionNoteTakingViewColumn.ColumnMargin, width < maxWidth ? width : maxWidth); } @@ -293,14 +299,14 @@ export class CollectionNoteTakingView extends CollectionSubView() { const childLayoutDoc = Doc.Layout(d, this.props.childLayoutTemplate?.()); const childDataDoc = !d.isTemplateDoc && !d.isTemplateForField ? undefined : this.props.DataDoc; const maxHeight = (lim => (lim === 0 ? this.props.PanelWidth() : lim === -1 ? 10000 : lim))(NumCast(this.layoutDoc.childLimitHeight, -1)); - const nw = Doc.NativeWidth(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._fitWidth || this.props.childFitWidth?.(d)) ? d[WidthSym]() : 0); - const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._fitWidth || this.props.childFitWidth?.(d)) ? d[HeightSym]() : 0); + const nw = Doc.NativeWidth(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._layout_fitWidth || this.props.childLayoutFitWidth?.(d)) ? d[Width]() : 0); + const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._layout_fitWidth || this.props.childLayoutFitWidth?.(d)) ? d[Height]() : 0); if (nw && nh) { const docWid = this.getDocWidth(d); return Math.min(maxHeight, (docWid * nh) / nw); } const childHeight = NumCast(childLayoutDoc._height); - const panelHeight = childLayoutDoc._fitWidth || this.props.childFitWidth?.(d) ? Number.MAX_SAFE_INTEGER : this.props.PanelHeight() - 2 * this.yMargin; + const panelHeight = childLayoutDoc._layout_fitWidth || this.props.childLayoutFitWidth?.(d) ? Number.MAX_SAFE_INTEGER : this.props.PanelHeight() - 2 * this.yMargin; return Math.min(childHeight, maxHeight, panelHeight); } @@ -318,7 +324,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { const n = headers.length; const curWidths = headers.reduce((sum, hdr) => sum + Math.abs(hdr.width), 0); const scaleFactor = 1 / curWidths; - this.dataDoc.columnHeaders = new List<SchemaHeaderField>( + this.dataDoc[this.fieldKey + '_columnHeaders'] = new List<SchemaHeaderField>( headers.map(h => { h.setWidth(Math.abs(h.width) * scaleFactor); return h; @@ -352,7 +358,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { }); // we alter the pivot fields of the docs in case they are moved to a new column. const colIndex = this.getColumnFromXCoord(xCoord); - const colHeader = colIndex === undefined ? 'unset' : StrCast(this.columnHeaders[colIndex].heading); + const colHeader = colIndex === undefined ? 'unset' : StrCast(this.colHeaderData[colIndex].heading); DragManager.docsBeingDragged.forEach(d => (d[this.notetakingCategoryField] = colHeader)); // used to notify sections to re-render this.docsDraggedRowCol.length = 0; @@ -365,12 +371,12 @@ export class CollectionNoteTakingView extends CollectionSubView() { // This function is used to know which document a column SHOULD be in while it is being dragged. getColumnFromXCoord = (xCoord: number): number | undefined => { let colIndex: number | undefined = undefined; - const numColumns = this.columnHeaders.length; + const numColumns = this.colHeaderData.length; const coords = []; let colStartXCoord = 0; for (let i = 0; i < numColumns; i++) { coords.push(colStartXCoord); - colStartXCoord += this.columnHeaders[i].width * this.availableWidth + this.DividerWidth; + colStartXCoord += this.colHeaderData[i].width * this.availableWidth + this.DividerWidth; } coords.push(this.PanelWidth); for (let i = 0; i < numColumns; i++) { @@ -386,7 +392,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { getDocsFromXCoord = (xCoord: number): Doc[] => { const docsMatchingHeader: Doc[] = []; const colIndex = this.getColumnFromXCoord(xCoord); - const colHeader = colIndex === undefined ? 'unset' : StrCast(this.columnHeaders[colIndex].heading); + const colHeader = colIndex === undefined ? 'unset' : StrCast(this.colHeaderData[colIndex].heading); this.childDocs?.map(d => { if (d instanceof Promise) return; const sectionValue = (d[this.notetakingCategoryField] as object) ?? 'unset'; @@ -401,7 +407,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { @action onKeyDown = (e: React.KeyboardEvent, fieldProps: FieldViewProps) => { const docView = fieldProps.DocumentView?.(); - if (docView && (e.ctrlKey || docView.rootDoc._singleLine) && ['Enter'].includes(e.key)) { + if (docView && (e.ctrlKey || docView.rootDoc._createDocOnCR) && ['Enter'].includes(e.key)) { e.stopPropagation?.(); const newDoc = Doc.MakeCopy(docView.rootDoc, true); Doc.GetProto(newDoc).text = undefined; @@ -436,10 +442,10 @@ export class CollectionNoteTakingView extends CollectionSubView() { } } } - } else if (de.complete.linkDragData?.dragDocument.context === this.props.Document && de.complete.linkDragData?.linkDragView?.props.CollectionFreeFormDocumentView?.()) { - const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, _fitWidth: true, title: 'dropped annotation' }); + } else if (de.complete.linkDragData?.dragDocument.embedContainer === this.props.Document && de.complete.linkDragData?.linkDragView?.props.CollectionFreeFormDocumentView?.()) { + const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, _layout_fitWidth: true, title: 'dropped annotation' }); this.props.addDocument?.(source); - de.complete.linkDocument = DocUtils.MakeLink(source, de.complete.linkDragData.linkSourceGetAnchor(), { linkRelationship: 'doc annotation' }); // TODODO this is where in text links get passed + de.complete.linkDocument = DocUtils.MakeLink(source, de.complete.linkDragData.linkSourceGetAnchor(), { link_relationship: 'doc annotation' }); // TODODO this is where in text links get passed e.stopPropagation(); } else if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) return this.internalAnchorAnnoDrop(e, de.complete.annoDragData); return false; @@ -468,7 +474,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { this.onPointerMove(true, e.clientX, e.clientY); docus?.map((doc: Doc) => this.addDocument(doc)); const newDoc = this.childDocs.lastElement(); - const colHeader = colInd === undefined ? 'unset' : StrCast(this.columnHeaders[colInd].heading); + const colHeader = colInd === undefined ? 'unset' : StrCast(this.colHeaderData[colInd].heading); newDoc[this.notetakingCategoryField] = colHeader; const docs = this.childDocList; if (docs && targInd !== -1) { @@ -502,7 +508,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { this.refList.push(ref); this.observer = new _global.ResizeObserver( action((entries: any) => { - if (this.layoutDoc._autoHeight && ref && this.refList.length && !SnappingManager.GetIsDragging()) { + if (this.layoutDoc._layout_autoHeight && ref && this.refList.length && !SnappingManager.GetIsDragging()) { const height = this.headerMargin + Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace('px', ''))))); if (!LightboxView.IsLightboxDocView(this.props.docViewPath())) { this.props.setHeight?.(height); @@ -516,7 +522,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { select={this.props.select} addDocument={this.addDocument} chromeHidden={this.chromeHidden} - columnHeaders={this.columnHeaders} + colHeaderData={this.colHeaderData} Document={this.props.Document} DataDoc={this.props.DataDoc} resizeColumns={this.resizeColumns} @@ -524,6 +530,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { numGroupColumns={this.numGroupColumns} gridGap={this.gridGap} pivotField={this.notetakingCategoryField} + fieldKey={this.fieldKey} dividerWidth={this.DividerWidth} maxColWidth={this.maxColWidth} availableWidth={this.availableWidth} @@ -546,15 +553,15 @@ export class CollectionNoteTakingView extends CollectionSubView() { @undoBatch @action addGroup = (value: string) => { - if (this.columnHeaders) { - for (const header of this.columnHeaders) { + if (this.colHeaderData) { + for (const header of this.colHeaderData) { if (header.heading === value) { alert('You cannot use an existing column name. Please try a new column name'); return value; } } } - const columnHeaders = Array.from(Cast(this.props.Document.columnHeaders, listSpec(SchemaHeaderField), null)); + const columnHeaders = Array.from(Cast(this.dataDoc[this.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null)); const newColWidth = 1 / (this.numGroupColumns + 1); columnHeaders.push(new SchemaHeaderField(value, undefined, undefined, newColWidth)); value && this.resizeColumns(columnHeaders); @@ -566,7 +573,7 @@ export class CollectionNoteTakingView extends CollectionSubView() { if (!e.isPropagationStopped()) { const subItems: ContextMenuProps[] = []; subItems.push({ description: `${this.layoutDoc._columnsFill ? 'Variable Size' : 'Autosize'} Column`, event: () => (this.layoutDoc._columnsFill = !this.layoutDoc._columnsFill), icon: 'plus' }); - subItems.push({ description: `${this.layoutDoc._autoHeight ? 'Variable Height' : 'Auto Height'}`, event: () => (this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight), icon: 'plus' }); + subItems.push({ description: `${this.layoutDoc._layout_autoHeight ? 'Variable Height' : 'Auto Height'}`, event: () => (this.layoutDoc._layout_autoHeight = !this.layoutDoc._layout_autoHeight), icon: 'plus' }); subItems.push({ description: 'Clear All', event: () => (this.dataDoc.data = new List([])), icon: 'times' }); ContextMenu.Instance.addItem({ description: 'Options...', subitems: subItems, icon: 'eye' }); } @@ -576,8 +583,8 @@ export class CollectionNoteTakingView extends CollectionSubView() { @action setColumnStartXCoords = (movementXScreen: number, colIndex: number) => { const movementX = this.props.ScreenToLocalTransform().transformDirection(movementXScreen, 0)[0]; - const leftHeader = this.columnHeaders[colIndex]; - const rightHeader = this.columnHeaders[colIndex + 1]; + const leftHeader = this.colHeaderData[colIndex]; + const rightHeader = this.colHeaderData[colIndex + 1]; leftHeader.setWidth(leftHeader.width + movementX / this.availableWidth); rightHeader.setWidth(rightHeader.width - movementX / this.availableWidth); }; @@ -600,42 +607,6 @@ export class CollectionNoteTakingView extends CollectionSubView() { return eles; } - @computed get buttonMenu() { - const menuDoc: Doc = Cast(this.rootDoc.buttonMenuDoc, Doc, null); - if (menuDoc) { - const width = NumCast(menuDoc._width, 30); - const height = NumCast(menuDoc._height, 30); - return ( - <div className="buttonMenu-docBtn" style={{ width, height }}> - <DocumentView - Document={menuDoc} - DataDoc={menuDoc} - isContentActive={this.props.isContentActive} - isDocumentActive={returnTrue} - addDocument={this.props.addDocument} - moveDocument={this.props.moveDocument} - addDocTab={this.props.addDocTab} - pinToPres={emptyFunction} - rootSelected={this.props.isSelected} - removeDocument={this.props.removeDocument} - ScreenToLocalTransform={Transform.Identity} - PanelWidth={() => 35} - PanelHeight={() => 35} - renderDepth={this.props.renderDepth} - focus={emptyFunction} - styleProvider={this.props.styleProvider} - docViewPath={returnEmptyDoclist} - whenChildContentsActiveChanged={emptyFunction} - bringToFront={emptyFunction} - docFilters={this.props.docFilters} - docRangeFilters={this.props.docRangeFilters} - searchFilterDocs={this.props.searchFilterDocs} - /> - </div> - ); - } - } - @computed get nativeWidth() { return Doc.NativeWidth(this.layoutDoc); } @@ -655,35 +626,25 @@ export class CollectionNoteTakingView extends CollectionSubView() { render() { TraceMobx(); - const buttonMenu = this.rootDoc.buttonMenu; - const noviceExplainer = StrCast(this.rootDoc.explainer); return ( - <> - {buttonMenu || noviceExplainer ? ( - <div className="documentButtonMenu" key="buttons"> - {buttonMenu ? this.buttonMenu : null} - {Doc.UserDoc().noviceMode && noviceExplainer ? <div className="documentExplanation">{noviceExplainer}</div> : null} - </div> - ) : null} - <div - className="collectionNoteTakingView" - ref={this.createRef} - key="notes" - style={{ - overflowY: this.props.isContentActive() ? 'auto' : 'hidden', - background: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor), - pointerEvents: this.backgroundEvents ? 'all' : undefined, - }} - onScroll={action(e => (this._scroll = e.currentTarget.scrollTop))} - onPointerLeave={action(e => (this.docsDraggedRowCol.length = 0))} - onPointerMove={e => e.buttons && this.onPointerMove(false, e.clientX, e.clientY)} - onDragOver={e => this.onPointerMove(true, e.clientX, e.clientY)} - onDrop={this.onExternalDrop.bind(this)} - onContextMenu={this.onContextMenu} - onWheel={e => this.props.isContentActive(true) && e.stopPropagation()}> - {this.renderedSections} - </div> - </> + <div + className="collectionNoteTakingView" + ref={this.createRef} + key="notes" + style={{ + overflowY: this.props.isContentActive() ? 'auto' : 'hidden', + background: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor), + pointerEvents: this.backgroundEvents ? 'all' : undefined, + }} + onScroll={action(e => (this._scroll = e.currentTarget.scrollTop))} + onPointerLeave={action(e => (this.docsDraggedRowCol.length = 0))} + onPointerMove={e => e.buttons && this.onPointerMove(false, e.clientX, e.clientY)} + onDragOver={e => this.onPointerMove(true, e.clientX, e.clientY)} + onDrop={this.onExternalDrop.bind(this)} + onContextMenu={this.onContextMenu} + onWheel={e => this.props.isContentActive(true) && e.stopPropagation()}> + {this.renderedSections} + </div> ); } } diff --git a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx index 28bdd0cb9..2f28ecd00 100644 --- a/src/client/views/collections/CollectionNoteTakingViewColumn.tsx +++ b/src/client/views/collections/CollectionNoteTakingViewColumn.tsx @@ -29,8 +29,9 @@ interface CSVFieldColumnProps { docList: Doc[]; heading: string; pivotField: string; + fieldKey: string | undefined; chromeHidden?: boolean; - columnHeaders: SchemaHeaderField[] | undefined; + colHeaderData: SchemaHeaderField[] | undefined; headingObject: SchemaHeaderField | undefined; yMargin: number; numGroupColumns: number; @@ -61,9 +62,9 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu // columnWidth returns the width of a column in absolute pixels @computed get columnWidth() { - if (!this.props.columnHeaders || !this.props.headingObject || this.props.columnHeaders.length === 1) return '100%'; - const i = this.props.columnHeaders.indexOf(this.props.headingObject); - return this.props.columnHeaders[i].width * 100 + '%'; + if (!this.props.colHeaderData || !this.props.headingObject || this.props.colHeaderData.length === 1) return '100%'; + const i = this.props.colHeaderData.indexOf(this.props.headingObject); + return this.props.colHeaderData[i].width * 100 + '%'; } private dropDisposer?: DragManager.DragDropDisposer; @@ -105,7 +106,7 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu headingChanged = (value: string, shiftDown?: boolean) => { const castedValue = this.getValue(value); if (castedValue) { - if (this.props.columnHeaders?.map(i => i.heading).indexOf(castedValue.toString()) !== -1) { + if (this.props.colHeaderData?.map(i => i.heading).indexOf(castedValue.toString()) !== -1) { return false; } this.props.docList.forEach(d => (d[this.props.pivotField] = castedValue)); @@ -120,14 +121,15 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu @action pointerEntered = () => SnappingManager.GetIsDragging() && (this._background = '#b4b4b4'); @action pointerLeave = () => (this._background = 'inherit'); - textCallback = (char: string) => this.addNewTextDoc('-typed text-', false, true); + @undoBatch + addTextNote = (char: string) => this.addNewTextDoc('-typed text-', false, true); // addNewTextDoc is called when a user starts typing in a column to create a new node @action addNewTextDoc = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => { if (!value && !forceEmptyNote) return false; const key = this.props.pivotField; - const newDoc = Docs.Create.TextDocument(value, { _height: 18, _width: 200, _fitWidth: true, title: value, _autoHeight: true }); + const newDoc = Docs.Create.TextDocument(value, { _height: 18, _width: 200, _layout_fitWidth: true, title: value, _layout_autoHeight: true }); const colValue = this.getValue(this.props.heading); newDoc[key] = colValue; FormattedTextBox.SelectOnLoad = newDoc[Id]; @@ -141,11 +143,11 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu @undoBatch @action deleteColumn = () => { - const columnHeaders = Array.from(Cast(this.props.Document.columnHeaders, listSpec(SchemaHeaderField), null)); + const colHdrData = Array.from(Cast(this.props.Document[this.props.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null)); if (this.props.headingObject) { this.props.docList.forEach(d => (d[this.props.pivotField] = undefined)); - columnHeaders.splice(columnHeaders.indexOf(this.props.headingObject), 1); - this.props.resizeColumns(columnHeaders); + colHdrData.splice(colHdrData.indexOf(this.props.headingObject), 1); + this.props.resizeColumns(colHdrData); } }; @@ -211,7 +213,7 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu !Doc.UserDoc().noviceMode && ContextMenu.Instance.addItem({ description: 'Containers ...', subitems: layoutItems, icon: 'eye' }); ContextMenu.Instance.setDefaultItem('::', (name: string): void => { Doc.GetProto(this.props.Document)[name] = ''; - const created = Docs.Create.TextDocument('', { title: name, _width: 250, _autoHeight: true }); + const created = Docs.Create.TextDocument('', { title: name, _width: 250, _layout_autoHeight: true }); if (created) { if (this.props.Document.isTemplateDoc) { Doc.MakeMetadataFieldTemplate(created, this.props.Document); @@ -243,7 +245,7 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu style={{ background: evContents !== `No Value` ? this._color : 'inherit' }}> <EditableView GetValue={() => evContents} isEditingCallback={isEditing => isEditing && this.props.select(false)} SetValue={this.headingChanged} contents={evContents} oneLine={true} /> </div> - {(this.props.columnHeaders?.length ?? 0) > 1 && ( + {(this.props.colHeaderData?.length ?? 0) > 1 && ( <button className="collectionNoteTakingView-sectionDelete" onClick={this.deleteColumn}> <FontAwesomeIcon icon="trash" size="lg" /> </button> @@ -271,7 +273,7 @@ export class CollectionNoteTakingViewColumn extends React.Component<CSVFieldColu {!this.props.chromeHidden ? ( <div className="collectionNoteTakingView-DocumentButtons" style={{ marginBottom: 10 }}> <div key={`${heading}-add-document`} className="collectionNoteTakingView-addDocumentButton"> - <EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} textCallback={this.textCallback} placeholder={"Type ':' for commands"} contents={'+ New Node'} menuCallback={this.menuCallback} /> + <EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} textCallback={this.addTextNote} placeholder={"Type ':' for commands"} contents={'+ New Node'} menuCallback={this.menuCallback} /> </div> <div key={`${this.props.Document[Id]}-addGroup`} className="collectionNoteTakingView-addDocumentButton"> <EditableView {...this.props.editableViewProps()} /> diff --git a/src/client/views/collections/CollectionPileView.tsx b/src/client/views/collections/CollectionPileView.tsx index 5b96a8682..57d9bbb49 100644 --- a/src/client/views/collections/CollectionPileView.tsx +++ b/src/client/views/collections/CollectionPileView.tsx @@ -1,6 +1,7 @@ import { action, computed, IReactionDisposer, reaction } from 'mobx'; import { observer } from 'mobx-react'; -import { Doc, DocListCast, HeightSym, WidthSym } from '../../../fields/Doc'; +import { Doc, DocListCast } from '../../../fields/Doc'; +import { Height, Width } from '../../../fields/DocSymbols'; import { NumCast, StrCast } from '../../../fields/Types'; import { emptyFunction, returnFalse, returnTrue, setupMoveUpEvents } from '../../../Utils'; import { DocUtils } from '../../documents/Documents'; @@ -22,7 +23,7 @@ export class CollectionPileView extends CollectionSubView() { componentDidMount() { if (this.layoutEngine() !== computePassLayout.name && this.layoutEngine() !== computeStarburstLayout.name) { - this.Document._pileLayoutEngine = computePassLayout.name; + this.Document._freeform_pileEngine = computePassLayout.name; } this._originalChrome = this.layoutDoc._chromeHidden; this.layoutDoc._chromeHidden = true; @@ -32,7 +33,7 @@ export class CollectionPileView extends CollectionSubView() { Object.values(this._disposers).forEach(disposer => disposer?.()); } - layoutEngine = () => StrCast(this.Document._pileLayoutEngine); + layoutEngine = () => StrCast(this.Document._freeform_pileEngine); @undoBatch addPileDoc = (doc: Doc | Doc[]) => { @@ -72,28 +73,29 @@ export class CollectionPileView extends CollectionSubView() { // toggles the pileup between starburst to compact toggleStarburst = action(() => { + this.layoutDoc._freeform_scale = undefined; if (this.layoutEngine() === computeStarburstLayout.name) { - if (this.rootDoc[WidthSym]() !== NumCast(this.rootDoc._starburstDiameter, 500)) { - this.rootDoc._starburstDiameter = this.rootDoc[WidthSym](); + if (this.rootDoc[Width]() !== NumCast(this.rootDoc._starburstDiameter, 500)) { + this.rootDoc._starburstDiameter = this.rootDoc[Width](); } const defaultSize = 110; - this.rootDoc.x = NumCast(this.rootDoc.x) + this.layoutDoc[WidthSym]() / 2 - NumCast(this.layoutDoc._starburstPileWidth, defaultSize) / 2; - this.rootDoc.y = NumCast(this.rootDoc.y) + this.layoutDoc[HeightSym]() / 2 - NumCast(this.layoutDoc._starburstPileHeight, defaultSize) / 2; - this.layoutDoc._width = NumCast(this.layoutDoc._starburstPileWidth, defaultSize); - this.layoutDoc._height = NumCast(this.layoutDoc._starburstPileHeight, defaultSize); + this.rootDoc.x = NumCast(this.rootDoc.x) + this.layoutDoc[Width]() / 2 - NumCast(this.layoutDoc._freeform_pileWidth, defaultSize) / 2; + this.rootDoc.y = NumCast(this.rootDoc.y) + this.layoutDoc[Height]() / 2 - NumCast(this.layoutDoc._freeform_pileHeight, defaultSize) / 2; + this.layoutDoc._width = NumCast(this.layoutDoc._freeform_pileWidth, defaultSize); + this.layoutDoc._height = NumCast(this.layoutDoc._freeform_pileHeight, defaultSize); DocUtils.pileup(this.childDocs, undefined, undefined, NumCast(this.layoutDoc._width) / 2, false); - this.layoutDoc._panX = 0; - this.layoutDoc._panY = -10; - this.props.Document._pileLayoutEngine = computePassLayout.name; + this.layoutDoc._freeform_panX = 0; + this.layoutDoc._freeform_panY = -10; + this.props.Document._freeform_pileEngine = computePassLayout.name; } else { const defaultSize = NumCast(this.rootDoc._starburstDiameter, 500); - this.rootDoc.x = NumCast(this.rootDoc.x) + this.layoutDoc[WidthSym]() / 2 - defaultSize / 2; - this.rootDoc.y = NumCast(this.rootDoc.y) + this.layoutDoc[HeightSym]() / 2 - defaultSize / 2; - this.layoutDoc._starburstPileWidth = this.layoutDoc[WidthSym](); - this.layoutDoc._starburstPileHeight = this.layoutDoc[HeightSym](); - this.layoutDoc._panX = this.layoutDoc._panY = 0; + this.rootDoc.x = NumCast(this.rootDoc.x) + this.layoutDoc[Width]() / 2 - defaultSize / 2; + this.rootDoc.y = NumCast(this.rootDoc.y) + this.layoutDoc[Height]() / 2 - defaultSize / 2; + this.layoutDoc._freeform_pileWidth = this.layoutDoc[Width](); + this.layoutDoc._freeform_pileHeight = this.layoutDoc[Height](); + this.layoutDoc._freeform_panX = this.layoutDoc._freeform_panY = 0; this.layoutDoc._width = this.layoutDoc._height = defaultSize; - this.props.Document._pileLayoutEngine = computeStarburstLayout.name; + this.props.Document._freeform_pileEngine = computeStarburstLayout.name; } }); diff --git a/src/client/views/collections/CollectionStackedTimeline.tsx b/src/client/views/collections/CollectionStackedTimeline.tsx index 22a575989..9d5cb257a 100644 --- a/src/client/views/collections/CollectionStackedTimeline.tsx +++ b/src/client/views/collections/CollectionStackedTimeline.tsx @@ -24,7 +24,7 @@ import { undoBatch, UndoManager } from '../../util/UndoManager'; import { AudioWaveform } from '../AudioWaveform'; import { CollectionSubView } from '../collections/CollectionSubView'; import { LightboxView } from '../LightboxView'; -import { DocFocusFunc, DocFocusOptions, DocumentView, DocumentViewProps } from '../nodes/DocumentView'; +import { DocFocusFunc, DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere } from '../nodes/DocumentView'; import { LabelBox } from '../nodes/LabelBox'; import { VideoBox } from '../nodes/VideoBox'; import './CollectionStackedTimeline.scss'; @@ -41,7 +41,9 @@ export type CollectionStackedTimelineProps = { mediaPath: string; dictationKey: string; rawDuration: number; + dataFieldKey: string; fieldKey: string; + thumbnails?: () => string[]; }; // trimming state: shows full clip, current trim bounds, or not trimming @@ -80,6 +82,9 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack get minTrimLength() { return Math.max(this._timeline?.getBoundingClientRect() ? 0.05 * this.clipDuration : 0, 0.5); } + @computed get thumbnails() { + return this.props.thumbnails?.(); + } @computed get trimStart() { return this.IsTrimming !== TrimScope.None ? this._trimStart : this.clipStart; @@ -102,7 +107,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack } @computed get currentTime() { - return NumCast(this.layoutDoc._currentTimecode); + return NumCast(this.layoutDoc._layout_currentTimecode); } @computed get zoomFactor() { @@ -114,7 +119,8 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack // onClick play scripts CollectionStackedTimeline.RangeScript = CollectionStackedTimeline.RangeScript || - ScriptField.MakeFunction(`scriptContext.clickAnchor(this, clientX)`, { + ScriptField.MakeFunction(`setTimeout(() => scriptContext.clickAnchor(this, clientX))`, { + // setTimeout is a hack to run script in its own properly named undo group (instead of being part of the generic onClick) self: Doc.name, scriptContext: 'any', clientX: 'number', @@ -219,12 +225,12 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack }; getLinkData(l: Doc) { - let la1 = l.anchor1 as Doc; - let la2 = l.anchor2 as Doc; + let la1 = l.link_anchor_1 as Doc; + let la2 = l.link_anchor_2 as Doc; const linkTime = NumCast(la2[this.props.startTag], NumCast(la1[this.props.startTag])); if (Doc.AreProtosEqual(la1, this.dataDoc)) { - la1 = l.anchor2 as Doc; - la2 = l.anchor1 as Doc; + la1 = l.link_anchor_2 as Doc; + la2 = l.link_anchor_1 as Doc; } return { la1, la2, linkTime }; } @@ -292,10 +298,9 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack const clientX = e.clientX; if (rect) { this._hoverTime = this.toTimeline(clientX - rect.x, rect.width); - if (this.dataDoc.thumbnails) { + if (this.thumbnails) { const nearest = Math.floor((this._hoverTime / this.props.rawDuration) * VideoBox.numThumbnails); - const thumbnails = StrListCast(this.dataDoc.thumbnails); - const imgField = thumbnails?.length > 0 ? new ImageField(thumbnails[nearest]) : undefined; + const imgField = this.thumbnails.length > 0 ? new ImageField(this.thumbnails[nearest]) : undefined; this._thumbnail = imgField?.url?.href ? imgField.url.href.replace('.png', '_m.png') : undefined; } } @@ -399,17 +404,15 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack docAnchor ?? Docs.Create.LabelDocument({ title: ComputedField.MakeFunction(`self["${endTag}"] ? "#" + formatToTime(self["${startTag}"]) + "-" + formatToTime(self["${endTag}"]) : "#" + formatToTime(self["${startTag}"])`) as any, - _minFontSize: 12, - _maxFontSize: 24, - _singleLine: true, + _label_minFontSize: 12, + _label_maxFontSize: 24, _stayInCollection: true, backgroundColor: 'rgba(128, 128, 128, 0.5)', - useLinkSmallAnchor: true, - hideLinkButton: true, + layout_hideLinkButton: true, onClick: FollowLinkScript(), annotationOn: rootDoc, - _timelineLabel: true, - borderRounding: anchorEndTime === undefined ? '100%' : undefined, + _isTimelineLabel: true, + layout_borderRounding: anchorEndTime === undefined ? '100%' : undefined, }); Doc.GetProto(anchor)[startTag] = anchorStartTime; Doc.GetProto(anchor)[endTag] = anchorEndTime; @@ -434,7 +437,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack this.scrollToTime(seekTimeInSeconds); } } else { - if (seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) && endTime > NumCast(this.layoutDoc._currentTimecode)) { + if (seekTimeInSeconds < NumCast(this.layoutDoc._layout_currentTimecode) && endTime > NumCast(this.layoutDoc._layout_currentTimecode)) { if (!this.layoutDoc.autoPlayAnchors && this.props.playing()) { this.props.Pause(); } else { @@ -455,7 +458,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack } const seekTimeInSeconds = this.anchorStart(anchorDoc) - 0.05; const endTime = this.anchorEnd(anchorDoc); - if (seekTimeInSeconds < NumCast(this.layoutDoc._currentTimecode) + 1e-4 && endTime > NumCast(this.layoutDoc._currentTimecode) - 1e-4) { + if (seekTimeInSeconds < NumCast(this.layoutDoc._layout_currentTimecode) + 1e-4 && endTime > NumCast(this.layoutDoc._layout_currentTimecode) - 1e-4) { if (this.props.playing()) this.props.Pause(); else if (this.layoutDoc.autoPlayAnchors) this.props.Play(); else if (!this.layoutDoc.autoPlayAnchors) { @@ -596,6 +599,7 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack {!this.props.PanelHeight() ? null : ( <AudioWaveform rawDuration={this.props.rawDuration} + fieldKey={this.props.dataFieldKey} duration={this.clipDuration} mediaPath={this.props.mediaPath} layoutDoc={this.layoutDoc} @@ -661,6 +665,8 @@ export class CollectionStackedTimeline extends CollectionSubView<CollectionStack interface StackedTimelineAnchorProps { mark: Doc; + whenChildContentsActiveChanged: (isActive: boolean) => void; + addDocTab: (doc: Doc, where: OpenWhere) => boolean; rangeClickScript: () => ScriptField; rangePlayScript: () => ScriptField; left: number; @@ -709,7 +715,7 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> () => this.props.currentTimecode(), time => { const dictationDoc = Cast(this.props.layoutDoc['data-dictation'], Doc, null); - const isDictation = dictationDoc && LinkManager.Links(this.props.mark).some(link => Cast(link.anchor1, Doc, null)?.annotationOn === dictationDoc); + const isDictation = dictationDoc && LinkManager.Links(this.props.mark).some(link => Cast(link.link_anchor_1, Doc, null)?.annotationOn === dictationDoc); if ( !LightboxView.LightboxDoc && // bcz: when should links be followed? we don't want to move away from the video to follow a link but we can open it in a sidebar/etc. But we don't know that upfront. @@ -746,7 +752,7 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> if (timelineOnly) { if (!left && time !== undefined && time <= NumCast(anchor[this.props.startTag])) time = undefined; Doc.SetInPlace(anchor, left ? this.props.startTag : this.props.endTag, time, true); - if (!left) Doc.SetInPlace(anchor, 'borderRounding', time !== undefined ? undefined : '100%', true); + if (!left) Doc.SetInPlace(anchor, 'layout_borderRounding', time !== undefined ? undefined : '100%', true); } else { anchor[left ? '_timecodeToShow' : '_timecodeToHide'] = time; } @@ -808,16 +814,14 @@ class StackedTimelineAnchor extends React.Component<StackedTimelineAnchorProps> isDocumentActive={this.props.isDocumentActive} PanelWidth={width} PanelHeight={height} - fitWidth={returnTrue} + layout_fitWidth={returnTrue} ScreenToLocalTransform={screenXf} - addDocTab={returnFalse} pinToPres={emptyFunction} - whenChildContentsActiveChanged={emptyFunction} focus={focusFunc} isContentActive={returnFalse} searchFilterDocs={returnEmptyDoclist} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} rootSelected={returnFalse} onClick={script} onDoubleClick={this.props.layoutDoc.autoPlayAnchors ? undefined : doublescript} diff --git a/src/client/views/collections/CollectionStackingView.scss b/src/client/views/collections/CollectionStackingView.scss index f3397e2c4..99a68e94b 100644 --- a/src/client/views/collections/CollectionStackingView.scss +++ b/src/client/views/collections/CollectionStackingView.scss @@ -372,7 +372,8 @@ .editableView-container-editing-oneLine, .editableView-container-editing { color: grey; - padding-top: 10px; + padding-top: 5px; + padding-bottom: 5px; width: 100%; } @@ -387,7 +388,8 @@ letter-spacing: 2px; color: grey; border: 0px; - padding-top: 10px; // 12px 10px 11px 10px; + padding-top: 5px; // 12px 10px 11px 10px; + padding-bottom: 5px; // 12px 10px 11px 10px; } } diff --git a/src/client/views/collections/CollectionStackingView.tsx b/src/client/views/collections/CollectionStackingView.tsx index eedf639aa..3c0d8cbc3 100644 --- a/src/client/views/collections/CollectionStackingView.tsx +++ b/src/client/views/collections/CollectionStackingView.tsx @@ -3,7 +3,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { CursorProperty } from 'csstype'; import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; -import { DataSym, Doc, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; +import { Doc, Opt } from '../../../fields/Doc'; +import { DocData, Height, Width } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; @@ -36,7 +37,7 @@ export type collectionStackingViewProps = { sortFunc?: (a: Doc, b: Doc) => number; chromeHidden?: boolean; // view type is stacking - viewType?: CollectionViewType; + type_collection?: CollectionViewType; NativeWidth?: () => number; NativeHeight?: () => number; }; @@ -49,7 +50,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection // Not sure what a pivot field is. Seems like we cause reaction in MobX get rid of it once we exit this view _pivotFieldDisposer?: IReactionDisposer; // Seems like we cause reaction in MobX get rid of our height once we exit this view - _autoHeightDisposer?: IReactionDisposer; + _layout_autoHeightDisposer?: IReactionDisposer; // keeping track of documents. Updated on internal and external drops. What's the difference? _docXfs: { height: () => number; width: () => number; stackedDocTransform: () => Transform }[] = []; // Doesn't look like this field is being used anywhere. Obsolete? @@ -65,8 +66,8 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection return this.props.chromeHidden || BoolCast(this.layoutDoc.chromeHidden); } // it looks like this gets the column headers that Mehek was showing just now - @computed get columnHeaders() { - return Cast(this.layoutDoc._columnHeaders, listSpec(SchemaHeaderField), null); + @computed get colHeaderData() { + return Cast(this.layoutDoc['_' + this.fieldKey + '_columnHeaders'], listSpec(SchemaHeaderField), null); } // Still not sure what a pivot is, but it appears that we can actually filter docs somehow? @computed get pivotField() { @@ -94,7 +95,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection } // are we stacking or masonry? @computed get isStackingView() { - return (this.props.viewType ?? this.layoutDoc._viewType) === CollectionViewType.Stacking; + return (this.props.type_collection ?? this.layoutDoc._type_collection) === CollectionViewType.Stacking; } // this is the number of StackingViewFieldColumns that we have @computed get numGroupColumns() { @@ -116,10 +117,10 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection constructor(props: any) { super(props); - if (this.columnHeaders === undefined) { + if (this.colHeaderData === undefined) { // TODO: what is a layout doc? Is it literally how this document is supposed to be layed out? // here we're making an empty list of column headers (again, what Mehek showed us) - this.layoutDoc._columnHeaders = new List<SchemaHeaderField>(); + this.layoutDoc['_' + this.fieldKey + '_columnHeaders'] = new List<SchemaHeaderField>(); } } @@ -154,14 +155,14 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection //TODO: this seems important get Sections() { // appears that pivot field IS actually for sorting - if (!this.pivotField || this.columnHeaders instanceof Promise) return new Map<SchemaHeaderField, Doc[]>(); + if (!this.pivotField || this.colHeaderData instanceof Promise) return new Map<SchemaHeaderField, Doc[]>(); - if (this.columnHeaders === undefined) { - setTimeout(() => (this.layoutDoc._columnHeaders = new List<SchemaHeaderField>()), 0); + if (this.colHeaderData === undefined) { + setTimeout(() => (this.layoutDoc['_' + this.fieldKey + '_columnHeaders'] = new List<SchemaHeaderField>()), 0); return new Map<SchemaHeaderField, Doc[]>(); } - const columnHeaders = Array.from(this.columnHeaders); - const fields = new Map<SchemaHeaderField, Doc[]>(columnHeaders.map(sh => [sh, []] as [SchemaHeaderField, []])); + const colHeaderData = Array.from(this.colHeaderData); + const fields = new Map<SchemaHeaderField, Doc[]>(colHeaderData.map(sh => [sh, []] as [SchemaHeaderField, []])); let changed = false; this.filteredChildren.map(d => { const sectionValue = (d[this.pivotField] ? d[this.pivotField] : `NO ${this.pivotField.toUpperCase()} VALUE`) as object; @@ -170,13 +171,13 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection const castedSectionValue = !isNaN(parsed) ? parsed : sectionValue; // look for if header exists already - const existingHeader = columnHeaders.find(sh => sh.heading === (castedSectionValue ? castedSectionValue.toString() : `NO ${this.pivotField.toUpperCase()} VALUE`)); + const existingHeader = colHeaderData.find(sh => sh.heading === (castedSectionValue ? castedSectionValue.toString() : `NO ${this.pivotField.toUpperCase()} VALUE`)); if (existingHeader) { fields.get(existingHeader)!.push(d); } else { const newSchemaHeader = new SchemaHeaderField(castedSectionValue ? castedSectionValue.toString() : `NO ${this.pivotField.toUpperCase()} VALUE`); fields.set(newSchemaHeader, [d]); - columnHeaders.push(newSchemaHeader); + colHeaderData.push(newSchemaHeader); changed = true; } }); @@ -187,13 +188,13 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection .filter(key => !fields.get(key)!.length) .map(header => { fields.delete(header); - columnHeaders.splice(columnHeaders.indexOf(header), 1); + colHeaderData.splice(colHeaderData.indexOf(header), 1); changed = true; }); } changed && setTimeout( - action(() => this.columnHeaders?.splice(0, this.columnHeaders.length, ...columnHeaders)), + action(() => this.colHeaderData?.splice(0, this.colHeaderData.length, ...colHeaderData)), 0 ); return fields; @@ -206,12 +207,12 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection // reset section headers when a new filter is inputted this._pivotFieldDisposer = reaction( () => this.pivotField, - () => (this.layoutDoc._columnHeaders = new List()) + () => (this.layoutDoc['_' + this.fieldKey + '_columnHeaders'] = new List()) ); - this._autoHeightDisposer = reaction( - () => this.layoutDoc._autoHeight, - autoHeight => - autoHeight && + this._layout_autoHeightDisposer = reaction( + () => this.layoutDoc._layout_autoHeight, + layout_autoHeight => + layout_autoHeight && this.props.setHeight?.( Math.min( NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), @@ -224,7 +225,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection componentWillUnmount() { super.componentWillUnmount(); this._pivotFieldDisposer?.(); - this._autoHeightDisposer?.(); + this._layout_autoHeightDisposer?.(); } isAnyChildContentActive = () => this.props.isAnyChildContentActive(); @@ -282,12 +283,12 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection if (docView && ['Enter'].includes(e.key) && e.ctrlKey) { e.stopPropagation?.(); const below = !e.altKey && e.key !== 'Tab'; - const layoutKey = StrCast(docView.LayoutFieldKey); + const layout_fieldKey = StrCast(docView.LayoutFieldKey); const newDoc = Doc.MakeCopy(docView.rootDoc, true); const dataField = docView.rootDoc[Doc.LayoutFieldKey(newDoc)]; - newDoc[DataSym][Doc.LayoutFieldKey(newDoc)] = dataField === undefined || Cast(dataField, listSpec(Doc), null)?.length !== undefined ? new List<Doc>([]) : undefined; - if (layoutKey !== 'layout' && docView.rootDoc[layoutKey] instanceof Doc) { - newDoc[layoutKey] = docView.rootDoc[layoutKey]; + newDoc[DocData][Doc.LayoutFieldKey(newDoc)] = dataField === undefined || Cast(dataField, listSpec(Doc), null)?.length !== undefined ? new List<Doc>([]) : undefined; + if (layout_fieldKey !== 'layout' && docView.rootDoc[layout_fieldKey] instanceof Doc) { + newDoc[layout_fieldKey] = docView.rootDoc[layout_fieldKey]; } Doc.GetProto(newDoc).text = undefined; FormattedTextBox.SelectOnLoad = newDoc[Id]; @@ -320,36 +321,36 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection <DocumentView ref={r => (dref = r || undefined)} Document={doc} - DataDoc={dataDoc || (!Doc.AreProtosEqual(doc[DataSym], doc) && doc[DataSym])} + DataDoc={dataDoc ?? (!Doc.AreProtosEqual(doc[DocData], doc) ? doc[DocData] : undefined)} renderDepth={this.props.renderDepth + 1} PanelWidth={width} PanelHeight={height} pointerEvents={this.props.DocumentView?.().props.onClick?.() ? returnNone : undefined} // if the stack has an onClick, then we don't want the contents to be interactive (see CollectionPileView) styleProvider={this.styleProvider} docViewPath={this.props.docViewPath} - fitWidth={this.props.childFitWidth} + layout_fitWidth={this.props.childLayoutFitWidth} isContentActive={doc.onClick ? this.isChildButtonContentActive : this.isChildContentActive} onKey={this.onKeyDown} onBrowseClick={this.props.onBrowseClick} isDocumentActive={this.isContentActive} LayoutTemplate={this.props.childLayoutTemplate} LayoutTemplateString={this.props.childLayoutString} - NativeWidth={this.props.childIgnoreNativeSize ? returnZero : this.props.childFitWidth?.(doc) || (doc._fitWidth && !Doc.NativeWidth(doc)) ? width : undefined} // explicitly ignore nativeWidth/height if childIgnoreNativeSize is set- used by PresBox - NativeHeight={this.props.childIgnoreNativeSize ? returnZero : this.props.childFitWidth?.(doc) || (doc._fitWidth && !Doc.NativeHeight(doc)) ? height : undefined} + NativeWidth={this.props.childIgnoreNativeSize ? returnZero : this.props.childLayoutFitWidth?.(doc) || (doc._layout_fitWidth && !Doc.NativeWidth(doc)) ? width : undefined} // explicitly ignore nativeWidth/height if childIgnoreNativeSize is set- used by PresBox + NativeHeight={this.props.childIgnoreNativeSize ? returnZero : this.props.childLayoutFitWidth?.(doc) || (doc._layout_fitWidth && !Doc.NativeHeight(doc)) ? height : undefined} dontCenter={this.props.childIgnoreNativeSize ? 'xy' : undefined} - dontRegisterView={BoolCast(this.layoutDoc.childDontRegisterViews, this.props.dontRegisterView)} // used to be true if DataDoc existed, but template textboxes won't autoHeight resize if dontRegisterView is set, but they need to. + dontRegisterView={BoolCast(this.layoutDoc.childDontRegisterViews, this.props.dontRegisterView)} // used to be true if DataDoc existed, but template textboxes won't layout_autoHeight resize if dontRegisterView is set, but they need to. rootSelected={this.rootSelected} - showTitle={this.props.childShowTitle} + layout_showTitle={this.props.childlayout_showTitle} dropAction={StrCast(this.layoutDoc.childDropAction) as dropActionType} onClick={this.onChildClickHandler} onDoubleClick={this.onChildDoubleClickHandler} ScreenToLocalTransform={stackedDocTransform} focus={this.focusDocument} - docFilters={this.childDocFilters} + childFilters={this.childDocFilters} hideDecorationTitle={this.props.childHideDecorationTitle?.()} hideResizeHandles={this.props.childHideResizeHandles?.()} hideTitle={this.props.childHideTitle?.()} - docRangeFilters={this.childDocRangeFilters} + childFiltersByRanges={this.childDocRangeFilters} searchFilterDocs={this.searchFilterDocs} xPadding={NumCast(this.layoutDoc._childXPadding, this.props.childXPadding)} yPadding={NumCast(this.layoutDoc._childYPadding, this.props.childYPadding)} @@ -376,8 +377,8 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection if (!d) return 0; const childLayoutDoc = Doc.Layout(d, this.props.childLayoutTemplate?.()); const maxWidth = this.columnWidth / this.numGroupColumns; - if (!this.layoutDoc._columnsFill && !(childLayoutDoc._fitWidth || this.props.childFitWidth?.(d))) { - return Math.min(d[WidthSym](), maxWidth); + if (!this.layoutDoc._columnsFill && !(childLayoutDoc._layout_fitWidth || this.props.childLayoutFitWidth?.(d))) { + return Math.min(d[Width](), maxWidth); } return maxWidth; } @@ -386,15 +387,15 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection const childLayoutDoc = Doc.Layout(d, this.props.childLayoutTemplate?.()); const childDataDoc = !d.isTemplateDoc && !d.isTemplateForField ? undefined : this.props.DataDoc; const maxHeight = (lim => (lim === 0 ? this.props.PanelWidth() : lim === -1 ? 10000 : lim))(NumCast(this.layoutDoc.childLimitHeight, -1)); - const nw = Doc.NativeWidth(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._fitWidth || this.props.childFitWidth?.(d)) ? d[WidthSym]() : 0); - const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._fitWidth || this.props.childFitWidth?.(d)) ? d[HeightSym]() : 0); + const nw = Doc.NativeWidth(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._layout_fitWidth || this.props.childLayoutFitWidth?.(d)) ? d[Width]() : 0); + const nh = Doc.NativeHeight(childLayoutDoc, childDataDoc) || (!(childLayoutDoc._layout_fitWidth || this.props.childLayoutFitWidth?.(d)) ? d[Height]() : 0); if (nw && nh) { const colWid = this.columnWidth / (this.isStackingView ? this.numGroupColumns : 1); const docWid = this.layoutDoc._columnsFill ? colWid : Math.min(this.getDocWidth(d), colWid); return Math.min(maxHeight, (docWid * nh) / nw); } const childHeight = NumCast(childLayoutDoc._height); - const panelHeight = childLayoutDoc._fitWidth || this.props.childFitWidth?.(d) ? Number.MAX_SAFE_INTEGER : this.props.PanelHeight() - 2 * this.yMargin; + const panelHeight = childLayoutDoc._layout_fitWidth || this.props.childLayoutFitWidth?.(d) ? Number.MAX_SAFE_INTEGER : this.props.PanelHeight() - 2 * this.yMargin; return Math.min(childHeight, maxHeight, panelHeight); } @@ -460,10 +461,10 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection docs.splice(insertInd - offset, 0, ...newDocs); } } - } else if (de.complete.linkDragData?.dragDocument.context === this.props.Document && de.complete.linkDragData?.linkDragView?.props.CollectionFreeFormDocumentView?.()) { - const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, _fitWidth: true, title: 'dropped annotation' }); + } else if (de.complete.linkDragData?.dragDocument.embedContainer === this.props.Document && de.complete.linkDragData?.linkDragView?.props.CollectionFreeFormDocumentView?.()) { + const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, _layout_fitWidth: true, title: 'dropped annotation' }); this.props.addDocument?.(source); - de.complete.linkDocument = DocUtils.MakeLink(source, de.complete.linkDragData.linkSourceGetAnchor(), { linkRelationship: 'doc annotation' }); // TODODO this is where in text links get passed + de.complete.linkDocument = DocUtils.MakeLink(source, de.complete.linkDragData.linkSourceGetAnchor(), { link_relationship: 'doc annotation' }); // TODODO this is where in text links get passed e.stopPropagation(); } else if (de.complete.annoDragData?.dragDocument && super.onInternalDrop(e, de)) return this.internalAnchorAnnoDrop(e, de.complete.annoDragData); return false; @@ -526,7 +527,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection this.refList.push(ref); this.observer = new _global.ResizeObserver( action((entries: any) => { - if (this.layoutDoc._autoHeight && ref && this.refList.length && !SnappingManager.GetIsDragging()) { + if (this.layoutDoc._layout_autoHeight && ref && this.refList.length && !SnappingManager.GetIsDragging()) { const height = this.headerMargin + Math.min(NumCast(this.layoutDoc._maxHeight, Number.MAX_SAFE_INTEGER), Math.max(...this.refList.map(r => Number(getComputedStyle(r).height.replace('px', ''))))); if (!LightboxView.IsLightboxDocView(this.props.docViewPath())) { this.props.setHeight?.(height); @@ -539,7 +540,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection }} addDocument={this.addDocument} chromeHidden={this.chromeHidden} - columnHeaders={this.columnHeaders} + colHeaderData={this.colHeaderData} Document={this.props.Document} DataDoc={this.props.DataDoc} renderChildren={this.children} @@ -581,7 +582,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection this.refList.push(ref); this.observer = new _global.ResizeObserver( action((entries: any) => { - if (this.layoutDoc._autoHeight && ref && this.refList.length && !SnappingManager.GetIsDragging()) { + if (this.layoutDoc._layout_autoHeight && ref && this.refList.length && !SnappingManager.GetIsDragging()) { const height = this.refList.reduce((p, r) => p + Number(getComputedStyle(r).height.replace('px', '')), 0); this.props.setHeight?.(this.headerMargin + height); } @@ -608,9 +609,9 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection /// add a new group category (column) to the active set of note categories. (e.g., if the pivot field is 'transportation', groups might be 'car', 'plane', 'bike', etc) @action addGroup = (value: string) => { - if (value && this.columnHeaders) { + if (value && this.colHeaderData) { const schemaHdrField = new SchemaHeaderField(value); - this.columnHeaders.push(schemaHdrField); + this.colHeaderData.push(schemaHdrField); return true; } return false; @@ -630,7 +631,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection const options = cm.findByDescription('Options...'); const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : []; optionItems.push({ description: `${this.layoutDoc._columnsFill ? 'Variable Size' : 'Autosize'} Column`, event: () => (this.layoutDoc._columnsFill = !this.layoutDoc._columnsFill), icon: 'plus' }); - optionItems.push({ description: `${this.layoutDoc._autoHeight ? 'Variable Height' : 'Auto Height'}`, event: () => (this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight), icon: 'plus' }); + optionItems.push({ description: `${this.layoutDoc._layout_autoHeight ? 'Variable Height' : 'Auto Height'}`, event: () => (this.layoutDoc._layout_autoHeight = !this.layoutDoc._layout_autoHeight), icon: 'plus' }); optionItems.push({ description: 'Clear All', event: () => (this.dataDoc[this.fieldKey ?? 'data'] = new List([])), icon: 'times' }); !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'compass' }); } @@ -649,7 +650,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection return35 = () => 35; @computed get buttonMenu() { - const menuDoc: Doc = Cast(this.rootDoc.buttonMenuDoc, Doc, null); + const menuDoc: Doc = Cast(this.rootDoc.layout_headerButton, Doc, null); // TODO:glr Allow support for multiple buttons if (menuDoc) { const width: number = NumCast(menuDoc._width, 30); @@ -678,8 +679,8 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection docViewPath={returnEmptyDoclist} whenChildContentsActiveChanged={emptyFunction} bringToFront={emptyFunction} - docFilters={this.props.docFilters} - docRangeFilters={this.props.docRangeFilters} + childFilters={this.props.childFilters} + childFiltersByRanges={this.props.childFiltersByRanges} searchFilterDocs={this.props.searchFilterDocs} /> </div> @@ -710,7 +711,7 @@ export class CollectionStackingView extends CollectionSubView<Partial<collection contents: '+ ADD A GROUP', }; const buttonMenu = this.rootDoc.buttonMenu; - const noviceExplainer = this.rootDoc.explainer; + const noviceExplainer = this.rootDoc.layout_explainer; return ( <> {buttonMenu || noviceExplainer ? ( diff --git a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx index d62c4dc62..6be9cb72d 100644 --- a/src/client/views/collections/CollectionStackingViewFieldColumn.tsx +++ b/src/client/views/collections/CollectionStackingViewFieldColumn.tsx @@ -34,7 +34,7 @@ interface CSVFieldColumnProps { heading: string; pivotField: string; chromeHidden?: boolean; - columnHeaders: SchemaHeaderField[] | undefined; + colHeaderData: SchemaHeaderField[] | undefined; headingObject: SchemaHeaderField | undefined; yMargin: number; columnWidth: number; @@ -93,7 +93,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC @undoBatch columnDrop = action((e: Event, de: DragManager.DropEvent) => { const drop = { docs: de.complete.docDragData?.droppedDocuments, val: this.getValue(this._heading) }; - drop.docs?.forEach(d => Doc.SetInPlace(d, this.props.pivotField, drop.val, false)); + this.props.pivotField && drop.docs?.forEach(d => Doc.SetInPlace(d, this.props.pivotField, drop.val, false)); }); getValue = (value: string): any => { const parsed = parseInt(value); @@ -107,7 +107,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC headingChanged = (value: string, shiftDown?: boolean) => { const castedValue = this.getValue(value); if (castedValue) { - if (this.props.columnHeaders?.map(i => i.heading).indexOf(castedValue.toString()) !== -1) { + if (this.props.colHeaderData?.map(i => i.heading).indexOf(castedValue.toString()) !== -1) { return false; } this.props.docList.forEach(d => (d[this.props.pivotField] = castedValue)); @@ -128,13 +128,13 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC @action pointerEntered = () => SnappingManager.GetIsDragging() && (this._background = '#b4b4b4'); @action pointerLeave = () => (this._background = 'inherit'); - textCallback = (char: string) => this.addNewTextDoc('-typed text-', false, true); + @undoBatch typedNote = (char: string) => this.addNewTextDoc('-typed text-', false, true); @action addNewTextDoc = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => { if (!value && !forceEmptyNote) return false; const key = this.props.pivotField; - const newDoc = Docs.Create.TextDocument(value, { _height: 18, _width: 200, _fitWidth: true, title: value, _autoHeight: true }); + const newDoc = Docs.Create.TextDocument(value, { _height: 18, _width: 200, _layout_fitWidth: true, title: value, _layout_autoHeight: true }); newDoc[key] = this.getValue(this.props.heading); const maxHeading = this.props.docList.reduce((maxHeading, doc) => (NumCast(doc.heading) > maxHeading ? NumCast(doc.heading) : maxHeading), 0); const heading = maxHeading === 0 || this.props.docList.length === 0 ? 1 : maxHeading === 1 ? 2 : 3; @@ -147,9 +147,9 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC @action deleteColumn = () => { this.props.docList.forEach(d => (d[this.props.pivotField] = undefined)); - if (this.props.columnHeaders && this.props.headingObject) { - const index = this.props.columnHeaders.indexOf(this.props.headingObject); - this.props.columnHeaders.splice(index, 1); + if (this.props.colHeaderData && this.props.headingObject) { + const index = this.props.colHeaderData.indexOf(this.props.headingObject); + this.props.colHeaderData.splice(index, 1); } }; @@ -163,15 +163,15 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC //TODO: I think this is where I'm supposed to edit stuff startDrag = (e: PointerEvent, down: number[], delta: number[]) => { - // is MakeAlias a way to make a copy of a doc without rendering it? - const alias = Doc.MakeAlias(this.props.Document); - alias._width = this.props.columnWidth / (this.props.columnHeaders?.length || 1); - alias._pivotField = undefined; + // is MakeEmbedding a way to make a copy of a doc without rendering it? + const embedding = Doc.MakeEmbedding(this.props.Document); + embedding._width = this.props.columnWidth / (this.props.colHeaderData?.length || 1); + embedding._pivotField = undefined; let value = this.getValue(this._heading); value = typeof value === 'string' ? `"${value}"` : value; - alias.viewSpecScript = ScriptField.MakeFunction(`doc.${this.props.pivotField} === ${value}`, { doc: Doc.name }); - if (alias.viewSpecScript) { - DragManager.StartDocumentDrag([this._headerRef.current!], new DragManager.DocumentDragData([alias]), e.clientX, e.clientY); + embedding.viewSpecScript = ScriptField.MakeFunction(`doc.${this.props.pivotField} === ${value}`, { doc: Doc.name }); + if (embedding.viewSpecScript) { + DragManager.StartDocumentDrag([this._headerRef.current!], new DragManager.DocumentDragData([embedding]), e.clientX, e.clientY); return true; } return false; @@ -266,7 +266,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC !Doc.noviceMode && ContextMenu.Instance.addItem({ description: 'Containers ...', subitems: layoutItems, icon: 'eye' }); ContextMenu.Instance.setDefaultItem('::', (name: string): void => { Doc.GetProto(this.props.Document)[name] = ''; - const created = Docs.Create.TextDocument('', { title: name, _width: 250, _autoHeight: true }); + const created = Docs.Create.TextDocument('', { title: name, _width: 250, _layout_autoHeight: true }); if (created) { if (this.props.Document.isTemplateDoc) { Doc.MakeMetadataFieldTemplate(created, this.props.Document); @@ -363,7 +363,7 @@ export class CollectionStackingViewFieldColumn extends React.Component<CSVFieldC <EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} - textCallback={this.textCallback} + textCallback={this.typedNote} placeholder={"Type ':' for commands"} contents={<FontAwesomeIcon icon={'plus'} />} menuCallback={this.menuCallback} diff --git a/src/client/views/collections/CollectionSubView.tsx b/src/client/views/collections/CollectionSubView.tsx index 5b9453666..d1b7f6ff6 100644 --- a/src/client/views/collections/CollectionSubView.tsx +++ b/src/client/views/collections/CollectionSubView.tsx @@ -1,11 +1,11 @@ import { action, computed, observable } from 'mobx'; import * as rp from 'request-promise'; import CursorField from '../../../fields/CursorField'; -import { AclPrivate, Doc, DocListCast, Field, Opt, StrListCast } from '../../../fields/Doc'; +import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../fields/Doc'; +import { AclPrivate } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { listSpec } from '../../../fields/Schema'; -import { ScriptField } from '../../../fields/ScriptField'; import { Cast, ScriptCast, StrCast } from '../../../fields/Types'; import { WebField } from '../../../fields/URLField'; import { GestureUtils } from '../../../pen-gestures/GestureUtils'; @@ -28,8 +28,8 @@ export function CollectionSubView<X>(moreProps?: X) { private gestureDisposer?: GestureUtils.GestureEventDisposer; protected _multiTouchDisposer?: InteractionUtils.MultiTouchEventDisposer; protected _mainCont?: HTMLDivElement; - @observable _focusFilters: Opt<string[]>; // docFilters that are overridden when previewing a link to an anchor which has docFilters set on it - @observable _focusRangeFilters: Opt<string[]>; // docRangeFilters that are overridden when previewing a link to an anchor which has docRangeFilters set on it + @observable _focusFilters: Opt<string[]>; // childFilters that are overridden when previewing a link to an anchor which has childFilters set on it + @observable _focusRangeFilters: Opt<string[]>; // childFiltersByRanges that are overridden when previewing a link to an anchor which has childFiltersByRanges set on it protected createDashEventsTarget = (ele: HTMLDivElement | null) => { //used for stacking and masonry view this.dropDisposer?.(); @@ -65,11 +65,7 @@ export function CollectionSubView<X>(moreProps?: X) { // to its children which may be templates. // If 'annotationField' is specified, then all children exist on that field of the extension document, otherwise, they exist directly on the data document under 'fieldKey' @computed get dataField() { - if (this.layoutDoc[this.props.fieldKey]) return this.layoutDoc[this.props.fieldKey]; - // sets the dataDoc's data field to an empty list if the data field is undefined - prevents issues with addonly - // setTimeout changes it outside of the @computed section - !this.dataDoc[this.props.fieldKey] && setTimeout(() => !this.dataDoc[this.props.fieldKey] && (this.dataDoc[this.props.fieldKey] = new List<Doc>())); - return this.dataDoc[this.props.fieldKey]; + return this.layoutDoc[this.props.fieldKey]; } get childLayoutPairs(): { layout: Doc; data: Doc }[] { @@ -85,13 +81,13 @@ export function CollectionSubView<X>(moreProps?: X) { get childDocList() { return Cast(this.dataField, listSpec(Doc)); } - collectionFilters = () => this._focusFilters ?? StrListCast(this.props.Document._docFilters); - collectionRangeDocFilters = () => this._focusRangeFilters ?? Cast(this.props.Document._docRangeFilters, listSpec('string'), []); + collectionFilters = () => this._focusFilters ?? StrListCast(this.props.Document._childFilters); + collectionRangeDocFilters = () => this._focusRangeFilters ?? Cast(this.props.Document._childFiltersByRanges, listSpec('string'), []); // child filters apply to the descendants of the documents in this collection - childDocFilters = () => [...(this.props.docFilters?.().filter(f => Utils.IsRecursiveFilter(f)) || []), ...this.collectionFilters()]; + childDocFilters = () => [...(this.props.childFilters?.().filter(f => Utils.IsRecursiveFilter(f)) || []), ...this.collectionFilters()]; // unrecursive filters apply to the documents in the collection, but no their children. See Utils.noRecursionHack - unrecursiveDocFilters = () => [...(this.props.docFilters?.().filter(f => !Utils.IsRecursiveFilter(f)) || [])]; - childDocRangeFilters = () => [...(this.props.docRangeFilters?.() || []), ...this.collectionRangeDocFilters()]; + unrecursiveDocFilters = () => [...(this.props.childFilters?.().filter(f => !Utils.IsRecursiveFilter(f)) || [])]; + childDocRangeFilters = () => [...(this.props.childFiltersByRanges?.() || []), ...this.collectionRangeDocFilters()]; searchFilterDocs = () => this.props.searchFilterDocs?.() ?? DocListCast(this.props.Document._searchFilterDocs); @computed.struct get childDocs() { TraceMobx(); @@ -110,38 +106,41 @@ export function CollectionSubView<X>(moreProps?: X) { rawdocs = rootDoc && !this.props.isAnnotationOverlay ? [Doc.GetProto(rootDoc)] : []; } - const childDocs = rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate && (this.props.ignoreUnrendered || !d.unrendered)).map(d => d as Doc); + const childDocs = rawdocs.filter(d => !(d instanceof Promise) && GetEffectiveAcl(Doc.GetProto(d)) !== AclPrivate && (this.props.ignoreUnrendered || !d.layout_unrendered)).map(d => d as Doc); const childDocFilters = this.childDocFilters(); - const docRangeFilters = this.childDocRangeFilters(); + const childFiltersByRanges = this.childDocRangeFilters(); const searchDocs = this.searchFilterDocs(); - if (this.props.Document.dontRegisterView || (!childDocFilters.length && !this.unrecursiveDocFilters().length && !docRangeFilters.length && !searchDocs.length)) { + if (this.props.Document.dontRegisterView || (!childDocFilters.length && !this.unrecursiveDocFilters().length && !childFiltersByRanges.length && !searchDocs.length)) { return childDocs.filter(cd => !cd.cookies); // remove any documents that require a cookie if there are no filters to provide one } const docsforFilter: Doc[] = []; childDocs.forEach(d => { // dragging facets - const dragged = this.props.docFilters?.().some(f => f.includes(Utils.noDragsDocFilter)); + const dragged = this.props.childFilters?.().some(f => f.includes(Utils.noDragsDocFilter)); if (dragged && DragManager.docsBeingDragged.includes(d)) return false; - let notFiltered = d.z || Doc.IsSystem(d) || DocUtils.FilterDocs([d], this.unrecursiveDocFilters(), docRangeFilters, this.props.Document).length > 0; + let notFiltered = d.z || Doc.IsSystem(d) || DocUtils.FilterDocs([d], this.unrecursiveDocFilters(), childFiltersByRanges, this.props.Document).length > 0; if (notFiltered) { - notFiltered = (!searchDocs.length || searchDocs.includes(d)) && DocUtils.FilterDocs([d], childDocFilters, docRangeFilters, this.props.Document).length > 0; + notFiltered = (!searchDocs.length || searchDocs.includes(d)) && DocUtils.FilterDocs([d], childDocFilters, childFiltersByRanges, this.props.Document).length > 0; const fieldKey = Doc.LayoutFieldKey(d); const annos = !Field.toString(Doc.LayoutField(d) as Field).includes(CollectionView.name); - const data = d[annos ? fieldKey + '-annotations' : fieldKey]; - if (data !== undefined) { - let subDocs = DocListCast(data); + const data = d[annos ? fieldKey + '_annotations' : fieldKey]; + const side = annos && d[fieldKey + '_sidebar']; + if (data !== undefined || side !== undefined) { + let subDocs = [...DocListCast(data), ...DocListCast(side)]; if (subDocs.length > 0) { let newarray: Doc[] = []; - notFiltered = notFiltered || (!searchDocs.length && DocUtils.FilterDocs(subDocs, childDocFilters, docRangeFilters, d).length); + notFiltered = notFiltered || (!searchDocs.length && DocUtils.FilterDocs(subDocs, childDocFilters, childFiltersByRanges, d).length); while (subDocs.length > 0 && !notFiltered) { newarray = []; subDocs.forEach(t => { const fieldKey = Doc.LayoutFieldKey(t); const annos = !Field.toString(Doc.LayoutField(t) as Field).includes(CollectionView.name); - notFiltered = notFiltered || ((!searchDocs.length || searchDocs.includes(t)) && ((!childDocFilters.length && !docRangeFilters.length) || DocUtils.FilterDocs([t], childDocFilters, docRangeFilters, d).length)); - DocListCast(t[annos ? fieldKey + '-annotations' : fieldKey]).forEach(newdoc => newarray.push(newdoc)); + notFiltered = + notFiltered || ((!searchDocs.length || searchDocs.includes(t)) && ((!childDocFilters.length && !childFiltersByRanges.length) || DocUtils.FilterDocs([t], childDocFilters, childFiltersByRanges, d).length)); + DocListCast(t[annos ? fieldKey + '_annotations' : fieldKey]).forEach(newdoc => newarray.push(newdoc)); + annos && DocListCast(t[fieldKey + '_sidebar']).forEach(newdoc => newarray.push(newdoc)); }); subDocs = newarray; } @@ -188,9 +187,9 @@ export function CollectionSubView<X>(moreProps?: X) { protected onInternalPreDrop(e: Event, de: DragManager.DropEvent, targetAction: dropActionType) { if (de.complete.docDragData) { - // if targetDropAction is, say 'alias', but we're just dragging within a collection, we want to ignore the targetAction. + // if targetDropAction is, say 'embed', but we're just dragging within a collection, we want to ignore the targetAction. // otherwise, the targetAction should become the actual action (which can still be overridden by the userDropAction -eg, shift/ctrl keys) - if (targetAction && !de.complete.docDragData.draggedDocuments.some(d => d.context === this.props.Document && this.childDocs.includes(d))) { + if (targetAction && !de.complete.docDragData.draggedDocuments.some(d => d.embedContainer === this.props.Document && this.childDocs.includes(d))) { de.complete.docDragData.dropAction = targetAction; } e.stopPropagation(); @@ -213,7 +212,7 @@ export function CollectionSubView<X>(moreProps?: X) { const movedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] === d); const addedDocs = docDragData.droppedDocuments.filter((d, i) => docDragData.draggedDocuments[i] !== d); if (movedDocs.length) { - const canAdd = this.props.Document._viewType === CollectionViewType.Pile || de.embedKey || this.props.Document.allowOverlayDrop || Doc.AreProtosEqual(Cast(movedDocs[0].annotationOn, Doc, null), this.props.Document); + const canAdd = this.props.Document._type_collection === CollectionViewType.Pile || de.embedKey || this.props.Document.allowOverlayDrop || Doc.AreProtosEqual(Cast(movedDocs[0].annotationOn, Doc, null), this.props.Document); added = docDragData.moveDocument(movedDocs, this.props.Document, canAdd ? this.addDocument : returnFalse); } else { ScriptCast(this.props.Document.dropConverter)?.script.run({ dragData: docDragData }); @@ -282,7 +281,7 @@ export function CollectionSubView<X>(moreProps?: X) { addDocument(Docs.Create.WebDocument(href, { ...options, title: href })); } } else if (text) { - addDocument(Docs.Create.TextDocument(text, { ...options, _showTitle: StrCast(Doc.UserDoc().showTitle), _width: 100, _height: 25 })); + addDocument(Docs.Create.TextDocument(text, { ...options, _layout_showTitle: StrCast(Doc.UserDoc().layout_showTitle), _width: 100, _height: 25 })); } return; } @@ -375,7 +374,7 @@ export function CollectionSubView<X>(moreProps?: X) { _width: 400, _height: 512, _nativeWidth: 850, - useCors: true, + data_useCors: true, }) ); return; @@ -417,7 +416,7 @@ export function CollectionSubView<X>(moreProps?: X) { proto._height = 20; return proto; }), - { _width: 150, _height: 600, title: 'across', backgroundColor: 'white', _singleLine: true } + { _width: 150, _height: 600, title: 'across', backgroundColor: 'white', _createDocOnCR: true } ) ); }); @@ -448,7 +447,7 @@ export function CollectionSubView<X>(moreProps?: X) { } if (generatedDocuments.length) { // Creating a dash document - const isFreeformView = this.props.Document._viewType === CollectionViewType.Freeform; + const isFreeformView = this.props.Document._type_collection === CollectionViewType.Freeform; const set = !isFreeformView ? generatedDocuments : generatedDocuments.length > 1 diff --git a/src/client/views/collections/CollectionTimeView.tsx b/src/client/views/collections/CollectionTimeView.tsx index 3cdb460a3..60e6815e5 100644 --- a/src/client/views/collections/CollectionTimeView.tsx +++ b/src/client/views/collections/CollectionTimeView.tsx @@ -35,7 +35,7 @@ export class CollectionTimeView extends CollectionSubView() { async componentDidMount() { this.props.setContentView?.(this); //const detailView = (await DocCastAsync(this.props.Document.childClickedOpenTemplateView)) || DocUtils.findTemplate("detailView", StrCast(this.rootDoc.type), ""); - ///const childText = "const alias = getAlias(self); switchView(alias, detailView); alias.dropAction='alias'; useRightSplit(alias, shiftKey); "; + ///const childText = "const embedding = getEmbedding(self); switchView(embedding, detailView); embedding.dropAction='embed'; useRightSplit(embedding, shiftKey); "; runInAction(() => { this._childClickedScript = ScriptField.MakeScript('openInLightbox(self)', { this: Doc.name }); this._viewDefDivClick = ScriptField.MakeScript('pivotColumnClick(this,payload)', { payload: 'any' }); @@ -47,18 +47,18 @@ export class CollectionTimeView extends CollectionSubView() { } getAnchor = (addAsAnnotation: boolean) => { - const anchor = Docs.Create.HTMLAnchorDocument([], { + const anchor = Docs.Create.HTMLMarkerDocument([], { title: ComputedField.MakeFunction(`"${this.pivotField}"])`) as any, annotationOn: this.rootDoc, }); - PresBox.pinDocView(anchor, { pinData: { viewType: true, pivot: true, filters: true } }, this.rootDoc); + PresBox.pinDocView(anchor, { pinData: { type_collection: true, pivot: true, filters: true } }, this.rootDoc); if (addAsAnnotation) { // when added as an annotation, links to anchors can be found as links to the document even if the anchors are not rendered - if (Cast(this.dataDoc[this.props.fieldKey + '-annotations'], listSpec(Doc), null) !== undefined) { - Cast(this.dataDoc[this.props.fieldKey + '-annotations'], listSpec(Doc), []).push(anchor); + if (Cast(this.dataDoc[this.props.fieldKey + '_annotations'], listSpec(Doc), null) !== undefined) { + Cast(this.dataDoc[this.props.fieldKey + '_annotations'], listSpec(Doc), []).push(anchor); } else { - this.dataDoc[this.props.fieldKey + '-annotations'] = new List<Doc>([anchor]); + this.dataDoc[this.props.fieldKey + '_annotations'] = new List<Doc>([anchor]); } } return anchor; @@ -125,8 +125,8 @@ export class CollectionTimeView extends CollectionSubView() { goTo = (prevFilterIndex: number) => { this.layoutDoc._pivotField = this.layoutDoc['_prevPivotFields' + prevFilterIndex]; - this.layoutDoc._docFilters = ObjectField.MakeCopy(this.layoutDoc['_prevDocFilter' + prevFilterIndex] as ObjectField); - this.layoutDoc._docRangeFilters = ObjectField.MakeCopy(this.layoutDoc['_prevDocRangeFilters' + prevFilterIndex] as ObjectField); + this.layoutDoc._childFilters = ObjectField.MakeCopy(this.layoutDoc['_prevDocFilter' + prevFilterIndex] as ObjectField); + this.layoutDoc._childFiltersByRanges = ObjectField.MakeCopy(this.layoutDoc['_prevDocRangeFilters' + prevFilterIndex] as ObjectField); this.layoutDoc._prevFilterIndex = prevFilterIndex; }; @@ -136,7 +136,7 @@ export class CollectionTimeView extends CollectionSubView() { if (prevFilterIndex > 0) { this.goTo(prevFilterIndex - 1); } else { - this.layoutDoc._docFilters = new List([]); + this.layoutDoc._childFilters = new List([]); } }; @@ -145,7 +145,7 @@ export class CollectionTimeView extends CollectionSubView() { <div className="collectionTimeView-innards" key="timeline" style={{ pointerEvents: this.props.isContentActive() ? undefined : 'none' }} onClick={this.contentsDown}> <CollectionFreeFormView {...this.props} - engineProps={{ pivotField: this.pivotField, docFilters: this.childDocFilters, docRangeFilters: this.childDocRangeFilters }} + engineProps={{ pivotField: this.pivotField, childFilters: this.childDocFilters, childFiltersByRanges: this.childDocRangeFilters }} fitContentsToBox={returnTrue} childClickScript={this._childClickedScript} viewDefDivClick={this.layoutEngine() === computeTimelineLayout.name ? undefined : this._viewDefDivClick} @@ -276,12 +276,12 @@ export class CollectionTimeView extends CollectionSubView() { ScriptingGlobals.add(function pivotColumnClick(pivotDoc: Doc, bounds: ViewDefBounds) { const pivotField = StrCast(pivotDoc._pivotField, 'author'); let prevFilterIndex = NumCast(pivotDoc._prevFilterIndex); - const originalFilter = StrListCast(ObjectField.MakeCopy(pivotDoc._docFilters as ObjectField)); - pivotDoc['_prevDocFilter' + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._docFilters as ObjectField); - pivotDoc['_prevDocRangeFilters' + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._docRangeFilters as ObjectField); + const originalFilter = StrListCast(ObjectField.MakeCopy(pivotDoc._childFilters as ObjectField)); + pivotDoc['_prevDocFilter' + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._childFilters as ObjectField); + pivotDoc['_prevDocRangeFilters' + prevFilterIndex] = ObjectField.MakeCopy(pivotDoc._childFiltersByRanges as ObjectField); pivotDoc['_prevPivotFields' + prevFilterIndex] = pivotField; pivotDoc._prevFilterIndex = ++prevFilterIndex; - pivotDoc._docFilters = new List(); + pivotDoc._childFilters = new List(); setTimeout( action(() => { const filterVals = bounds.payload as string[]; @@ -292,7 +292,7 @@ ScriptingGlobals.add(function pivotColumnClick(pivotDoc: Doc, bounds: ViewDefBou pivotDoc._pivotField = filterVals[0]; } } - const newFilters = StrListCast(pivotDoc._docFilters); + const newFilters = StrListCast(pivotDoc._childFilters); if (newFilters.length && originalFilter.length && newFilters.lastElement() === originalFilter.lastElement()) { pivotDoc._prevFilterIndex = --prevFilterIndex; pivotDoc['_prevDocFilter' + prevFilterIndex] = undefined; diff --git a/src/client/views/collections/CollectionTreeView.tsx b/src/client/views/collections/CollectionTreeView.tsx index f81c17a7b..ed1e0c067 100644 --- a/src/client/views/collections/CollectionTreeView.tsx +++ b/src/client/views/collections/CollectionTreeView.tsx @@ -1,6 +1,7 @@ import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; -import { DataSym, Doc, DocListCast, HeightSym, Opt, StrListCast, WidthSym } from '../../../fields/Doc'; +import { Doc, DocListCast, Opt, StrListCast } from '../../../fields/Doc'; +import { DocData, Height, Width } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { listSpec } from '../../../fields/Schema'; @@ -104,7 +105,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree componentDidMount() { //this.props.setContentView?.(this); this._disposers.autoheight = reaction( - () => this.rootDoc.autoHeight, + () => this.rootDoc.layout_autoHeight, auto => auto && this.computeHeight(), { fireImmediately: true } ); @@ -114,25 +115,25 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree if (!this._isDisposing) { const titleHeight = !this._titleRef ? this.marginTop() : Number(getComputedStyle(this._titleRef).height.replace('px', '')); const bodyHeight = Array.from(this.refList).reduce((p, r) => p + Number(getComputedStyle(r).height.replace('px', '')), this.marginBot()) + 6; - this.layoutDoc._autoHeightMargins = bodyHeight; + this.layoutDoc._layout_autoHeightMargins = bodyHeight; !this.props.dontRegisterView && this.props.setHeight?.(bodyHeight + titleHeight); } }; unobserveHeight = (ref: any) => { this.refList.delete(ref); - this.rootDoc.autoHeight && this.computeHeight(); + this.rootDoc.layout_autoHeight && this.computeHeight(); }; observeHeight = (ref: any) => { if (ref) { this.refList.add(ref); this.observer = new _global.ResizeObserver( action((entries: any) => { - if (this.rootDoc.autoHeight && ref && this.refList.size && !SnappingManager.GetIsDragging()) { + if (this.rootDoc.layout_autoHeight && ref && this.refList.size && !SnappingManager.GetIsDragging()) { this.computeHeight(); } }) ); - this.rootDoc.autoHeight && this.computeHeight(); + this.rootDoc.layout_autoHeight && this.computeHeight(); this.observer.observe(ref); } }; @@ -144,7 +145,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree protected onInternalPreDrop = (e: Event, de: DragManager.DropEvent, targetAction: dropActionType) => { const dragData = de.complete.docDragData; if (dragData) { - const isInTree = () => Doc.AreProtosEqual(dragData.treeViewDoc, this.props.Document) || dragData.draggedDocuments.some(d => d.context === this.doc && this.childDocs.includes(d)); + const isInTree = () => Doc.AreProtosEqual(dragData.treeViewDoc, this.props.Document) || dragData.draggedDocuments.some(d => d.embedContainer === this.doc && this.childDocs.includes(d)); dragData.dropAction = targetAction && !isInTree() ? targetAction : this.doc === dragData?.treeViewDoc ? 'same' : dragData.dropAction; } }; @@ -154,15 +155,15 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree @action remove = (doc: Doc | Doc[]): boolean => { const docs = doc instanceof Doc ? [doc] : doc; - const targetDataDoc = this.doc[DataSym]; + const targetDataDoc = this.doc[DocData]; const value = DocListCast(targetDataDoc[this.props.fieldKey]); const result = value.filter(v => !docs.includes(v)); if ((doc instanceof Doc ? [doc] : doc).some(doc => SelectionManager.Views().some(dv => Doc.AreProtosEqual(dv.rootDoc, doc)))) SelectionManager.DeselectAll(); - if (result.length !== value.length) { - const ind = targetDataDoc[this.props.fieldKey].indexOf(doc); - const prev = ind && targetDataDoc[this.props.fieldKey][ind - 1]; + if (result.length !== value.length && doc instanceof Doc) { + const ind = DocListCast(targetDataDoc[this.props.fieldKey]).indexOf(doc); + const prev = ind && DocListCast(targetDataDoc[this.props.fieldKey])[ind - 1]; this.props.removeDocument?.(doc); - if (ind > 0) { + if (ind > 0 && prev) { FormattedTextBox.SelectOnLoad = prev[Id]; DocumentManager.Instance.getDocumentView(prev, this.props.DocumentView?.())?.select(false); } @@ -175,8 +176,8 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree addDoc = (docs: Doc | Doc[], relativeTo: Opt<Doc>, before?: boolean): boolean => { const doAddDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => { - const res = flg && Doc.AddDocToList(this.doc[DataSym], this.props.fieldKey, doc, relativeTo, before); - res && (doc.context = this.props.Document); + const res = flg && Doc.AddDocToList(this.doc[DocData], this.props.fieldKey, doc, relativeTo, before); + res && (doc.embedContainer = this.props.Document); return res; }, true); if (this.doc.resolvedDataDoc instanceof Promise) return false; @@ -229,18 +230,18 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree return ( <FormattedTextBox {...this.props} - fieldKey={'text'} + fieldKey="text" renderDepth={this.props.renderDepth + 1} isContentActive={this.isContentActive} isDocumentActive={this.isContentActive} rootSelected={returnTrue} - forceAutoHeight={true} // needed to make the title resize even if the rest of the tree view is not autoHeight + forceAutoHeight={true} // needed to make the title resize even if the rest of the tree view is not layout_autoHeight PanelWidth={this.documentTitleWidth} PanelHeight={this.documentTitleHeight} NativeDimScaling={returnOne} onKey={this.onKey} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} addDocument={returnFalse} moveDocument={returnFalse} @@ -313,12 +314,12 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree } @computed get noviceExplainer() { - return !Doc.noviceMode || !this.rootDoc.explainer ? null : <div className="documentExplanation"> {StrCast(this.rootDoc.explainer)} </div>; + return !Doc.noviceMode || !this.rootDoc.layout_explainer ? null : <div className="documentExplanation"> {StrCast(this.rootDoc.layout_explainer)} </div>; } return35 = () => 35; @computed get buttonMenu() { - const menuDoc = Cast(this.rootDoc.buttonMenuDoc, Doc, null); + const menuDoc = Cast(this.rootDoc.layout_headerButton, Doc, null); // To create a multibutton menu add a CollectionLinearView return !menuDoc ? null : ( <div className="buttonMenu-docBtn" style={{ width: NumCast(menuDoc._width, 30), height: NumCast(menuDoc._height, 30) }}> @@ -342,8 +343,8 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree docViewPath={returnEmptyDoclist} whenChildContentsActiveChanged={emptyFunction} bringToFront={emptyFunction} - docFilters={this.props.docFilters} - docRangeFilters={this.props.docRangeFilters} + childFilters={this.props.childFilters} + childFiltersByRanges={this.props.childFiltersByRanges} searchFilterDocs={this.props.searchFilterDocs} /> </div> @@ -368,16 +369,16 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree marginX = () => NumCast(this.doc._xMargin); marginTop = () => NumCast(this.doc._yMargin); marginBot = () => NumCast(this.doc._yMargin); - documentTitleWidth = () => Math.min(this.layoutDoc?.[WidthSym](), this.panelWidth()); - documentTitleHeight = () => (this.layoutDoc?.[HeightSym]() || 0) - NumCast(this.layoutDoc.autoHeightMargins); + documentTitleWidth = () => Math.min(this.layoutDoc?.[Width](), this.panelWidth()); + documentTitleHeight = () => (this.layoutDoc?.[Height]() || 0) - NumCast(this.layoutDoc.layout_autoHeightMargins); truncateTitleWidth = () => this.treeViewtruncateTitleWidth; onChildClick = () => this.props.onChildClick?.() || ScriptCast(this.doc.onChildClick); panelWidth = () => Math.max(0, this.props.PanelWidth() - 2 * this.marginX() * (this.props.NativeDimScaling?.() || 1)); - addAnnotationDocument = (doc: Doc | Doc[]) => this.addDocument(doc, `${this.props.fieldKey}-annotations`) || false; - remAnnotationDocument = (doc: Doc | Doc[]) => this.removeDocument(doc, `${this.props.fieldKey}-annotations`) || false; + addAnnotationDocument = (doc: Doc | Doc[]) => this.addDocument(doc, `${this.props.fieldKey}_annotations`) || false; + remAnnotationDocument = (doc: Doc | Doc[]) => this.removeDocument(doc, `${this.props.fieldKey}_annotations`) || false; moveAnnotationDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (document: Doc | Doc[], annotationKey?: string) => boolean) => - this.moveDocument(doc, targetCollection, addDocument, `${this.props.fieldKey}-annotations`) || false; + this.moveDocument(doc, targetCollection, addDocument, `${this.props.fieldKey}_annotations`) || false; @observable _headerHeight = 0; @computed get content() { @@ -430,7 +431,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree render() { TraceMobx(); - const scale = (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1) || 1; + const scale = (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1) || 1; return ( <div style={{ transform: `scale(${scale})`, transformOrigin: 'top left', width: `${100 / scale}%`, height: `${100 / scale}%` }}> {!(this.doc instanceof Doc) || !this.treeChildren ? null : this.doc.treeViewHasOverlay ? ( @@ -443,7 +444,7 @@ export class CollectionTreeView extends CollectionSubView<Partial<collectionTree isAnnotationOverlay={true} isAnnotationOverlayScrollable={true} childDocumentsActive={this.props.isDocumentActive} - fieldKey={this.props.fieldKey + '-annotations'} + fieldKey={this.props.fieldKey + '_annotations'} dropAction={'move'} select={emptyFunction} addDocument={this.addAnnotationDocument} diff --git a/src/client/views/collections/CollectionView.tsx b/src/client/views/collections/CollectionView.tsx index b76033a0c..7913d3188 100644 --- a/src/client/views/collections/CollectionView.tsx +++ b/src/client/views/collections/CollectionView.tsx @@ -47,8 +47,8 @@ interface CollectionViewProps_ extends FieldViewProps { childDocuments?: Doc[]; // used to override the documents shown by the sub collection to an explicit list (see LinkBox) childDocumentsActive?: () => boolean | undefined; // whether child documents can be dragged if collection can be dragged (eg., in a when a Pile document is in startburst mode) childContentsActive?: () => boolean | undefined; - childFitWidth?: (child: Doc) => boolean; - childShowTitle?: () => string; + childLayoutFitWidth?: (child: Doc) => boolean; + childlayout_showTitle?: () => string; childOpacity?: () => number; childContextMenuItems?: () => { script: ScriptField; label: string }[]; childHideTitle?: () => boolean; // whether to hide the documentdecorations title for children @@ -87,7 +87,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab } get collectionViewType(): CollectionViewType | undefined { - const viewField = StrCast(this.layoutDoc._viewType); + const viewField = StrCast(this.layoutDoc._type_collection); if (CollectionView._safeMode) { switch (viewField) { case CollectionViewType.Freeform: @@ -127,15 +127,15 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab } }; - setupViewTypes(category: string, func: (viewType: CollectionViewType) => Doc) { - if (!Doc.IsSystem(this.rootDoc) && this.rootDoc._viewType !== CollectionViewType.Docking && !this.rootDoc.isGroup && !this.rootDoc.annotationOn) { + setupViewTypes(category: string, func: (type_collection: CollectionViewType) => Doc) { + if (!Doc.IsSystem(this.rootDoc) && this.rootDoc._type_collection !== CollectionViewType.Docking && !this.rootDoc.isGroup && !this.rootDoc.annotationOn) { // prettier-ignore const subItems: ContextMenuProps[] = [ { description: 'Freeform', event: () => func(CollectionViewType.Freeform), icon: 'signature' }, { description: 'Schema', event: () => func(CollectionViewType.Schema), icon: 'th-list' }, { description: 'Tree', event: () => func(CollectionViewType.Tree), icon: 'tree' }, - { description: 'Stacking', event: () => (func(CollectionViewType.Stacking)._autoHeight = true), icon: 'ellipsis-v' }, - { description: 'Notetaking', event: () => (func(CollectionViewType.NoteTaking)._autoHeight = true), icon: 'ellipsis-v' }, + { description: 'Stacking', event: () => (func(CollectionViewType.Stacking)._layout_autoHeight = true), icon: 'ellipsis-v' }, + { description: 'Notetaking', event: () => (func(CollectionViewType.NoteTaking)._layout_autoHeight = true), icon: 'ellipsis-v' }, { description: 'Multicolumn', event: () => func(CollectionViewType.Multicolumn), icon: 'columns' }, { description: 'Multirow', event: () => func(CollectionViewType.Multirow), icon: 'columns' }, { description: 'Masonry', event: () => func(CollectionViewType.Masonry), icon: 'columns' }, @@ -159,8 +159,8 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab // need to test this because GoldenLayout causes a parallel hierarchy in the React DOM for its children and the main document view7 !Doc.noviceMode && this.setupViewTypes('UI Controls...', vtype => { - const newRendition = Doc.MakeAlias(this.rootDoc); - newRendition._viewType = vtype; + const newRendition = Doc.MakeEmbedding(this.rootDoc); + newRendition._type_collection = vtype; this.props.addDocTab(newRendition, OpenWhere.addRight); return newRendition; }); @@ -190,9 +190,9 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab description: `Edit ${func.name} script`, icon: 'edit', event: (obj: any) => { - const alias = Doc.MakeAlias(this.rootDoc); - DocUtils.makeCustomViewClicked(alias, undefined, func.key); - this.props.addDocTab(alias, OpenWhere.addRight); + const embedding = Doc.MakeEmbedding(this.rootDoc); + DocUtils.makeCustomViewClicked(embedding, undefined, func.key); + this.props.addDocTab(embedding, OpenWhere.addRight); }, }) ); @@ -244,7 +244,7 @@ export class CollectionView extends ViewBoxAnnotatableComponent<ViewBoxAnnotatab <div className="collectionView" onContextMenu={this.onContextMenu} - style={{ pointerEvents: this.props.DocumentView?.()?.props.docViewPath().lastElement()?.rootDoc?._viewType === CollectionViewType.Freeform && this.rootDoc._lockedPosition ? 'none' : undefined }}> + style={{ pointerEvents: this.props.DocumentView?.()?.props.docViewPath().lastElement()?.rootDoc?._type_collection === CollectionViewType.Freeform && this.rootDoc._lockedPosition ? 'none' : undefined }}> {this.renderSubView(this.collectionViewType, props)} </div> ); diff --git a/src/client/views/collections/TabDocView.tsx b/src/client/views/collections/TabDocView.tsx index 45604c1bf..75e4e8abf 100644 --- a/src/client/views/collections/TabDocView.tsx +++ b/src/client/views/collections/TabDocView.tsx @@ -5,22 +5,21 @@ import { clamp } from 'lodash'; import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as ReactDOM from 'react-dom/client'; -import { DataSym, Doc, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; +import { Doc, Opt } from '../../../fields/Doc'; +import { DocData, Height, Width } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { FieldId } from '../../../fields/RefField'; -import { listSpec } from '../../../fields/Schema'; import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../fields/Types'; import { emptyFunction, lightOrDark, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents, simulateMouseClick, Utils } from '../../../Utils'; import { DocServer } from '../../DocServer'; -import { DocUtils } from '../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager, dropActionType } from '../../util/DragManager'; import { SelectionManager } from '../../util/SelectionManager'; import { SnappingManager } from '../../util/SnappingManager'; import { Transform } from '../../util/Transform'; -import { undoBatch, UndoManager } from '../../util/UndoManager'; +import { undoable, UndoManager } from '../../util/UndoManager'; import { DashboardView } from '../DashboardView'; import { Colors, Shadows } from '../global/globalEnums'; import { LightboxView } from '../LightboxView'; @@ -116,15 +115,13 @@ export class TabDocView extends React.Component<TabDocViewProps> { titleEle.size = StrCast(doc.title).length + 3; titleEle.value = doc.title; - titleEle.onkeydown = (e: KeyboardEvent) => { - e.stopPropagation(); - }; - titleEle.onchange = undoBatch( - action((e: any) => { + titleEle.onkeydown = (e: KeyboardEvent) => e.stopPropagation(); + titleEle.onchange = (e: any) => { + undoable(() => { titleEle.size = e.currentTarget.value.length + 3; Doc.GetProto(doc).title = e.currentTarget.value; - }) - ); + }, 'edit tab title')(); + }; if (tab.element[0].children[1].children.length === 1) { iconWrap.className = 'lm_iconWrap lm_moreInfo'; @@ -198,7 +195,9 @@ export class TabDocView extends React.Component<TabDocViewProps> { action(selected => { if (selected) this._activated = true; const toggle = tab.element[0].children[2].children[0] as HTMLInputElement; - selected && tab.contentItem !== tab.header.parent.getActiveContentItem() && UndoManager.RunInBatch(() => tab.header.parent.setActiveContentItem(tab.contentItem), 'tab switch'); + if (selected && tab.contentItem !== tab.header.parent.getActiveContentItem()) { + undoable(() => tab.header.parent.setActiveContentItem(tab.contentItem), 'tab switch')(); + } toggle.style.fontWeight = selected ? 'bold' : ''; // toggle.style.textTransform = selected ? "uppercase" : ""; }), @@ -234,7 +233,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { public static PinDoc(docs: Doc | Doc[], pinProps: PinProps) { const docList = docs instanceof Doc ? [docs] : docs; - const batch = UndoManager.StartBatch('pinning doc'); + const batch = UndoManager.StartBatch('Pin doc to pres trail'); const curPres = Doc.ActivePresentation ?? Doc.MakeCopy(Doc.UserDoc().emptyTrail as Doc, true); if (!Doc.ActivePresentation) { @@ -252,11 +251,11 @@ export class TabDocView extends React.Component<TabDocViewProps> { const pinDoc = Doc.MakeDelegate(anchorDoc && anchorDoc !== doc ? anchorDoc : doc); pinDoc.presentationTargetDoc = anchorDoc ?? doc; pinDoc.title = doc.title + ' - Slide'; - pinDoc.data = new List<Doc>(); // the children of the alias' layout are the presentation slide children. the alias' data field might be children of a collection, PDF data, etc -- in any case we don't want the tree view to "see" this data + pinDoc.data = new List<Doc>(); // the children of the embedding's layout are the presentation slide children. the embedding's data field might be children of a collection, PDF data, etc -- in any case we don't want the tree view to "see" this data pinDoc.presMovement = doc.type === DocumentType.SCRIPTING || pinProps?.pinDocLayout ? PresMovement.None : PresMovement.Zoom; pinDoc.presDuration = pinDoc.presDuration ?? 1000; pinDoc.groupWithUp = false; - pinDoc.context = curPres; + pinDoc.embedContainer = curPres; // these should potentially all be props passed down by the CollectionTreeView to the TreeView elements. That way the PresBox could configure all of its children at render time pinDoc.treeViewRenderAsBulletHeader = true; // forces a tree view to render the document next to the bullet in the header area pinDoc.treeViewHeaderWidth = '100%'; // forces the header to grow to be the same size as its largest sibling. @@ -264,7 +263,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { pinDoc.treeViewFieldKey = 'data'; // tree view will treat the 'data' field as the field where the hierarchical children are located instead of using the document's layout string field pinDoc.treeViewExpandedView = 'data'; // in case the data doc has an expandedView set, this will mask that field and use the 'data' field when expanding the tree view pinDoc.treeViewHideHeaderIfTemplate = true; // this will force the document to render itself as the tree view header - const duration = NumCast(doc[`${Doc.LayoutFieldKey(pinDoc)}-duration`], null); + const duration = NumCast(doc[`${Doc.LayoutFieldKey(pinDoc)}_duration`], null); if (pinProps.pinViewport) PresBox.pinDocView(pinDoc, pinProps, anchorDoc ?? doc); if (!pinProps?.audioRange && duration !== undefined) { @@ -283,7 +282,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { pinDoc.title = doc.title + ' (move)'; pinDoc.presMovement = PresMovement.Pan; } - if (pinDoc.isInkMask) { + if (pinDoc.stroke_isInkMask) { pinDoc.presHideAfter = true; pinDoc.presHideBefore = true; pinDoc.presMovement = PresMovement.None; @@ -294,12 +293,12 @@ export class TabDocView extends React.Component<TabDocViewProps> { pinDoc && PresBox.Instance?.addToSelectedArray(pinDoc); //Update selected array }); if ( + // open the presentation trail if it's not already opened !Array.from(CollectionDockingView.Instance?.tabMap ?? []) .map(d => d.DashDoc) .includes(curPres) ) { - const docs = Cast(Doc.MyOverlayDocs.data, listSpec(Doc), []); - if (docs.includes(curPres)) docs.splice(docs.indexOf(curPres), 1); + if (Doc.IsInMyOverlay(curPres)) Doc.RemFromMyOverlay(curPres); CollectionDockingView.AddSplit(curPres, OpenWhereMod.right); setTimeout(() => DocumentManager.Instance.showDocument(docList.lastElement(), { willPan: true }), 100); // keeps the pinned doc in view since the sidebar shifts things } @@ -349,7 +348,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { // lightbox - will add the document to any collection along the path from the document to the docking view that has a field isLightbox. if none is found, it adds to the full screen lightbox addDocTab = (doc: Doc, location: OpenWhere) => { SelectionManager.DeselectAll(); - const whereFields = doc._viewType === CollectionViewType.Docking ? [OpenWhere.dashboard] : location.split(':'); + const whereFields = doc._type_collection === CollectionViewType.Docking ? [OpenWhere.dashboard] : location.split(':'); const keyValue = whereFields[1]?.includes('KeyValue'); const whereMods: OpenWhereMod = whereFields.length > 1 ? (whereFields[1].replace('KeyValue', '') as OpenWhereMod) : OpenWhereMod.none; if (doc.dockingConfig) return DashboardView.openDashboard(doc); @@ -357,7 +356,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { switch (whereFields[0]) { case undefined: case OpenWhere.lightbox: if (this.layoutDoc?._isLightbox) { - const lightboxView = !doc.annotationOn && DocCast(doc.context) ? DocumentManager.Instance.getFirstDocumentView(DocCast(doc.context)) : undefined; + const lightboxView = !doc.annotationOn && DocCast(doc.embedContainer) ? DocumentManager.Instance.getFirstDocumentView(DocCast(doc.embedContainer)) : undefined; const data = lightboxView?.dataDoc[Doc.LayoutFieldKey(lightboxView.rootDoc)]; if (lightboxView && (!data || data instanceof List)) { lightboxView.layoutDoc[Doc.LayoutFieldKey(lightboxView.rootDoc)] = new List<Doc>([doc]); @@ -383,7 +382,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { }; getCurrentFrame = () => { - return NumCast(Cast(PresBox.Instance.childDocs[PresBox.Instance.itemIndex].presentationTargetDoc, Doc, null)._currentFrame); + return NumCast(Cast(PresBox.Instance.activeItem.presentationTargetDoc, Doc, null)._currentFrame); }; static Activate = (tabDoc: Doc) => { const tab = Array.from(CollectionDockingView.Instance?.tabMap!).find(tab => tab.DashDoc === tabDoc); @@ -408,8 +407,8 @@ export class TabDocView extends React.Component<TabDocViewProps> { PanelHeight = () => this._panelHeight; miniMapColor = () => this.tabColor; tabView = () => this._view; - disableMinimap = () => !this._document || this._document.layout !== CollectionView.LayoutString(Doc.LayoutFieldKey(this._document)) || this._document?._viewType !== CollectionViewType.Freeform; - hideMinimap = () => this.disableMinimap() || BoolCast(this._document?.hideMinimap); + disableMinimap = () => !this._document || this._document.layout !== CollectionView.LayoutString(Doc.LayoutFieldKey(this._document)) || this._document?._type_collection !== CollectionViewType.Freeform; + hideMinimap = () => this.disableMinimap() || BoolCast(this._document?.layout_hideMinimap); @computed get docView() { return !this._activated || !this._document ? null : ( @@ -425,7 +424,7 @@ export class TabDocView extends React.Component<TabDocViewProps> { LayoutTemplateString={this.props.keyValue ? KeyValueBox.LayoutString() : undefined} hideTitle={this.props.keyValue} Document={this._document} - DataDoc={!Doc.AreProtosEqual(this._document[DataSym], this._document) ? this._document[DataSym] : undefined} + DataDoc={!Doc.AreProtosEqual(this._document[DocData], this._document) ? this._document[DocData] : undefined} onBrowseClick={MainView.Instance.exploreMode} waitForDoubleClickToClick={MainView.Instance.waitForDoubleClick} isContentActive={returnTrue} @@ -433,8 +432,8 @@ export class TabDocView extends React.Component<TabDocViewProps> { PanelWidth={this.PanelWidth} PanelHeight={this.PanelHeight} styleProvider={DefaultStyleProvider} - docFilters={CollectionDockingView.Instance?.childDocFilters ?? returnEmptyDoclist} - docRangeFilters={CollectionDockingView.Instance?.childDocRangeFilters ?? returnEmptyDoclist} + childFilters={CollectionDockingView.Instance?.childDocFilters ?? returnEmptyDoclist} + childFiltersByRanges={CollectionDockingView.Instance?.childDocRangeFilters ?? returnEmptyDoclist} searchFilterDocs={CollectionDockingView.Instance?.searchFilterDocs ?? returnEmptyDoclist} addDocument={undefined} removeDocument={this.remDocTab} @@ -449,19 +448,19 @@ export class TabDocView extends React.Component<TabDocViewProps> { pinToPres={TabDocView.PinDoc} /> <TabMinimapView key="minimap" hideMinimap={this.hideMinimap} addDocTab={this.addDocTab} PanelHeight={this.PanelHeight} PanelWidth={this.PanelWidth} background={this.miniMapColor} document={this._document} tabView={this.tabView} /> - <Tooltip key="ttip" title={<div className="dash-tooltip">{this._document.hideMinimap ? 'Open minimap' : 'Close minimap'}</div>}> + <Tooltip key="ttip" title={<div className="dash-tooltip">{this._document.layout_hideMinimap ? 'Open minimap' : 'Close minimap'}</div>}> <div className="miniMap-hidden" style={{ - display: this.disableMinimap() || this._document._viewType !== 'freeform' ? 'none' : undefined, - color: this._document.hideMinimap ? Colors.BLACK : Colors.WHITE, - backgroundColor: this._document.hideMinimap ? Colors.LIGHT_GRAY : Colors.MEDIUM_BLUE, - boxShadow: this._document.hideMinimap ? Shadows.STANDARD_SHADOW : undefined, + display: this.disableMinimap() || this._document._type_collection !== 'freeform' ? 'none' : undefined, + color: this._document.layout_hideMinimap ? Colors.BLACK : Colors.WHITE, + backgroundColor: this._document.layout_hideMinimap ? Colors.LIGHT_GRAY : Colors.MEDIUM_BLUE, + boxShadow: this._document.layout_hideMinimap ? Shadows.STANDARD_SHADOW : undefined, }} onPointerDown={e => e.stopPropagation()} onClick={action(e => { e.stopPropagation(); - this._document!.hideMinimap = !this._document!.hideMinimap; + this._document!.layout_hideMinimap = !this._document!.layout_hideMinimap; })}> <FontAwesomeIcon icon={'globe-asia'} size="lg" /> </div> @@ -535,7 +534,7 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> { return 'gray'; } })(doc.type as DocumentType); - return !background ? undefined : <div style={{ width: doc[WidthSym](), height: doc[HeightSym](), position: 'absolute', display: 'block', background }} />; + return !background ? undefined : <div style={{ width: doc[Width](), height: doc[Height](), position: 'absolute', display: 'block', background }} />; } } }; @@ -559,8 +558,8 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> { this, e, action((e: PointerEvent, down: number[], delta: number[]) => { - doc._panX = clamp(NumCast(doc._panX) + (delta[0] / miniSize) * renderBounds.dim, renderBounds.l, renderBounds.l + renderBounds.dim); - doc._panY = clamp(NumCast(doc._panY) + (delta[1] / miniSize) * renderBounds.dim, renderBounds.t, renderBounds.t + renderBounds.dim); + doc._freeform_panX = clamp(NumCast(doc._freeform_panX) + (delta[0] / miniSize) * renderBounds.dim, renderBounds.l, renderBounds.l + renderBounds.dim); + doc._freeform_panY = clamp(NumCast(doc._freeform_panY) + (delta[1] / miniSize) * renderBounds.dim, renderBounds.t, renderBounds.t + renderBounds.dim); return false; }), emptyFunction, @@ -569,10 +568,10 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> { }; render() { if (!this.renderBounds) return null; - const miniWidth = (this.props.PanelWidth() / NumCast(this.props.document._viewScale, 1) / this.renderBounds.dim) * 100; - const miniHeight = (this.props.PanelHeight() / NumCast(this.props.document._viewScale, 1) / this.renderBounds.dim) * 100; - const miniLeft = 50 + ((NumCast(this.props.document._panX) - this.renderBounds.cx) / this.renderBounds.dim) * 100 - miniWidth / 2; - const miniTop = 50 + ((NumCast(this.props.document._panY) - this.renderBounds.cy) / this.renderBounds.dim) * 100 - miniHeight / 2; + const miniWidth = (this.props.PanelWidth() / NumCast(this.props.document._freeform_scale, 1) / this.renderBounds.dim) * 100; + const miniHeight = (this.props.PanelHeight() / NumCast(this.props.document._freeform_scale, 1) / this.renderBounds.dim) * 100; + const miniLeft = 50 + ((NumCast(this.props.document._freeform_) - this.renderBounds.cx) / this.renderBounds.dim) * 100 - miniWidth / 2; + const miniTop = 50 + ((NumCast(this.props.document._freeform_panY) - this.renderBounds.cy) / this.renderBounds.dim) * 100 - miniHeight / 2; const miniSize = this.returnMiniSize(); return this.props.hideMinimap() ? null : ( <div className="miniMap" style={{ width: miniSize, height: miniSize, background: this.props.background() }}> @@ -603,8 +602,8 @@ export class TabMinimapView extends React.Component<TabMinimapViewProps> { styleProvider={TabMinimapView.miniStyleProvider} addDocTab={this.props.addDocTab} pinToPres={TabDocView.PinDoc} - docFilters={CollectionDockingView.Instance?.childDocFilters ?? returnEmptyDoclist} - docRangeFilters={CollectionDockingView.Instance?.childDocRangeFilters ?? returnEmptyDoclist} + childFilters={CollectionDockingView.Instance?.childDocFilters ?? returnEmptyDoclist} + childFiltersByRanges={CollectionDockingView.Instance?.childDocRangeFilters ?? returnEmptyDoclist} searchFilterDocs={CollectionDockingView.Instance?.searchFilterDocs ?? returnEmptyDoclist} fitContentsToBox={returnTrue} /> diff --git a/src/client/views/collections/TreeView.tsx b/src/client/views/collections/TreeView.tsx index 4adf86683..6bc30c451 100644 --- a/src/client/views/collections/TreeView.tsx +++ b/src/client/views/collections/TreeView.tsx @@ -2,7 +2,8 @@ import { IconProp } from '@fortawesome/fontawesome-svg-core'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; -import { DataSym, Doc, DocListCast, Field, HeightSym, Opt, StrListCast, WidthSym } from '../../../fields/Doc'; +import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../fields/Doc'; +import { DocData, Height, Width } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { List } from '../../../fields/List'; import { RichTextField } from '../../../fields/RichTextField'; @@ -10,7 +11,7 @@ import { listSpec } from '../../../fields/Schema'; import { ComputedField, ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, DocCast, FieldValue, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, returnZero, simulateMouseClick, Utils } from '../../../Utils'; +import { emptyFunction, return18, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnTrue, returnZero, simulateMouseClick, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { CollectionViewType, DocumentType } from '../../documents/DocumentTypes'; import { DocumentManager } from '../../util/DocumentManager'; @@ -102,7 +103,7 @@ export class TreeView extends React.Component<TreeViewProps> { private _treedropDisposer?: DragManager.DragDropDisposer; get treeViewOpenIsTransient() { - return this.props.treeView.doc.treeViewOpenIsTransient || Doc.IsPrototype(this.doc); + return this.props.treeView.doc.treeViewOpenIsTransient || Doc.IsDataProto(this.doc); } set treeViewOpen(c: boolean) { if (this.treeViewOpenIsTransient) this._transientOpenState = c; @@ -118,14 +119,14 @@ export class TreeView extends React.Component<TreeViewProps> { return 'TreeView(' + this.props.document.title + ')'; } // this makes mobx trace() statements more descriptive get defaultExpandedView() { - return this.doc.viewType === CollectionViewType.Docking + return this.doc._type_collection === CollectionViewType.Docking ? this.fieldKey : this.props.treeView.dashboardMode ? this.fieldKey : this.props.treeView.fileSysMode ? this.doc.isFolder ? this.fieldKey - : 'aliases' // for displaying + : 'embeddings' // for displaying : this.props.treeView.outlineMode || this.childDocs ? this.fieldKey : Doc.noviceMode @@ -146,7 +147,7 @@ export class TreeView extends React.Component<TreeViewProps> { return NumCast(this.props.treeViewParent.maxEmbedHeight, 200); } @computed get dataDoc() { - return this.props.document.treeViewChildrenOnRoot ? this.doc : this.doc[DataSym]; + return this.props.document.treeViewChildrenOnRoot ? this.doc : this.doc[DocData]; } @computed get layoutDoc() { return Doc.Layout(this.doc); @@ -160,11 +161,11 @@ export class TreeView extends React.Component<TreeViewProps> { @computed get childLinks() { return this.childDocList('links'); } - @computed get childAliases() { - return this.childDocList('aliases'); + @computed get childEmbeddings() { + return this.childDocList('proto_embeddings'); } @computed get childAnnos() { - return this.childDocList(this.fieldKey + '-annotations'); + return this.childDocList(this.fieldKey + '_annotations'); } @computed get selected() { return SelectionManager.IsSelected(this._docRef); @@ -191,10 +192,10 @@ export class TreeView extends React.Component<TreeViewProps> { }; @undoBatch @action remove = (doc: Doc | Doc[], key: string) => { this.props.treeView.props.select(false); - const ind = this.dataDoc[key].indexOf(doc); + const ind = DocListCast(this.dataDoc[key]).indexOf(doc instanceof Doc ? doc : doc.lastElement()); const res = (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && Doc.RemoveDocFromList(this.dataDoc, key, doc), true); - res && ind > 0 && DocumentManager.Instance.getDocumentView(this.dataDoc[key][ind - 1], this.props.treeView.props.DocumentView?.())?.select(false); + res && ind > 0 && DocumentManager.Instance.getDocumentView(DocListCast(this.dataDoc[key])[ind - 1], this.props.treeView.props.DocumentView?.())?.select(false); return res; }; @@ -219,11 +220,13 @@ export class TreeView extends React.Component<TreeViewProps> { if (this.props.document.isFolder || Doc.IsSystem(this.props.document)) { this.treeViewOpen = !this.treeViewOpen; } else { - // choose an appropriate alias or make one. --- choose the first alias that (1) user owns, (2) has no context field ... otherwise make a new alias - const bestAlias = - docView.props.Document.author === Doc.CurrentUserEmail && !Doc.IsPrototype(docView.props.Document) ? docView.props.Document : DocListCast(this.props.document.aliases).find(doc => !doc.context && doc.author === Doc.CurrentUserEmail); - const nextBestAlias = DocListCast(this.props.document.aliases).find(doc => doc.author === Doc.CurrentUserEmail); - this.props.addDocTab(bestAlias ?? nextBestAlias ?? Doc.MakeAlias(this.props.document), OpenWhere.lightbox); + // choose an appropriate embedding or make one. --- choose the first embedding that (1) user owns, (2) has no context field ... otherwise make a new embedding + const bestEmbedding = + docView.props.Document.author === Doc.CurrentUserEmail && !Doc.IsDataProto(docView.props.Document) + ? docView.props.Document + : DocListCast(this.props.document.proto_embeddings).find(doc => !doc.embedContainer && doc.author === Doc.CurrentUserEmail); + const nextBestEmbedding = DocListCast(this.props.document.proto_embeddings).find(doc => doc.author === Doc.CurrentUserEmail); + this.props.addDocTab(bestEmbedding ?? nextBestEmbedding ?? Doc.MakeEmbedding(this.props.document), OpenWhere.lightbox); } }; @@ -310,17 +313,17 @@ export class TreeView extends React.Component<TreeViewProps> { title: '-title-', treeViewExpandedViewLock: true, treeViewExpandedView: 'data', - _viewType: CollectionViewType.Tree, - hideLinkButton: true, - _showSidebar: true, - _fitWidth: true, + _type_collection: CollectionViewType.Tree, + layout_hideLinkButton: true, + _layout_showSidebar: true, + _layout_fitWidth: true, treeViewType: TreeViewType.outline, x: 0, y: 0, _xMargin: 0, _yMargin: 0, - _autoHeight: true, - _singleLine: true, + _layout_autoHeight: true, + _createDocOnCR: true, _width: 1000, _height: 10, }); @@ -359,7 +362,7 @@ export class TreeView extends React.Component<TreeViewProps> { if (de.complete.linkDragData) { const sourceDoc = de.complete.linkDragData.linkSourceGetAnchor(); const destDoc = this.doc; - DocUtils.MakeLink(sourceDoc, destDoc, { linkRelationship: 'tree link' }); + DocUtils.MakeLink(sourceDoc, destDoc, { link_relationship: 'tree link' }); e.stopPropagation(); } const docDragData = de.complete.docDragData; @@ -378,14 +381,14 @@ export class TreeView extends React.Component<TreeViewProps> { const innerAdd = (doc: Doc) => { const dataIsComputed = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc[this.fieldKey])) instanceof ComputedField; const added = (!dataIsComputed || (this.dropping && this.moving)) && Doc.AddDocToList(this.dataDoc, this.fieldKey, doc); - dataIsComputed && (doc.context = this.doc.context); + dataIsComputed && (doc.embedContainer = this.doc.embedContainer); return added; }; return (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && innerAdd(doc), true as boolean); }; const addDoc = inside ? localAdd : parentAddDoc; const move = (!dropAction || dropAction === 'proto' || dropAction === 'move' || dropAction === 'same') && moveDocument; - const canAdd = (!this.props.treeView.outlineMode && !StrCast((inside ? this.props.document : this.props.treeViewParent)?.freezeChildren).includes('add')) || forceAdd; + const canAdd = (!this.props.treeView.outlineMode && !StrCast((inside ? this.props.document : this.props.treeViewParent)?.treeViewFreezeChildren).includes('add')) || forceAdd; if (canAdd) { this.props.parentTreeView instanceof TreeView && (this.props.parentTreeView.dropping = true); const res = UndoManager.RunInTempBatch(() => droppedDocuments.reduce((added, d) => (move ? move(d, undefined, addDoc) || (dropAction === 'proto' ? addDoc(d) : false) : addDoc(d)) || added, false)); @@ -408,16 +411,16 @@ export class TreeView extends React.Component<TreeViewProps> { embeddedPanelHeight = () => { const layoutDoc = (temp => temp && Doc.expandTemplateLayout(temp, this.props.document))(this.props.treeView.props.childLayoutTemplate?.()) || this.layoutDoc; return Math.min( - layoutDoc[HeightSym](), + layoutDoc[Height](), this.MAX_EMBED_HEIGHT, (() => { const aspect = Doc.NativeAspect(layoutDoc); if (aspect) return this.embeddedPanelWidth() / (aspect || 1); - return layoutDoc._fitWidth + return layoutDoc._layout_fitWidth ? !Doc.NativeHeight(layoutDoc) ? NumCast(layoutDoc._height) : Math.min((this.embeddedPanelWidth() * NumCast(layoutDoc.scrollHeight, Doc.NativeHeight(layoutDoc))) / (Doc.NativeWidth(layoutDoc) || NumCast(this.props.treeViewParent._height))) - : (this.embeddedPanelWidth() * layoutDoc[HeightSym]()) / layoutDoc[WidthSym](); + : (this.embeddedPanelWidth() * layoutDoc[Height]()) / layoutDoc[Width](); })() ); }; @@ -441,7 +444,7 @@ export class TreeView extends React.Component<TreeViewProps> { const innerAdd = (doc: Doc) => { const dataIsComputed = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc[key])) instanceof ComputedField; const added = (!dataIsComputed || (this.dropping && this.moving)) && Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true); - dataIsComputed && (doc.context = this.doc.context); + dataIsComputed && (doc.embedContainer = this.doc.embedContainer); return added; }; return (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && innerAdd(doc), true as boolean); @@ -527,7 +530,7 @@ export class TreeView extends React.Component<TreeViewProps> { TraceMobx(); const expandKey = this.treeViewExpandedView; const sortings = (this.props.styleProvider?.(this.doc, this.props.treeView.props, StyleProp.TreeViewSortings) as { [key: string]: { color: string; label: string } }) ?? {}; - if (['links', 'annotations', 'aliases', this.fieldKey].includes(expandKey)) { + if (['links', 'annotations', 'embeddings', this.fieldKey].includes(expandKey)) { const sorting = StrCast(this.doc.treeViewSortCriterion, TreeSort.None); const sortKeys = Object.keys(sortings); const curSortIndex = Math.max( @@ -549,12 +552,12 @@ export class TreeView extends React.Component<TreeViewProps> { } const dataIsComputed = ComputedField.WithoutComputed(() => FieldValue(this.dataDoc[key])) instanceof ComputedField; const added = (!dataIsComputed || (this.dropping && this.moving)) && Doc.AddDocToList(this.dataDoc, key, doc, addBefore, before, false, true); - !dataIsComputed && added && (doc.context = this.doc.context); + !dataIsComputed && added && (doc.embedContainer = this.doc.embedContainer); return added; }; const addDoc = (doc: Doc | Doc[], addBefore?: Doc, before?: boolean) => (doc instanceof Doc ? [doc] : doc).reduce((flg, doc) => flg && localAdd(doc, addBefore, before), true); - const docs = expandKey === 'aliases' ? this.childAliases : expandKey === 'links' ? this.childLinks : expandKey === 'annotations' ? this.childAnnos : this.childDocs; + const docs = expandKey === 'embeddings' ? this.childEmbeddings : expandKey === 'links' ? this.childLinks : expandKey === 'annotations' ? this.childAnnos : this.childDocs; let downX = 0, downY = 0; if (docs?.length && this._renderCount < docs?.length) { @@ -670,7 +673,7 @@ export class TreeView extends React.Component<TreeViewProps> { @computed get renderBullet() { TraceMobx(); - const iconType = this.props.treeView.props.styleProvider?.(this.doc, this.props.treeView.props, StyleProp.TreeViewIcon + (this.treeViewOpen ? ':open' : '')) || 'question'; + const iconType = this.props.treeView.props.styleProvider?.(this.doc, this.props.treeView.props, StyleProp.TreeViewIcon + (this.treeViewOpen ? ':open' : !this.childDocs.length ? ':empty' : '')) || 'question'; const checked = this.onCheckedClick ? this.doc.treeViewChecked ?? 'unchecked' : undefined; return ( <div @@ -706,13 +709,13 @@ export class TreeView extends React.Component<TreeViewProps> { } @computed get validExpandViewTypes() { - const annos = () => (DocListCast(this.doc[this.fieldKey + '-annotations']).length && !this.props.treeView.dashboardMode ? 'annotations' : ''); + const annos = () => (DocListCast(this.doc[this.fieldKey + '_annotations']).length && !this.props.treeView.dashboardMode ? 'annotations' : ''); const links = () => (LinkManager.Links(this.doc).length && !this.props.treeView.dashboardMode ? 'links' : ''); const data = () => (this.childDocs || this.props.treeView.dashboardMode ? this.fieldKey : ''); - const aliases = () => (this.props.treeView.dashboardMode ? '' : 'aliases'); + const embeddings = () => (this.props.treeView.dashboardMode ? '' : 'embeddings'); const fields = () => (Doc.noviceMode ? '' : 'fields'); - const layout = Doc.noviceMode || this.doc.viewType === CollectionViewType.Docking ? [] : ['layout']; - return [data(), ...layout, ...(this.props.treeView.fileSysMode ? [aliases(), links(), annos()] : []), fields()].filter(m => m); + const layout = Doc.noviceMode || this.doc._type_collection === CollectionViewType.Docking ? [] : ['layout']; + return [data(), ...layout, ...(this.props.treeView.fileSysMode ? [embeddings(), links(), annos()] : []), fields()].filter(m => m); } @action expandNextviewType = () => { @@ -729,7 +732,7 @@ export class TreeView extends React.Component<TreeViewProps> { return this.props.treeViewHideHeaderFields() || this.doc.treeViewHideHeaderFields ? null : ( <> {customHeaderButtons} {/* e.g.,. hide button is set by dashboardStyleProvider */} - {this.doc.hideContextMenu ? null : ( + {this.doc._layout_hideContextMenu ? null : ( <FontAwesomeIcon title="context menu" key="bars" @@ -759,7 +762,7 @@ export class TreeView extends React.Component<TreeViewProps> { const makeFolder = { script: ScriptField.MakeFunction(`scriptContext.makeFolder()`, { scriptContext: 'any' })!, icon: 'folder-plus', label: 'New Folder' }; const deleteItem = { script: ScriptField.MakeFunction(`scriptContext.deleteItem()`, { scriptContext: 'any' })!, icon: 'folder-plus', label: 'Delete' }; const folderOp = this.childDocs?.length ? [makeFolder] : []; - const openAlias = { script: ScriptField.MakeFunction(`openDoc(getAlias(self), ${OpenWhere.addRight})`)!, icon: 'copy', label: 'Open Alias' }; + const openEmbedding = { script: ScriptField.MakeFunction(`openDoc(getEmbedding(self), "${OpenWhere.addRight}")`)!, icon: 'copy', label: 'Open New Embedding' }; const focusDoc = { script: ScriptField.MakeFunction(`DocFocusOrOpen(self)`)!, icon: 'eye', label: 'Focus or Open' }; return [ ...(this.props.contextMenuItems ?? []).filter(mi => (!mi.filter ? true : mi.filter.script.run({ doc: this.doc })?.result)), @@ -768,10 +771,10 @@ export class TreeView extends React.Component<TreeViewProps> { : Doc.IsSystem(this.doc) ? [] : this.props.treeView.fileSysMode && this.doc === Doc.GetProto(this.doc) - ? [openAlias, makeFolder] - : this.doc.viewType === CollectionViewType.Docking + ? [openEmbedding, makeFolder] + : this.doc._type_collection === CollectionViewType.Docking ? [] - : [deleteItem, openAlias, focusDoc]), + : [deleteItem, openEmbedding, focusDoc]), ]; }; childContextMenuItems = () => { @@ -854,7 +857,6 @@ export class TreeView extends React.Component<TreeViewProps> { }; titleWidth = () => Math.max(20, Math.min(this.props.treeView.truncateTitleWidth(), this.props.panelWidth())) / (this.props.treeView.props.NativeDimScaling?.() || 1) - this.headerEleWidth - treeBulletWidth(); - return18 = () => 18; /** * Renders the EditableView title element for placement into the tree. */ @@ -896,7 +898,7 @@ export class TreeView extends React.Component<TreeViewProps> { } })} Document={this.doc} - fitWidth={returnTrue} + layout_fitWidth={returnTrue} DataDoc={undefined} scriptContext={this} hideDecorationTitle={this.props.treeView.outlineMode} @@ -916,10 +918,11 @@ export class TreeView extends React.Component<TreeViewProps> { moveDocument={this.move} removeDocument={this.props.removeDoc} ScreenToLocalTransform={this.getTransform} - NativeHeight={this.return18} + NativeHeight={return18} NativeWidth={returnZero} + shouldNotScale={returnTrue} PanelWidth={this.titleWidth} - PanelHeight={this.return18} + PanelHeight={return18} contextMenuItems={this.contextMenuItems} renderDepth={1} isContentActive={this.props.isContentActive} @@ -927,13 +930,13 @@ export class TreeView extends React.Component<TreeViewProps> { focus={this.refocus} whenChildContentsActiveChanged={this.props.whenChildContentsActiveChanged} bringToFront={emptyFunction} - disableDocBrushing={this.props.treeView.props.disableDocBrushing} + disableBrushing={this.props.treeView.props.disableBrushing} hideLinkButton={BoolCast(this.props.treeView.props.Document.childHideLinkButton)} dontRegisterView={BoolCast(this.props.treeView.props.Document.childDontRegisterViews, this.props.dontRegisterView)} xPadding={NumCast(this.props.treeView.props.Document.childXPadding, this.props.treeView.props.childXPadding)} yPadding={NumCast(this.props.treeView.props.Document.childYPadding, this.props.treeView.props.childYPadding)} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} /> ); @@ -985,7 +988,7 @@ export class TreeView extends React.Component<TreeViewProps> { ref={action((r: DocumentView | null) => (this._dref = r))} Document={this.doc} DataDoc={undefined} - fitWidth={this.fitWidthFilter} + layout_fitWidth={this.fitWidthFilter} PanelWidth={this.embeddedPanelWidth} PanelHeight={this.embeddedPanelHeight} LayoutTemplateString={asText ? FormattedTextBox.LayoutString('text') : undefined} @@ -1007,8 +1010,8 @@ export class TreeView extends React.Component<TreeViewProps> { treeViewDoc={this.props.treeView?.props.Document} rootSelected={returnTrue} docViewPath={this.props.treeView.props.docViewPath} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} addDocument={this.props.addDocument} moveDocument={this.move} @@ -1018,7 +1021,7 @@ export class TreeView extends React.Component<TreeViewProps> { yPadding={NumCast(this.props.treeView.props.Document.childYPadding, this.props.treeView.props.childYPadding)} addDocTab={this.props.addDocTab} pinToPres={this.props.treeView.props.pinToPres} - disableDocBrushing={this.props.treeView.props.disableDocBrushing} + disableBrushing={this.props.treeView.props.disableBrushing} bringToFront={returnFalse} scriptContext={this} /> @@ -1028,7 +1031,7 @@ export class TreeView extends React.Component<TreeViewProps> { // renders the text version of a document as the header. This is used in the file system mode and in other vanilla tree views. @computed get renderTitleAsHeader() { - return this.props.treeView.Document.treeViewHideUnrendered && this.doc.unrendered && !this.doc.treeViewFieldKey ? ( + return this.props.treeView.Document.treeViewHideUnrendered && this.doc.layout_unrendered && !this.doc.treeViewFieldKey ? ( <div></div> ) : ( <> @@ -1179,7 +1182,7 @@ export class TreeView extends React.Component<TreeViewProps> { TreeView._editTitleOnLoad = editTitle ? { id: child[Id], parent } : undefined; Doc.AddDocToList(newParent, fieldKey, child, addAfter, false); newParent.treeViewOpen = true; - child.context = treeView.Document; + child.embedContainer = treeView.Document; } }; const indent = i === 0 ? undefined : (editTitle: boolean) => dentDoc(editTitle, docs[i - 1], undefined, treeViewRefs.get(docs[i - 1])); @@ -1188,7 +1191,7 @@ export class TreeView extends React.Component<TreeViewProps> { const childLayout = Doc.Layout(pair.layout); const rowHeight = () => { const aspect = Doc.NativeAspect(childLayout); - return aspect ? Math.min(childLayout[WidthSym](), rowWidth()) / aspect : childLayout[HeightSym](); + return aspect ? Math.min(childLayout[Width](), rowWidth()) / aspect : childLayout[Height](); }; return ( <TreeView @@ -1208,7 +1211,7 @@ export class TreeView extends React.Component<TreeViewProps> { onCheckedClick={onCheckedClick} onChildClick={onChildClick} renderDepth={renderDepth} - removeDoc={StrCast(treeViewParent.freezeChildren).includes('remove') ? undefined : remove} + removeDoc={StrCast(treeViewParent.treeViewFreezeChildren).includes('remove') ? undefined : remove} addDocument={addDocument} styleProvider={styleProvider} panelWidth={rowWidth} diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx index fee4705e6..1e76d373c 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLayoutEngines.tsx @@ -1,4 +1,5 @@ -import { Doc, Field, FieldResult, HeightSym, WidthSym } from '../../../../fields/Doc'; +import { Doc, Field, FieldResult } from '../../../../fields/Doc'; +import { Height, Width } from '../../../../fields/DocSymbols'; import { Id, ToString } from '../../../../fields/FieldSymbols'; import { ObjectField } from '../../../../fields/ObjectField'; import { RefField } from '../../../../fields/RefField'; @@ -90,8 +91,8 @@ export function computePassLayout(poolData: Map<string, PoolData>, pivotDoc: Doc docMap.set(layout[Id], { x: NumCast(layout.x), y: NumCast(layout.y), - width: layout[WidthSym](), - height: layout[HeightSym](), + width: layout[Width](), + height: layout[Height](), pair: { layout, data }, transition: 'all .3s', replica: '', @@ -105,8 +106,8 @@ export function computeStarburstLayout(poolData: Map<string, PoolData>, pivotDoc const burstDiam = [NumCast(pivotDoc._width), NumCast(pivotDoc._height)]; const burstScale = NumCast(pivotDoc._starburstDocScale, 1); childPairs.forEach(({ layout, data }, i) => { - const aspect = layout[HeightSym]() / layout[WidthSym](); - const docSize = Math.min(Math.min(400, layout[WidthSym]()), Math.min(400, layout[WidthSym]()) / aspect) * burstScale; + const aspect = layout[Height]() / layout[Width](); + const docSize = Math.min(Math.min(400, layout[Width]()), Math.min(400, layout[Width]()) / aspect) * burstScale; const deg = (i / childPairs.length) * Math.PI * 2; docMap.set(layout[Id], { x: Math.min(burstDiam[0] / 2 - docSize, Math.max(-burstDiam[0] / 2, (Math.cos(deg) * burstDiam[0]) / 2 - docSize / 2)), diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx index 0dfd119d7..f1d98d22a 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormLinkView.tsx @@ -1,6 +1,7 @@ import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; -import { CssSym, Doc, Field } from '../../../../fields/Doc'; +import { Doc, Field } from '../../../../fields/Doc'; +import { DocCss } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { List } from '../../../../fields/List'; import { Cast, NumCast, StrCast } from '../../../../fields/Types'; @@ -34,11 +35,11 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo this._anchorDisposer = reaction( () => [ this.props.A.props.ScreenToLocalTransform(), - Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.anchor1, Doc, null)?.annotationOn, Doc, null)?.scrollTop, - Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.anchor1, Doc, null)?.annotationOn, Doc, null)?.[CssSym], + Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.link_anchor_1, Doc, null)?.annotationOn, Doc, null)?.layout_scrollTop, + Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.link_anchor_1, Doc, null)?.annotationOn, Doc, null)?.[DocCss], this.props.B.props.ScreenToLocalTransform(), - Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.anchor2, Doc, null)?.annotationOn, Doc, null)?.scrollTop, - Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.anchor2, Doc, null)?.annotationOn, Doc, null)?.[CssSym], + Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.link_anchor_2, Doc, null)?.annotationOn, Doc, null)?.layout_scrollTop, + Cast(Cast(Cast(this.props.A.rootDoc, Doc, null)?.link_anchor_2, Doc, null)?.annotationOn, Doc, null)?.[DocCss], ], action(() => { this._start = Date.now(); @@ -58,7 +59,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo 0 ); // since the render code depends on querying the Dom through getBoudndingClientRect, we need to delay triggering render() setTimeout( - action(() => (!LinkDocs.length || !linkDoc.linkDisplay) && (this._opacity = 0.05)), + action(() => (!LinkDocs.length || !linkDoc.link_displayLine) && (this._opacity = 0.05)), 750 ); // this will unhighlight the link line. const a = A.ContentDiv.getBoundingClientRect(); @@ -72,36 +73,36 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo // if there's an element in the DOM with a classname containing a link anchor's id, // then that DOM element is a hyperlink source for the current anchor and we want to place our link box at it's top right // otherwise, we just use the computed nearest point on the document boundary to the target Document - const targetAhyperlink = Array.from(window.document.getElementsByClassName((linkDoc.anchor1 as Doc)[Id])).lastElement(); - const targetBhyperlink = Array.from(window.document.getElementsByClassName((linkDoc.anchor2 as Doc)[Id])).lastElement(); + const targetAhyperlink = Array.from(window.document.getElementsByClassName((linkDoc.link_anchor_1 as Doc)[Id])).lastElement(); + const targetBhyperlink = Array.from(window.document.getElementsByClassName((linkDoc.link_anchor_2 as Doc)[Id])).lastElement(); if ((!targetAhyperlink && !a.width) || (!targetBhyperlink && !b.width)) return; if (!targetAhyperlink) { - if (linkDoc.linkAutoMove) { - linkDoc.anchor1_x = ((apt.point.x - aleft) / awidth) * 100; - linkDoc.anchor1_y = ((apt.point.y - atop) / aheight) * 100; + if (linkDoc.link_autoMoveAnchors) { + linkDoc.link_anchor_1_x = ((apt.point.x - aleft) / awidth) * 100; + linkDoc.link_anchor_1_y = ((apt.point.y - atop) / aheight) * 100; } } else { const m = targetAhyperlink.getBoundingClientRect(); const mp = A.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5); const mpx = mp[0] / A.props.PanelWidth(); const mpy = mp[1] / A.props.PanelHeight(); - if (mpx >= 0 && mpx <= 1) linkDoc.anchor1_x = mpx * 100; - if (mpy >= 0 && mpy <= 1) linkDoc.anchor1_y = mpy * 100; + if (mpx >= 0 && mpx <= 1) linkDoc.link_anchor_1_x = mpx * 100; + if (mpy >= 0 && mpy <= 1) linkDoc.link_anchor_1_y = mpy * 100; if (getComputedStyle(targetAhyperlink).fontSize === '0px') linkDoc.opacity = 0; else linkDoc.opacity = 1; } if (!targetBhyperlink) { - if (linkDoc.linkAutoMove) { - linkDoc.anchor2_x = ((bpt.point.x - bleft) / bwidth) * 100; - linkDoc.anchor2_y = ((bpt.point.y - btop) / bheight) * 100; + if (linkDoc.link_autoMoveAnchors) { + linkDoc.link_anchor_2_x = ((bpt.point.x - bleft) / bwidth) * 100; + linkDoc.link_anchor_2_y = ((bpt.point.y - btop) / bheight) * 100; } } else { const m = targetBhyperlink.getBoundingClientRect(); const mp = B.props.ScreenToLocalTransform().transformPoint(m.right, m.top + 5); const mpx = mp[0] / B.props.PanelWidth(); const mpy = mp[1] / B.props.PanelHeight(); - if (mpx >= 0 && mpx <= 1) linkDoc.anchor2_x = mpx * 100; - if (mpy >= 0 && mpy <= 1) linkDoc.anchor2_y = mpy * 100; + if (mpx >= 0 && mpx <= 1) linkDoc.link_anchor_2_x = mpx * 100; + if (mpy >= 0 && mpy <= 1) linkDoc.link_anchor_2_y = mpy * 100; if (getComputedStyle(targetBhyperlink).fontSize === '0px') linkDoc.opacity = 0; else linkDoc.opacity = 1; } @@ -112,8 +113,8 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo this, e, (e, down, delta) => { - this.props.LinkDocs[0].linkOffsetX = NumCast(this.props.LinkDocs[0].linkOffsetX) + delta[0]; - this.props.LinkDocs[0].linkOffsetY = NumCast(this.props.LinkDocs[0].linkOffsetY) + delta[1]; + this.props.LinkDocs[0].link_relationship_OffsetX = NumCast(this.props.LinkDocs[0].link_relationship_OffsetX) + delta[0]; + this.props.LinkDocs[0].link_relationship_OffsetY = NumCast(this.props.LinkDocs[0].link_relationship_OffsetY) + delta[1]; return false; }, emptyFunction, @@ -205,8 +206,8 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo const bclipped = bleft !== b.left || btop !== b.top; if (aclipped && bclipped) return undefined; const clipped = aclipped || bclipped; - const pt1inside = NumCast(LinkDocs[0].anchor1_x) % 100 !== 0 && NumCast(LinkDocs[0].anchor1_y) % 100 !== 0; - const pt2inside = NumCast(LinkDocs[0].anchor2_x) % 100 !== 0 && NumCast(LinkDocs[0].anchor2_y) % 100 !== 0; + const pt1inside = NumCast(LinkDocs[0].link_anchor_1_x) % 100 !== 0 && NumCast(LinkDocs[0].link_anchor_1_y) % 100 !== 0; + const pt2inside = NumCast(LinkDocs[0].link_anchor_2_x) % 100 !== 0 && NumCast(LinkDocs[0].link_anchor_2_y) % 100 !== 0; const pt1 = [aleft + a.width / 2, atop + a.height / 2]; const pt2 = [bleft + b.width / 2, btop + b.width / 2]; const pt2vec = pt2inside ? [-0.7071, 0.7071] : [(bDocBounds.left + bDocBounds.right) / 2 - pt2[0], (bDocBounds.top + bDocBounds.bottom) / 2 - pt2[1]]; @@ -223,8 +224,8 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo const aActive = A.isSelected() || Doc.IsBrushed(A.rootDoc); const bActive = B.isSelected() || Doc.IsBrushed(B.rootDoc); - const textX = (Math.min(pt1[0], pt2[0]) + Math.max(pt1[0], pt2[0])) / 2 + NumCast(LinkDocs[0].linkOffsetX); - const textY = (pt1[1] + pt2[1]) / 2 + NumCast(LinkDocs[0].linkOffsetY); + const textX = (Math.min(pt1[0], pt2[0]) + Math.max(pt1[0], pt2[0])) / 2 + NumCast(LinkDocs[0].link_relationship_OffsetX); + const textY = (pt1[1] + pt2[1]) / 2 + NumCast(LinkDocs[0].link_relationship_OffsetY); return { a, b, @@ -244,12 +245,12 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo const link = this.props.LinkDocs[0]; const { a, b, pt1norm, pt2norm, aActive, bActive, textX, textY, pt1, pt2 } = this.renderData; - const linkRelationship = Field.toString(link?.linkRelationship as any as Field); //get string representing relationship - const linkRelationshipList = Doc.UserDoc().linkRelationshipList as List<string>; - const linkColorList = Doc.UserDoc().linkColorList as List<string>; - const linkRelationshipSizes = Doc.UserDoc().linkRelationshipSizes as List<number>; + const linkRelationship = Field.toString(link?.link_relationship as any as Field); //get string representing relationship + const linkRelationshipList = Doc.UserDoc().link_relationshipList as List<string>; + const linkColorList = Doc.UserDoc().link_ColorList as List<string>; + const linkRelationshipSizes = Doc.UserDoc().link_relationshipSizes as List<number>; const currRelationshipIndex = linkRelationshipList.indexOf(linkRelationship); - const linkDescription = Field.toString(link.description as any as Field); + const linkDescription = Field.toString(link.link_description as any as Field); const linkSize = currRelationshipIndex === -1 || currRelationshipIndex >= linkRelationshipSizes.length ? -1 : linkRelationshipSizes[currRelationshipIndex]; @@ -261,11 +262,11 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo //thickness varies linearly from 3px to 12px for increasing link count const strokeWidth = linkSize === -1 ? '3px' : Math.floor(2 + 10 * (linkSize / Math.max(...linkRelationshipSizes))) + 'px'; - if (link.linkDisplayArrow === undefined) { - link.linkDisplayArrow = false; + if (link.link_displayArrow === undefined) { + link.link_displayArrow = false; } - return link.opacity === 0 || !a.width || !b.width || (!link.linkDisplay && !aActive && !bActive) ? null : ( + return link.opacity === 0 || !a.width || !b.width || (!link.link_displayLine && !aActive && !bActive) ? null : ( <> <defs> <marker id={`${link[Id] + 'arrowhead'}`} markerWidth="4" markerHeight="3" refX="0" refY="1.5" orient="auto"> @@ -294,7 +295,7 @@ export class CollectionFreeFormLinkView extends React.Component<CollectionFreeFo style={{ pointerEvents: 'visibleStroke', opacity: this._opacity, stroke, strokeWidth }} onClick={this.onClickLine} d={`M ${pt1[0]} ${pt1[1]} C ${pt1[0] + pt1norm[0]} ${pt1[1] + pt1norm[1]}, ${pt2[0] + pt2norm[0]} ${pt2[1] + pt2norm[1]}, ${pt2[0]} ${pt2[1]}`} - markerEnd={link.linkDisplayArrow ? `url(#${link[Id] + 'arrowhead'})` : ''} + markerEnd={link.link_displayArrow ? `url(#${link[Id] + 'arrowhead'})` : ''} /> {textX === undefined || !linkDescription ? null : ( <text filter={`url(#${link[Id] + 'background'})`} className="collectionfreeformlinkview-linkText" x={textX} y={textY} onPointerDown={this.pointerDown}> diff --git a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx index 29bdc0e2d..11151e74e 100644 --- a/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx +++ b/src/client/views/collections/collectionFreeForm/CollectionFreeFormView.tsx @@ -4,7 +4,8 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction, import { observer } from 'mobx-react'; import { computedFn } from 'mobx-utils'; import { DateField } from '../../../../fields/DateField'; -import { DataSym, Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../../fields/Doc'; +import { Doc, DocListCast, Opt } from '../../../../fields/Doc'; +import { DocData, Height, Width } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { InkData, InkField, InkTool, PointData, Segment } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; @@ -53,7 +54,6 @@ import { MarqueeView } from './MarqueeView'; import React = require('react'); export type collectionFreeformViewProps = { - noPointerWheel?: () => boolean; // turn off pointerwheel interactions (see PDFViewer) NativeWidth?: () => number; NativeHeight?: () => number; originTopLeft?: boolean; @@ -101,13 +101,13 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return this.props.isAnnotationOverlay; } public get scaleFieldKey() { - return this.props.viewField ? this.props.viewField + '-viewScale' : '_viewScale'; + return (this.props.viewField ?? '') + '_freeform_scale'; } private get panXFieldKey() { - return this.props.viewField ? this.props.viewField + '-panX' : '_panX'; + return (this.props.viewField ?? '') + '_freeform_panX'; } private get panYFieldKey() { - return this.props.viewField ? this.props.viewField + '-panY' : '_panY'; + return (this.props.viewField ?? '') + '_freeform_panY'; } private get borderWidth() { return this.isAnnotationOverlay ? 0 : COLLECTION_BORDER_WIDTH; @@ -147,7 +147,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection }; } @computed get fitContentsToBox() { - return (this.props.fitContentsToBox?.() || this.Document._fitContentsToBox) && !this.isAnnotationOverlay; + return (this.props.fitContentsToBox?.() || this.Document._freeform_fitContentsToBox) && !this.isAnnotationOverlay; } @computed get contentBounds() { const cb = Cast(this.rootDoc.contentBounds, listSpec('number')); @@ -174,7 +174,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const dv = this.props.DocumentView?.(); const scaling = this.fitContentsToBox || !this.nativeDimScaling ? 1 : this.nativeDimScaling; // if freeform has a native aspect, then the panel height needs to be adjusted to match it - const aspect = dv?.nativeWidth && dv?.nativeHeight && !dv.layoutDoc.fitWidth ? dv.nativeHeight / dv.nativeWidth : this.props.PanelHeight() / this.props.PanelWidth(); + const aspect = dv?.nativeWidth && dv?.nativeHeight && !dv.layoutDoc.layout_fitWidth ? dv.nativeHeight / dv.nativeWidth : this.props.PanelHeight() / this.props.PanelWidth(); return this.props.isAnnotationOverlay || this.props.originTopLeft ? 0 : (aspect * this.props.PanelWidth()) / 2 / scaling; // shift so pan position is at center of window for non-overlay collections } @computed get cachedGetLocalTransform(): Transform { @@ -240,16 +240,16 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection shrinkWrap = () => { if (this.props.DocumentView?.().nativeWidth) return; const vals = this.fitToContentVals; - this.layoutDoc._panX = vals.bounds.cx; - this.layoutDoc._panY = vals.bounds.cy; - this.layoutDoc._viewScale = vals.scale; + this.layoutDoc._freeform_panX = vals.bounds.cx; + this.layoutDoc._freeform_panY = vals.bounds.cy; + this.layoutDoc._freeform_scale = vals.scale; }; freeformData = (force?: boolean) => (!this._firstRender && (this.fitContentsToBox || force) ? this.fitToContentVals : undefined); reverseNativeScaling = () => (this.fitContentsToBox ? true : false); - // panx, pany, zoomscale all attempt to get values first from the layout controller, then from the layout/dataDoc (or template layout doc), and finally from the resolved template data document. + // freeform_panx, freeform_pany, freeform_scale all attempt to get values first from the layout controller, then from the layout/dataDoc (or template layout doc), and finally from the resolved template data document. // this search order, for example, allows icons of cropped images to find the panx/pany/zoom on the cropped image's data doc instead of the usual layout doc because the zoom/panX/panY define the cropped image - panX = () => this.freeformData()?.bounds.cx ?? NumCast(this.Document[this.panXFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.panX, 1)); - panY = () => this.freeformData()?.bounds.cy ?? NumCast(this.Document[this.panYFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.panY, 1)); + panX = () => this.freeformData()?.bounds.cx ?? NumCast(this.Document[this.panXFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.freeform_panX, 1)); + panY = () => this.freeformData()?.bounds.cy ?? NumCast(this.Document[this.panYFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.freeform_panY, 1)); zoomScaling = () => this.freeformData()?.scale ?? NumCast(Doc.Layout(this.Document)[this.scaleFieldKey], NumCast(Cast(this.Document.resolvedDataDoc, Doc, null)?.[this.scaleFieldKey], 1)); contentTransform = () => this.props.isAnnotationOverlay && this.zoomScaling() === 1 ? `` : `translate(${this.cachedCenteringShiftX}px, ${this.cachedCenteringShiftY}px) scale(${this.zoomScaling()}) translate(${-this.panX()}px, ${-this.panY()}px)`; @@ -298,7 +298,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection isCurrent(doc: Doc) { const dispTime = NumCast(doc._timecodeToShow, -1); const endTime = NumCast(doc._timecodeToHide, dispTime + 1.5); - const curTime = NumCast(this.Document._currentTimecode, -1); + const curTime = NumCast(this.Document._layout_currentTimecode, -1); return dispTime === -1 || curTime === -1 || (curTime - dispTime >= -1e-4 && curTime <= endTime); } @@ -349,7 +349,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection .map(pair => pair.layout) .slice() .sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex)); - zsorted.forEach((doc, index) => (doc.zIndex = doc.isInkMask ? 5000 : index + 1)); + zsorted.forEach((doc, index) => (doc.zIndex = doc.stroke_isInkMask ? 5000 : index + 1)); const dvals = CollectionFreeFormDocumentView.getValues(refDoc, NumCast(refDoc.activeFrame, 1000)); const dropPos = this.Document._currentFrame !== undefined ? [NumCast(dvals.x), NumCast(dvals.y)] : [NumCast(refDoc.x), NumCast(refDoc.y)]; @@ -367,7 +367,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection d.x = x + NumCast(d.x) - dropPos[0]; d.y = y + NumCast(d.y) - dropPos[1]; } - d._lastModified = new DateField(); + d._layout_modificationDate = new DateField(); const nd = [Doc.NativeWidth(layoutDoc), Doc.NativeHeight(layoutDoc)]; layoutDoc._width = NumCast(layoutDoc._width, 300); layoutDoc._height = NumCast(layoutDoc._height, nd[0] && nd[1] ? (nd[1] / nd[0]) * NumCast(layoutDoc._width) : 300); @@ -419,11 +419,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection internalLinkDrop(e: Event, de: DragManager.DropEvent, linkDragData: DragManager.LinkDragData, xp: number, yp: number) { if (linkDragData.linkDragView.props.docViewPath().includes(this.props.docViewPath().lastElement())) { // dragged document is a child of this collection - if (!linkDragData.linkDragView.props.CollectionFreeFormDocumentView?.() || linkDragData.dragDocument.context !== this.props.Document) { - // if the source doc view's context isn't this same freeformcollectionlinkDragData.dragDocument.context === this.props.Document + if (!linkDragData.linkDragView.props.CollectionFreeFormDocumentView?.() || linkDragData.dragDocument.embedContainer !== this.props.Document) { + // if the source doc view's embedContainer isn't this same freeformcollectionlinkDragData.dragDocument.embedContainer === this.props.Document const source = Docs.Create.TextDocument('', { _width: 200, _height: 75, x: xp, y: yp, title: 'dropped annotation' }); this.props.addDocument?.(source); - de.complete.linkDocument = DocUtils.MakeLink(linkDragData.linkSourceGetAnchor(), source, { linkRelationship: 'annotated by:annotation of' }); // TODODO this is where in text links get passed + de.complete.linkDocument = DocUtils.MakeLink(linkDragData.linkSourceGetAnchor(), source, { link_relationship: 'annotated by:annotation of' }); // TODODO this is where in text links get passed } e.stopPropagation(); // do nothing if link is dropped into any freeform view parent of dragged document return true; @@ -447,7 +447,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return this.childLayoutPairs .map(pair => pair.layout) .reduce((cluster, cd) => { - const grouping = this.props.Document._useClusters ? NumCast(cd.cluster, -1) : NumCast(cd.group, -1); + const grouping = this.props.Document._freeform_useClusters ? NumCast(cd.layout_cluster, -1) : NumCast(cd.group, -1); if (grouping !== -1) { const layoutDoc = Doc.Layout(cd); const cx = NumCast(cd.x) - this._clusterDistance / 2; @@ -464,10 +464,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection if (cluster !== -1) { const ptsParent = e instanceof PointerEvent ? e : e.targetTouches.item(0); if (ptsParent) { - const eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => (this.props.Document._useClusters ? NumCast(cd.cluster) : NumCast(cd.group, -1)) === cluster); + const eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => (this.props.Document._freeform_useClusters ? NumCast(cd.layout_cluster) : NumCast(cd.group, -1)) === cluster); const clusterDocs = eles.map(ele => DocumentManager.Instance.getDocumentView(ele, this.props.DocumentView?.())!); const { left, top } = clusterDocs[0].getBounds() || { left: 0, top: 0 }; - const de = new DragManager.DocumentDragData(eles, e.ctrlKey || e.altKey ? 'alias' : undefined); + const de = new DragManager.DocumentDragData(eles, e.ctrlKey || e.altKey ? 'embed' : undefined); de.moveDocument = this.props.moveDocument; de.offset = this.getTransform().transformDirection(ptsParent.clientX - left, ptsParent.clientY - top); DragManager.StartDocumentDrag( @@ -484,10 +484,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return false; } - @undoBatch @action - updateClusters(_useClusters: boolean) { - this.props.Document._useClusters = _useClusters; + updateClusters(_freeform_useClusters: boolean) { + this.props.Document._freeform_useClusters = _freeform_useClusters; this._clusterSets.length = 0; this.childLayoutPairs.map(pair => pair.layout).map(c => this.updateCluster(c)); } @@ -495,70 +494,74 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @action updateClusterDocs(docs: Doc[]) { const childLayouts = this.childLayoutPairs.map(pair => pair.layout); - if (this.props.Document._useClusters) { + if (this.props.Document._freeform_useClusters) { const docFirst = docs[0]; docs.map(doc => this._clusterSets.map(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1))); - const preferredInd = NumCast(docFirst.cluster); - docs.map(doc => (doc.cluster = -1)); + const preferredInd = NumCast(docFirst.layout_cluster); + docs.map(doc => (doc.layout_cluster = -1)); docs.map(doc => this._clusterSets.map((set, i) => set.map(member => { - if (docFirst.cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && Doc.overlapping(doc, member, this._clusterDistance)) { - docFirst.cluster = i; + if (docFirst.layout_cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && Doc.overlapping(doc, member, this._clusterDistance)) { + docFirst.layout_cluster = i; } }) ) ); - if (docFirst.cluster === -1 && preferredInd !== -1 && this._clusterSets.length > preferredInd && (!this._clusterSets[preferredInd] || !this._clusterSets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)) { - docFirst.cluster = preferredInd; + if ( + docFirst.layout_cluster === -1 && + preferredInd !== -1 && + this._clusterSets.length > preferredInd && + (!this._clusterSets[preferredInd] || !this._clusterSets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length) + ) { + docFirst.layout_cluster = preferredInd; } this._clusterSets.map((set, i) => { - if (docFirst.cluster === -1 && !set.filter(member => Doc.IndexOf(member, childLayouts) !== -1).length) { - docFirst.cluster = i; + if (docFirst.layout_cluster === -1 && !set.filter(member => Doc.IndexOf(member, childLayouts) !== -1).length) { + docFirst.layout_cluster = i; } }); - if (docFirst.cluster === -1) { + if (docFirst.layout_cluster === -1) { docs.map(doc => { - doc.cluster = this._clusterSets.length; + doc.layout_cluster = this._clusterSets.length; this._clusterSets.push([doc]); }); } else if (this._clusterSets.length) { - for (let i = this._clusterSets.length; i <= NumCast(docFirst.cluster); i++) !this._clusterSets[i] && this._clusterSets.push([]); - docs.map(doc => this._clusterSets[(doc.cluster = NumCast(docFirst.cluster))].push(doc)); + for (let i = this._clusterSets.length; i <= NumCast(docFirst.layout_cluster); i++) !this._clusterSets[i] && this._clusterSets.push([]); + docs.map(doc => this._clusterSets[(doc.layout_cluster = NumCast(docFirst.layout_cluster))].push(doc)); } - childLayouts.map(child => !this._clusterSets.some((set, i) => Doc.IndexOf(child, set) !== -1 && child.cluster === i) && this.updateCluster(child)); + childLayouts.map(child => !this._clusterSets.some((set, i) => Doc.IndexOf(child, set) !== -1 && child.layout_cluster === i) && this.updateCluster(child)); } } - @undoBatch @action updateCluster(doc: Doc) { const childLayouts = this.childLayoutPairs.map(pair => pair.layout); - if (this.props.Document._useClusters) { + if (this.props.Document._freeform_useClusters) { this._clusterSets.forEach(set => Doc.IndexOf(doc, set) !== -1 && set.splice(Doc.IndexOf(doc, set), 1)); - const preferredInd = NumCast(doc.cluster); - doc.cluster = -1; + const preferredInd = NumCast(doc.layout_cluster); + doc.layout_cluster = -1; this._clusterSets.forEach((set, i) => set.forEach(member => { - if (doc.cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && Doc.overlapping(doc, member, this._clusterDistance)) { - doc.cluster = i; + if (doc.layout_cluster === -1 && Doc.IndexOf(member, childLayouts) !== -1 && Doc.overlapping(doc, member, this._clusterDistance)) { + doc.layout_cluster = i; } }) ); - if (doc.cluster === -1 && preferredInd !== -1 && this._clusterSets.length > preferredInd && (!this._clusterSets[preferredInd] || !this._clusterSets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)) { - doc.cluster = preferredInd; + if (doc.layout_cluster === -1 && preferredInd !== -1 && this._clusterSets.length > preferredInd && (!this._clusterSets[preferredInd] || !this._clusterSets[preferredInd].filter(member => Doc.IndexOf(member, childLayouts) !== -1).length)) { + doc.layout_cluster = preferredInd; } this._clusterSets.forEach((set, i) => { - if (doc.cluster === -1 && !set.filter(member => Doc.IndexOf(member, childLayouts) !== -1).length) { - doc.cluster = i; + if (doc.layout_cluster === -1 && !set.filter(member => Doc.IndexOf(member, childLayouts) !== -1).length) { + doc.layout_cluster = i; } }); - if (doc.cluster === -1) { - doc.cluster = this._clusterSets.length; + if (doc.layout_cluster === -1) { + doc.layout_cluster = this._clusterSets.length; this._clusterSets.push([doc]); } else if (this._clusterSets.length) { - for (let i = this._clusterSets.length; i <= doc.cluster; i++) !this._clusterSets[i] && this._clusterSets.push([]); - this._clusterSets[doc.cluster ?? 0].push(doc); + for (let i = this._clusterSets.length; i <= doc.layout_cluster; i++) !this._clusterSets[i] && this._clusterSets.push([]); + this._clusterSets[doc.layout_cluster ?? 0].push(doc); } } } @@ -567,8 +570,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection let styleProp = this.props.styleProvider?.(doc, props, property); // bcz: check 'props' used to be renderDepth + 1 switch (property) { case StyleProp.BackgroundColor: - const cluster = NumCast(doc?.cluster); - if (this.Document._useClusters && doc?.type !== DocumentType.IMG) { + const cluster = NumCast(doc?.layout_cluster); + if (this.Document._freeform_useClusters && doc?.type !== DocumentType.IMG) { if (this._clusterSets.length <= cluster) { setTimeout(() => doc && this.updateCluster(doc)); } else { @@ -592,7 +595,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection trySelectCluster = (addToSel: boolean) => { if (this._hitCluster !== -1) { !addToSel && SelectionManager.DeselectAll(); - const eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => (this.props.Document._useClusters ? NumCast(cd.cluster) : NumCast(cd.group, -1)) === this._hitCluster); + const eles = this.childLayoutPairs.map(pair => pair.layout).filter(cd => (this.props.Document._freeform_useClusters ? NumCast(cd.layout_cluster) : NumCast(cd.group, -1)) === this._hitCluster); this.selectDocuments(eles); return true; } @@ -633,7 +636,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection case InkTool.None: if (!(this.props.layoutEngine?.() || StrCast(this.layoutDoc._layoutEngine))) { this._hitCluster = this.pickCluster(this.getTransform().transformPoint(e.clientX, e.clientY)); - setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, this._hitCluster !== -1 ? true : false); + setupMoveUpEvents(this, e, this.onPointerMove, emptyFunction, emptyFunction, this._hitCluster !== -1 ? true : false, false); } break; } @@ -679,7 +682,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const B = this.getTransform().transformBounds(ge.bounds.left, ge.bounds.top, ge.bounds.width, ge.bounds.height); const inkDoc = Docs.Create.InkDocument( ActiveInkColor(), - Doc.ActiveTool, ActiveInkWidth() * this.props.ScreenToLocalTransform().Scale, ActiveInkBezierApprox(), ActiveFillColor(), @@ -706,7 +708,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection case GestureUtils.Gestures.Rectangle: if (this._inkToTextStartX && this._inkToTextStartY) { const end = this.getTransform().transformPoint(Math.max(...ge.points.map(p => p.X)), Math.max(...ge.points.map(p => p.Y))); - const setDocs = this.getActiveDocuments().filter(s => s.proto?.type === 'rtf' && s.color); + const setDocs = this.getActiveDocuments().filter(s => DocCast(s.proto)?.type === DocumentType.RTF && s.color); const sets = setDocs.map(sd => { return Cast(sd.text, RichTextField)?.Text as string; }); @@ -718,9 +720,9 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const inks = this.getActiveDocuments().filter(doc => { if (doc.type === 'ink') { const l = NumCast(doc.x); - const r = l + doc[WidthSym](); + const r = l + doc[Width](); const t = NumCast(doc.y); - const b = t + doc[HeightSym](); + const b = t + doc[Height](); const pass = !(this._inkToTextStartX! > r || end[0] < l || this._inkToTextStartY! > b || end[1] < t); return pass; } @@ -835,7 +837,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this.getEraserIntersections({ X: currPoint.X - delta[0], Y: currPoint.Y - delta[1] }, currPoint).forEach(intersect => { if (!this._deleteList.includes(intersect.inkView)) { this._deleteList.push(intersect.inkView); - SetActiveInkWidth(StrCast(intersect.inkView.rootDoc.strokeWidth?.toString()) || '1'); + SetActiveInkWidth(StrCast(intersect.inkView.rootDoc.stroke_width?.toString()) || '1'); SetActiveInkColor(StrCast(intersect.inkView.rootDoc.color?.toString()) || 'black'); // create a new curve by appending all curves of the current segment together in order to render a single new stroke. if (!e.shiftKey) { @@ -867,9 +869,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection Doc.ActiveTool = InkTool.None; } else { if (this.tryDragCluster(e, this._hitCluster)) { + e.stopPropagation(); // we're moving a cluster, so stop propagation and return true to end panning and let the document drag take over return true; } - this.pan(e); + // pan the view if this is a regular collection, or it's an overlay and the overlay is zoomed (otherwise, there's nothing to pan) + if (!this.props.isAnnotationOverlay || 1 - NumCast(this.rootDoc._freeform_scale_min, 1) / this.getLocalTransform().inverse().Scale) { + this.pan(e); + e.stopPropagation(); // if we are actually panning, stop propagation -- this will preven things like the overlayView from dragging the document while we're panning + } } return false; }; @@ -1014,7 +1021,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @action zoom = (pointX: number, pointY: number, deltaY: number): void => { - if (this.Document._isGroup) return; + if (this.Document._isGroup || this.Document._freeform_noZoom) return; let deltaScale = deltaY > 0 ? 1 / 1.05 : 1.05; if (deltaScale < 0) deltaScale = -deltaScale; const [x, y] = this.getTransform().transformPoint(pointX, pointY); @@ -1022,50 +1029,42 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection if (deltaScale * invTransform.Scale > 20) { deltaScale = 20 / invTransform.Scale; } - if (deltaScale < 1 && invTransform.Scale <= NumCast(this.rootDoc._viewScaleMin, 1) && this.isAnnotationOverlay) { + if (deltaScale < 1 && invTransform.Scale <= NumCast(this.rootDoc._freeform_scale_min, 1) && this.isAnnotationOverlay) { + this.setPan(0, 0); return; } - if (deltaScale * invTransform.Scale < NumCast(this.rootDoc._viewScaleMin, 1) && this.isAnnotationOverlay) { - deltaScale = NumCast(this.rootDoc._viewScaleMin, 1) / invTransform.Scale; + if (deltaScale * invTransform.Scale < NumCast(this.rootDoc._freeform_scale_min, 1) && this.isAnnotationOverlay) { + deltaScale = NumCast(this.rootDoc._freeform_scale_min, 1) / invTransform.Scale; } - const localTransform = this.getLocalTransform().inverse().scaleAbout(deltaScale, x, y); + const localTransform = invTransform.scaleAbout(deltaScale, x, y); if (localTransform.Scale >= 0.05 || localTransform.Scale > this.zoomScaling()) { const safeScale = Math.min(Math.max(0.05, localTransform.Scale), 20); this.props.Document[this.scaleFieldKey] = Math.abs(safeScale); - this.setPan(-localTransform.TranslateX / safeScale, (this.props.originTopLeft ? undefined : NumCast(this.props.Document.scrollTop) * safeScale) || -localTransform.TranslateY / safeScale); + this.setPan(-localTransform.TranslateX / safeScale, (this.props.originTopLeft ? undefined : NumCast(this.props.Document.layout_scrollTop) * safeScale) || -localTransform.TranslateY / safeScale); } }; @action onPointerWheel = (e: React.WheelEvent): void => { - if (this.props.noPointerWheel?.() || this.Document._isGroup || !this.isContentActive()) return; // group style collections neither pan nor zoom + if (this.Document._isGroup || !this.isContentActive()) return; // group style collections neither pan nor zoom PresBox.Instance?.pauseAutoPres(); - if (this.layoutDoc._Transform || DocListCast(Doc.MyOverlayDocs?.data).includes(this.props.Document) || this.props.Document.treeViewOutlineMode === TreeViewType.outline) return; + if (this.layoutDoc._Transform || this.props.Document.treeViewOutlineMode === TreeViewType.outline) return; e.stopPropagation(); - e.preventDefault(); + const docHeight = NumCast(this.rootDoc[Doc.LayoutFieldKey(this.rootDoc) + '_nativeHeight'], this.nativeHeight); + const scrollable = NumCast(this.layoutDoc[this.scaleFieldKey], 1) === 1 && docHeight > this.props.PanelHeight() / this.nativeDimScaling; switch (!e.ctrlKey ? Doc.UserDoc().freeformScrollMode : freeformScrollMode.Pan) { case freeformScrollMode.Pan: // if ctrl is selected then zoom - if (e.ctrlKey) { - if (this.props.isContentActive(true)) { - this.zoom(e.clientX, e.clientY, e.deltaY); // if (!this.props.isAnnotationOverlay) // bcz: do we want to zoom in on images/videos/etc? - } - } // otherwise pan - else if (this.props.isContentActive(true)) { - const dx = -e.deltaX; - const dy = -e.deltaY; - if (e.shiftKey) { - !this.props.isAnnotationOverlayScrollable && this.scrollPan({ deltaX: dy, deltaY: 0 }); - } else { - !this.props.isAnnotationOverlayScrollable && this.scrollPan({ deltaX: dx, deltaY: dy }); - } + if (!e.ctrlKey && this.props.isContentActive(true)) { + this.scrollPan({ deltaX: -e.deltaX, deltaY: e.shiftKey ? 0 : -Math.max(-1, Math.min(1, e.deltaY)) }); + break; } - break; default: case freeformScrollMode.Zoom: - if (this.props.isContentActive(true)) { - !this.props.isAnnotationOverlayScrollable && this.zoom(e.clientX, e.clientY, e.deltaY); // if (!this.props.isAnnotationOverlay) // bcz: do we want to zoom in on images/videos/etc? + if ((e.ctrlKey || !scrollable) && this.props.isContentActive(true)) { + this.zoom(e.clientX, e.clientY, Math.max(-1, Math.min(1, e.deltaY))); // if (!this.props.isAnnotationOverlay) // bcz: do we want to zoom in on images/videos/etc? + e.preventDefault(); } break; } @@ -1108,31 +1107,30 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection else if (ranges.yrange.max <= panY - panelHgtMin / 2) panY = ranges.yrange.min - (this.props.originTopLeft ? panelHgtMax / 2 : panelHgtMin / 2); } } - if (!this.layoutDoc._lockedTransform || LightboxView.LightboxDoc || DocListCast(Doc.MyOverlayDocs?.data).includes(this.Document)) { + if (!this.layoutDoc._lockedTransform || LightboxView.LightboxDoc) { this.setPanZoomTransition(panTime); - const scale = this.getLocalTransform().inverse().Scale; - const minScale = NumCast(this.rootDoc._viewScaleMin, 1); - const minPanX = NumCast(this.rootDoc._panXMin, 0); - const minPanY = NumCast(this.rootDoc._panYMin, 0); - const newPanX = Math.min(minPanX + (1 - minScale / scale) * NumCast(this.rootDoc._panXMax, this.nativeWidth), Math.max(minPanX, panX)); + const minScale = NumCast(this.rootDoc._freeform_scale_min, 1); + const scale = 1 - minScale / this.getLocalTransform().inverse().Scale; + const minPanX = NumCast(this.rootDoc._freeform_panX_min, 0); + const minPanY = NumCast(this.rootDoc._freeform_panY_min, 0); + const maxPanX = NumCast(this.rootDoc._freeform_panX_max, this.nativeWidth); + const newPanX = Math.min(minPanX + scale * maxPanX, Math.max(minPanX, panX)); const fitYscroll = (((this.nativeHeight / this.nativeWidth) * this.props.PanelWidth() - this.props.PanelHeight()) * this.props.ScreenToLocalTransform().Scale) / minScale; const nativeHeight = (this.props.PanelHeight() / this.props.PanelWidth() / (this.nativeHeight / this.nativeWidth)) * this.nativeHeight; const maxScrollTop = this.nativeHeight / this.props.ScreenToLocalTransform().Scale - this.props.PanelHeight(); const maxPanY = - minPanY + // minPanY + scrolling introduced by view scaling + scrolling introduced by fitWidth - (1 - minScale / scale) * NumCast(this.rootDoc._panYMax, nativeHeight) + + minPanY + // minPanY + scrolling introduced by view scaling + scrolling introduced by layout_fitWidth + scale * NumCast(this.rootDoc._panY_max, nativeHeight) + (!this.props.getScrollHeight?.() ? fitYscroll : 0); // when not zoomed, scrolling is handled via a scrollbar, not panning let newPanY = Math.max(minPanY, Math.min(maxPanY, panY)); - if (NumCast(this.rootDoc.scrollTop) && NumCast(this.rootDoc._viewScale, minScale) !== minScale) { - const relTop = NumCast(this.rootDoc.scrollTop) / maxScrollTop; - this.rootDoc.scrollTop = undefined; + if (false && NumCast(this.rootDoc.layout_scrollTop) && NumCast(this.rootDoc._freeform_scale, minScale) !== minScale) { + const relTop = NumCast(this.rootDoc.layout_scrollTop) / maxScrollTop; + this.rootDoc.layout_scrollTop = undefined; newPanY = minPanY + relTop * (maxPanY - minPanY); - } else if (fitYscroll && this.rootDoc.scrollTop === undefined && NumCast(this.rootDoc._viewScale, minScale) === minScale) { + } else if (fitYscroll > 2 && this.rootDoc.layout_scrollTop === undefined && NumCast(this.rootDoc._freeform_scale, minScale) === minScale) { const maxPanY = minPanY + fitYscroll; const relTop = (panY - minPanY) / (maxPanY - minPanY); - setTimeout(() => { - this.rootDoc.scrollTop = relTop * maxScrollTop; - }, 10); + setTimeout(() => (this.rootDoc.layout_scrollTop = relTop * maxScrollTop), 10); newPanY = minPanY; } !this.Document._verticalScroll && (this.Document[this.panXFieldKey] = this.isAnnotationOverlay ? newPanX : panX); @@ -1143,7 +1141,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @action nudge = (x: number, y: number, nudgeTime: number = 500) => { const collectionDoc = this.props.docViewPath().lastElement().rootDoc; - if (collectionDoc?._viewType !== CollectionViewType.Freeform || collectionDoc._panX !== undefined) { + if (collectionDoc?._type_collection !== CollectionViewType.Freeform || collectionDoc._freeform_ !== undefined) { this.setPan( NumCast(this.layoutDoc[this.panXFieldKey]) + ((this.props.PanelWidth() / 2) * x) / this.zoomScaling(), // nudge x,y as a function of panel dimension and scale NumCast(this.layoutDoc[this.panYFieldKey]) + ((this.props.PanelHeight() / 2) * -y) / this.zoomScaling(), @@ -1162,7 +1160,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection docs.sort((doc1, doc2) => NumCast(doc1.zIndex) - NumCast(doc2.zIndex)); let zfirst = docs.length ? NumCast(docs[0].zIndex) : 0; doc.zIndex = zfirst - 1; - } else if (doc.isInkMask) { + } else if (doc.stroke_isInkMask) { doc.zIndex = 5000; } else { const docs = this.childLayoutPairs.map(pair => pair.layout).slice(); @@ -1204,7 +1202,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection calculatePanIntoView = (doc: Doc, xf: Transform, scale?: number) => { const layoutdoc = Doc.Layout(doc); const pt = xf.transformPoint(NumCast(doc.x), NumCast(doc.y)); - const pt2 = xf.transformPoint(NumCast(doc.x) + layoutdoc[WidthSym](), NumCast(doc.y) + layoutdoc[HeightSym]()); + const pt2 = xf.transformPoint(NumCast(doc.x) + layoutdoc[Width](), NumCast(doc.y) + layoutdoc[Height]()); const bounds = { left: pt[0], right: pt2[0], top: pt[1], bot: pt2[1], width: pt2[0] - pt[0], height: pt2[1] - pt[1] }; if (scale !== undefined) { @@ -1222,8 +1220,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const panelWidth = this.props.isAnnotationOverlay ? this.nativeWidth : this.props.PanelWidth(); const panelHeight = this.props.isAnnotationOverlay ? this.nativeHeight : this.props.PanelHeight(); - const pw = panelWidth / NumCast(this.layoutDoc._viewScale, 1); - const ph = panelHeight / NumCast(this.layoutDoc._viewScale, 1); + const pw = panelWidth / NumCast(this.layoutDoc._freeform_scale, 1); + const ph = panelHeight / NumCast(this.layoutDoc._freeform_scale, 1); const cx = NumCast(this.layoutDoc[this.panXFieldKey]) + (this.props.isAnnotationOverlay ? pw / 2 : 0); const cy = NumCast(this.layoutDoc[this.panYFieldKey]) + (this.props.isAnnotationOverlay ? ph / 2 : 0); const screen = { left: cx - pw / 2, right: cx + pw / 2, top: cy - ph / 2, bot: cy + ph / 2 }; @@ -1248,17 +1246,17 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection @action onKeyDown = (e: React.KeyboardEvent, fieldProps: FieldViewProps) => { const docView = fieldProps.DocumentView?.(); - if (docView && (e.metaKey || e.ctrlKey || e.altKey || docView.rootDoc._singleLine) && ['Tab', 'Enter'].includes(e.key)) { + if (docView && (e.metaKey || e.ctrlKey || e.altKey || docView.rootDoc._createDocOnCR) && ['Tab', 'Enter'].includes(e.key)) { e.stopPropagation?.(); const below = !e.altKey && e.key !== 'Tab'; - const layoutKey = StrCast(docView.LayoutFieldKey); + const layout_fieldKey = StrCast(docView.LayoutFieldKey); const newDoc = Doc.MakeCopy(docView.rootDoc, true); const dataField = docView.rootDoc[Doc.LayoutFieldKey(newDoc)]; - newDoc[DataSym][Doc.LayoutFieldKey(newDoc)] = dataField === undefined || Cast(dataField, listSpec(Doc), null)?.length !== undefined ? new List<Doc>([]) : undefined; + newDoc[DocData][Doc.LayoutFieldKey(newDoc)] = dataField === undefined || Cast(dataField, listSpec(Doc), null)?.length !== undefined ? new List<Doc>([]) : undefined; if (below) newDoc.y = NumCast(docView.rootDoc.y) + NumCast(docView.rootDoc._height) + 10; else newDoc.x = NumCast(docView.rootDoc.x) + NumCast(docView.rootDoc._width) + 10; - if (layoutKey !== 'layout' && docView.rootDoc[layoutKey] instanceof Doc) { - newDoc[layoutKey] = docView.rootDoc[layoutKey]; + if (layout_fieldKey !== 'layout' && docView.rootDoc[layout_fieldKey] instanceof Doc) { + newDoc[layout_fieldKey] = docView.rootDoc[layout_fieldKey]; } Doc.GetProto(newDoc).text = undefined; FormattedTextBox.SelectOnLoad = newDoc[Id]; @@ -1294,10 +1292,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection onDoubleClick={this.onChildDoubleClickHandler} onBrowseClick={this.onBrowseClickHandler} ScreenToLocalTransform={childLayout.z ? this.getContainerTransform : this.getTransform} - PanelWidth={childLayout[WidthSym]} - PanelHeight={childLayout[HeightSym]} - docFilters={this.childDocFilters} - docRangeFilters={this.childDocRangeFilters} + PanelWidth={childLayout[Width]} + PanelHeight={childLayout[Height]} + childFilters={this.childDocFilters} + childFiltersByRanges={this.childDocRangeFilters} searchFilterDocs={this.searchFilterDocs} isDocumentActive={this.props.childDocumentsActive?.() ? this.props.isDocumentActive : this.isContentActive} isContentActive={this.props.childContentsActive ?? emptyFunction} @@ -1315,11 +1313,11 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection sizeProvider={this.childSizeProvider} dropAction={StrCast(this.props.Document.childDropAction) as dropActionType} bringToFront={this.bringToFront} - showTitle={this.props.childShowTitle} + layout_showTitle={this.props.childlayout_showTitle} dontRegisterView={this.props.dontRenderDocuments || this.props.dontRegisterView} pointerEvents={this.pointerEvents} //rotation={this.props.styleProvider?.(childLayout, this.props, StyleProp.JitterRotation) || 0} - //fitContentsToBox={this.props.fitContentsToBox || BoolCast(this.props.freezeChildDimensions)} // bcz: check this + //fitContentsToBox={this.props.fitContentsToBox || BoolCast(this.props.treeViewFreezeChildDimensions)} // bcz: check this /> ); } @@ -1329,7 +1327,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection case OpenWhere.inParent: return this.props.addDocument?.(doc) || false; case OpenWhere.inParentFromScreen: - const docContext = DocCast((doc instanceof Doc ? doc : doc?.[0])?.context); + const docContext = DocCast((doc instanceof Doc ? doc : doc?.[0])?.embedContainer); return ( (this.addDocument?.( (doc instanceof Doc ? [doc] : doc).map(doc => { @@ -1521,7 +1519,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection elements.push({ ele: this.getChildDocView(entry[1]), bounds: childData.opacity === 0 ? { ...childData, width: 0, height: 0 } : { ...childData, width: childSize.width, height: childSize.height }, - inkMask: BoolCast(entry[1].pair.layout.isInkMask) ? NumCast(entry[1].pair.layout.opacity, 1) : -1, + inkMask: BoolCast(entry[1].pair.layout.stroke_isInkMask) ? NumCast(entry[1].pair.layout.opacity, 1) : -1, }); }); @@ -1531,20 +1529,20 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection else this.props.Document[this.scaleFieldKey] = Math.max(1, this.zoomScaling()); // NumCast(this.props.Document[this.scaleFieldKey])); } - this.Document._useClusters && !this._clusterSets.length && this.childDocs.length && this.updateClusters(true); + this.Document._freeform_useClusters && !this._clusterSets.length && this.childDocs.length && this.updateClusters(true); return elements; } getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { // create an anchor that saves information about the current state of the freeform view (pan, zoom, view type) - const anchor = Docs.Create.CollectionAnchorDocument({ title: 'ViewSpec - ' + StrCast(this.layoutDoc._viewType), unrendered: true, presTransition: 500, annotationOn: this.rootDoc }); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), pannable: !this.Document._isGroup, viewType: true, filters: true } }, this.rootDoc); + const anchor = Docs.Create.CollectionConfigDocument({ title: 'ViewSpec - ' + StrCast(this.layoutDoc._type_collection), layout_unrendered: true, presTransition: 500, annotationOn: this.rootDoc }); + PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), pannable: !this.Document._isGroup, type_collection: true, filters: true } }, this.rootDoc); if (addAsAnnotation) { - if (Cast(this.dataDoc[this.props.fieldKey + '-annotations'], listSpec(Doc), null) !== undefined) { - Cast(this.dataDoc[this.props.fieldKey + '-annotations'], listSpec(Doc), []).push(anchor); + if (Cast(this.dataDoc[this.props.fieldKey + '_annotations'], listSpec(Doc), null) !== undefined) { + Cast(this.dataDoc[this.props.fieldKey + '_annotations'], listSpec(Doc), []).push(anchor); } else { - this.dataDoc[this.props.fieldKey + '-annotations'] = new List<Doc>([anchor]); + this.dataDoc[this.props.fieldKey + '_annotations'] = new List<Doc>([anchor]); } } return anchor; @@ -1561,14 +1559,14 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection this._disposers.groupBounds = reaction( () => { if (this.Document._isGroup && this.childDocs.length === this.childDocList?.length) { - const clist = this.childDocs.map(cd => ({ x: NumCast(cd.x), y: NumCast(cd.y), width: cd[WidthSym](), height: cd[HeightSym]() })); + const clist = this.childDocs.map(cd => ({ x: NumCast(cd.x), y: NumCast(cd.y), width: cd[Width](), height: cd[Height]() })); return aggregateBounds(clist, NumCast(this.layoutDoc._xPadding), NumCast(this.layoutDoc._yPadding)); } return undefined; }, cbounds => { if (cbounds) { - const c = [NumCast(this.layoutDoc.x) + this.layoutDoc[WidthSym]() / 2, NumCast(this.layoutDoc.y) + this.layoutDoc[HeightSym]() / 2]; + const c = [NumCast(this.layoutDoc.x) + this.layoutDoc[Width]() / 2, NumCast(this.layoutDoc.y) + this.layoutDoc[Height]() / 2]; const p = [NumCast(this.layoutDoc[this.panXFieldKey]), NumCast(this.layoutDoc[this.panYFieldKey])]; const pbounds = { x: cbounds.x - p[0] + c[0], @@ -1634,8 +1632,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection CollectionFreeFormView.UpdateIcon( this.layoutDoc[Id] + '-icon' + new Date().getTime(), this.props.docViewPath().lastElement().ContentDiv!, - this.layoutDoc[WidthSym](), - this.layoutDoc[HeightSym](), + this.layoutDoc[Width](), + this.layoutDoc[Height](), this.props.PanelWidth(), this.props.PanelHeight(), 0, @@ -1644,8 +1642,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection '', (iconFile, nativeWidth, nativeHeight) => { this.dataDoc.icon = new ImageField(iconFile); - this.dataDoc['icon-nativeWidth'] = nativeWidth; - this.dataDoc['icon-nativeHeight'] = nativeHeight; + this.dataDoc['icon_nativeWidth'] = nativeWidth; + this.dataDoc['icon_nativeHeight'] = nativeHeight; } ); @@ -1694,7 +1692,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection if ((e as any).handlePan || this.props.isAnnotationOverlay) return; (e as any).handlePan = true; - if (!this.layoutDoc._noAutoscroll && !this.props.renderDepth && this._marqueeRef) { + if (!this.layoutDoc._freeform_noAutoPan && !this.props.renderDepth && this._marqueeRef) { const dragX = e.detail.clientX; const dragY = e.detail.clientY; const bounds = this._marqueeRef?.getBoundingClientRect(); @@ -1759,12 +1757,6 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection return; } !Doc.noviceMode && Doc.UserDoc().defaultTextLayout && appearanceItems.push({ description: 'Reset default note style', event: () => (Doc.UserDoc().defaultTextLayout = undefined), icon: 'eye' }); - appearanceItems.push({ description: (Doc.UserDoc().defaultToFlashcards ? 'Disable' : 'Enable') + ' Flashcard Notes', event: () => (Doc.UserDoc().defaultToFlashcards = !Doc.UserDoc().defaultToFlashcards), icon: 'eye' }); - appearanceItems.push({ - description: `${this.fitContentsToBox ? 'Make Zoomable' : 'Scale to Window'}`, - event: () => (this.Document._fitContentsToBox = !this.fitContentsToBox), - icon: !this.fitContentsToBox ? 'expand-arrows-alt' : 'compress-arrows-alt', - }); appearanceItems.push({ description: `Pin View`, event: () => TabDocView.PinDoc(this.rootDoc, { pinViewport: MarqueeView.CurViewBounds(this.rootDoc, this.props.PanelWidth(), this.props.PanelHeight()) }), icon: 'map-pin' }); !Doc.noviceMode && appearanceItems.push({ description: `update icon`, event: this.updateIcon, icon: 'compress-arrows-alt' }); this.props.renderDepth && appearanceItems.push({ description: 'Ungroup collection', event: this.promoteCollection, icon: 'table' }); @@ -1781,7 +1773,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection !Doc.noviceMode ? viewCtrlItems.push({ description: (SnappingManager.GetShowSnapLines() ? 'Hide' : 'Show') + ' Snap Lines', event: () => SnappingManager.SetShowSnapLines(!SnappingManager.GetShowSnapLines()), icon: 'compress-arrows-alt' }) : null; - !Doc.noviceMode ? viewCtrlItems.push({ description: (this.Document._useClusters ? 'Hide' : 'Show') + ' Clusters', event: () => this.updateClusters(!this.Document._useClusters), icon: 'braille' }) : null; + !Doc.noviceMode ? viewCtrlItems.push({ description: (this.Document._freeform_useClusters ? 'Hide' : 'Show') + ' Clusters', event: () => this.updateClusters(!this.Document._freeform_useClusters), icon: 'braille' }) : null; !viewctrls && ContextMenu.Instance.addItem({ description: 'UI Controls...', subitems: viewCtrlItems, icon: 'eye' }); const options = ContextMenu.Instance.findByDescription('Options...'); @@ -1847,10 +1839,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection incrementalRender = action(() => { if (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath())) { - const unrendered = this.childDocs.filter(doc => !this._renderCutoffData.get(doc[Id])); + const layout_unrendered = this.childDocs.filter(doc => !this._renderCutoffData.get(doc[Id])); const loadIncrement = 5; - for (var i = 0; i < Math.min(unrendered.length, loadIncrement); i++) { - this._renderCutoffData.set(unrendered[i][Id] + '', true); + for (var i = 0; i < Math.min(layout_unrendered.length, loadIncrement); i++) { + this._renderCutoffData.set(layout_unrendered[i][Id] + '', true); } } this.childDocs.some(doc => !this._renderCutoffData.get(doc[Id])) && setTimeout(this.incrementalRender, 1); @@ -1897,7 +1889,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection r?.addEventListener('dashDragAutoScroll', this.onDragAutoScroll as any); }} style={{ opacity: this.props.dontRenderDocuments ? 0.7 : undefined }}> - {this.layoutDoc._backgroundGridShow ? ( + {this.layoutDoc._freeform_backgroundGrid ? ( <div> <CollectionFreeFormBackgroundGrid // bcz : UGHH don't know why, but if we don't wrap in a div, then PDF's don't render whenn taking snapshot of a dashboard and the background grid is on!!? PanelWidth={this.props.PanelWidth} @@ -1937,7 +1929,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection const nh = this.nativeHeight; const hscale = nh ? this.props.PanelHeight() / nh : 1; const wscale = nw ? this.props.PanelWidth() / nw : 1; - return wscale < hscale || this.layoutDoc.fitWidth ? wscale : hscale; + return wscale < hscale || this.layoutDoc.layout_fitWidth ? wscale : hscale; } nativeDim = () => this.nativeDimScaling; @@ -1969,6 +1961,12 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection lightboxPanelWidth = () => Math.max(0, this.props.PanelWidth() - 30); lightboxPanelHeight = () => Math.max(0, this.props.PanelHeight() - 30); lightboxScreenToLocal = () => this.props.ScreenToLocalTransform().translate(-15, -15); + onPassiveWheel = (e: WheelEvent) => { + const docHeight = NumCast(this.rootDoc[Doc.LayoutFieldKey(this.rootDoc) + '_nativeHeight'], this.nativeHeight); + const scrollable = NumCast(this.layoutDoc[this.scaleFieldKey], 1) === 1 && docHeight > this.props.PanelHeight() / this.nativeDimScaling; + this.props.isSelected() && !scrollable && e.preventDefault(); + }; + _oldWheel: any; render() { TraceMobx(); return ( @@ -1976,8 +1974,10 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection className="collectionfreeformview-container" ref={r => { this.createDashEventsTarget(r); + this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel); + this._oldWheel = r; // prevent wheel events from passivly propagating up through containers - !this.props.isAnnotationOverlay && r?.addEventListener('wheel', (e: WheelEvent) => this.props.isSelected() && e.preventDefault(), { passive: false }); + r?.addEventListener('wheel', this.onPassiveWheel, { passive: false }); }} onWheel={this.onPointerWheel} onClick={this.onClick} @@ -1987,12 +1987,7 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection onDragOver={e => e.preventDefault()} onContextMenu={this.onContextMenu} style={{ - pointerEvents: - this.props.Document.type === DocumentType.MARKER - ? 'none' // bcz: ugh.. this is here to prevent markers, which render as freeform views, from grabbing events -- need a better approach. - : SnappingManager.GetIsDragging() && this.childDocs.includes(DragManager.docsBeingDragged.lastElement()) - ? 'all' - : (this.props.pointerEvents?.() as any), + pointerEvents: SnappingManager.GetIsDragging() && this.childDocs.includes(DragManager.docsBeingDragged.lastElement()) ? 'all' : (this.props.pointerEvents?.() as any), textAlign: this.isAnnotationOverlay ? 'initial' : undefined, transform: `scale(${this.nativeDimScaling || 1})`, width: `${100 / (this.nativeDimScaling || 1)}%`, @@ -2012,8 +2007,8 @@ export class CollectionFreeFormView extends CollectionSubView<Partial<collection onKey={this.onKeyDown} onDoubleClick={this.onChildDoubleClickHandler} onBrowseClick={this.onBrowseClickHandler} - docFilters={this.childDocFilters} - docRangeFilters={this.childDocRangeFilters} + childFilters={this.childDocFilters} + childFiltersByRanges={this.childDocRangeFilters} searchFilterDocs={this.searchFilterDocs} isDocumentActive={this.props.childDocumentsActive?.() ? this.props.isDocumentActive : this.isContentActive} isContentActive={this.props.childContentsActive ?? emptyFunction} @@ -2310,7 +2305,7 @@ ScriptingGlobals.add(function sendToBack(doc: Doc) { }); ScriptingGlobals.add(function resetView() { SelectionManager.Docs().forEach(doc => { - doc._panX = doc._panY = 0; - doc._viewScale = 1; + doc._freeform_panX = doc._freeform_panY = 0; + doc._freeform_scale = 1; }); }); diff --git a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx index 11d466b0f..77cb7f88c 100644 --- a/src/client/views/collections/collectionFreeForm/MarqueeView.tsx +++ b/src/client/views/collections/collectionFreeForm/MarqueeView.tsx @@ -1,6 +1,7 @@ import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; -import { AclAdmin, AclAugment, AclEdit, DataSym, Doc, Opt } from '../../../../fields/Doc'; +import { Doc, Opt } from '../../../../fields/Doc'; +import { AclAdmin, AclAugment, AclEdit, DocData } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { InkData, InkField, InkTool } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; @@ -52,8 +53,8 @@ export interface MarqueeViewBounds { @observer export class MarqueeView extends React.Component<SubCollectionViewProps & MarqueeViewProps> { public static CurViewBounds(pinDoc: Doc, panelWidth: number, panelHeight: number) { - const ps = NumCast(pinDoc._viewScale, 1); - return { left: NumCast(pinDoc._panX) - panelWidth / 2 / ps, top: NumCast(pinDoc._panY) - panelHeight / 2 / ps, width: panelWidth / ps, height: panelHeight / ps }; + const ps = NumCast(pinDoc._freeform_scale, 1); + return { left: NumCast(pinDoc._freeform_panX) - panelWidth / 2 / ps, top: NumCast(pinDoc._freeform_panY) - panelHeight / 2 / ps, width: panelWidth / ps, height: panelHeight / ps }; } private _commandExecuted = false; @@ -102,7 +103,6 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this._lassoPts = []; }; - @undoBatch @action onKeyPress = (e: KeyboardEvent) => { //make textbox and add it to this collection @@ -110,8 +110,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque const cm = ContextMenu.Instance; const [x, y] = this.Transform.transformPoint(this._downX, this._downY); if (e.key === '?') { - cm.setDefaultItem('?', (str: string) => this.props.addDocTab(Docs.Create.WebDocument(`https://bing.com/search?q=${str}`, { _width: 400, x, y, _height: 512, _nativeWidth: 850, title: 'bing', useCors: true }), OpenWhere.addRight)); - + cm.setDefaultItem('?', (str: string) => this.props.addDocTab(Docs.Create.WebDocument(`https://bing.com/search?q=${str}`, { _width: 400, x, y, _height: 512, _nativeWidth: 850, title: 'bing', data_useCors: true }), OpenWhere.addRight)); cm.displayMenu(this._downX, this._downY, undefined, true); e.stopPropagation(); } else if (e.key === 'u' && this.props.ungroup) { @@ -173,7 +172,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque e.stopPropagation(); } else if (!e.ctrlKey && !e.metaKey && SelectionManager.Views().length < 2) { FormattedTextBox.SelectOnLoadChar = Doc.UserDoc().defaultTextLayout && !this.props.childLayoutString ? e.key : ''; - FormattedTextBox.LiveTextUndo = UndoManager.StartBatch('live text batch'); + FormattedTextBox.LiveTextUndo = UndoManager.StartBatch('type new note'); this.props.addLiveTextDocument(DocUtils.GetNewTextDoc('-typed text-', x, y, 200, 100, this.props.xPadding === 0)); e.stopPropagation(); } @@ -196,7 +195,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque const rowProto = new Doc(); rowProto.title = rowProto.Id; rowProto._width = 200; - rowProto.isPrototype = true; + rowProto.isDataDoc = true; for (let i = 1; i < ns.length - 1; i++) { const values = ns[i].split('\t'); if (values.length === 1 && columns.length > 1) { @@ -204,7 +203,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque continue; } const docDataProto = Doc.MakeDelegate(rowProto); - docDataProto.isPrototype = true; + docDataProto.isDataDoc = true; columns.forEach((col, i) => (docDataProto[columns[i]] = values.length > i ? (values[i].indexOf(Number(values[i]).toString()) !== -1 ? Number(values[i]) : values[i]) : undefined)); if (groupAttr) { docDataProto._group = groupAttr; @@ -327,7 +326,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } else { this._downX = x; this._downY = y; - const effectiveAcl = GetEffectiveAcl(this.props.Document[DataSym]); + const effectiveAcl = GetEffectiveAcl(this.props.Document[DocData]); if ([AclAdmin, AclEdit, AclAugment].includes(effectiveAcl)) { PreviewCursor.Show(x, y, this.onKeyPress, this.props.addLiveTextDocument, this.props.getTransform, this.props.addDocument, this.props.nudge, this.props.slowLoadDocuments); } @@ -338,7 +337,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque @action onClick = (e: React.MouseEvent): void => { if (this.props.pointerEvents?.() === 'none') return; - if (Math.abs(e.clientX - this._downX) < Utils.DRAG_THRESHOLD && Math.abs(e.clientY - this._downY) < Utils.DRAG_THRESHOLD) { + if (Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, Date.now())) { if (Doc.ActiveTool === InkTool.None) { if (!(e.nativeEvent as any).marqueeHit) { (e.nativeEvent as any).marqueeHit = true; @@ -379,10 +378,10 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque Doc.GetProto(doc).data = new List<Doc>(selected); Doc.GetProto(doc).title = makeGroup ? 'grouping' : 'nested freeform'; !this.props.isAnnotationOverlay && Doc.AddFileOrphan(Doc.GetProto(doc)); - doc._panX = doc._panY = 0; + doc._freeform_panX = doc._freeform_panY = 0; return doc; })(Doc.MakeCopy(Doc.UserDoc().emptyCollection as Doc, true)); - newCollection.system = undefined; + newCollection.isSystem = undefined; newCollection._width = this.Bounds.width; newCollection._height = this.Bounds.height; newCollection._isGroup = makeGroup; @@ -390,12 +389,13 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque newCollection.enableDragWhenActive = makeGroup; newCollection.x = this.Bounds.left; newCollection.y = this.Bounds.top; - newCollection.fitWidth = true; - selected.forEach(d => (d.context = newCollection)); + newCollection.layout_fitWidth = true; + selected.forEach(d => (d.embedContainer = newCollection)); this.hideMarquee(); return newCollection; }); + @undoBatch @action pileup = (e: KeyboardEvent | React.PointerEvent | undefined) => { const selected = this.marqueeSelect(false); @@ -432,8 +432,8 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque } const newCollection = this.getCollection(selected, (e as KeyboardEvent)?.key === 't' ? Docs.Create.StackingDocument : undefined, group); - newCollection._panX = this.Bounds.left + this.Bounds.width / 2; - newCollection._panY = this.Bounds.top + this.Bounds.height / 2; + newCollection._freeform_panX = this.Bounds.left + this.Bounds.width / 2; + newCollection._freeform_panY = this.Bounds.top + this.Bounds.height / 2; newCollection._currentFrame = activeFrame; this.props.addDocument?.(newCollection); this.props.selectDocuments([newCollection]); @@ -508,35 +508,35 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque }; @undoBatch - @action - summary = (e: KeyboardEvent | React.PointerEvent | undefined) => { + summary = action((e: KeyboardEvent | React.PointerEvent | undefined) => { const selected = this.marqueeSelect(false).map(d => { this.props.removeDocument?.(d); d.x = NumCast(d.x) - this.Bounds.left; d.y = NumCast(d.y) - this.Bounds.top; return d; }); - const summary = Docs.Create.TextDocument('', { backgroundColor: '#e2ad32', x: this.Bounds.left, y: this.Bounds.top, followLinkToggle: true, _width: 200, _height: 200, _fitContentsToBox: true, _showSidebar: true, title: 'overview' }); + const summary = Docs.Create.TextDocument('', { + backgroundColor: '#e2ad32', + x: this.Bounds.left, + y: this.Bounds.top, + followLinkToggle: true, + _width: 200, + _height: 200, + _layout_fitContentsToBox: true, + _layout_showSidebar: true, + title: 'overview', + }); const portal = Docs.Create.FreeformDocument(selected, { x: this.Bounds.left + 200, y: this.Bounds.top, isGroup: true, backgroundColor: 'transparent' }); - DocUtils.MakeLink(summary, portal, { linkRelationship: 'summary of:summarized by' }); + DocUtils.MakeLink(summary, portal, { link_relationship: 'summary of:summarized by' }); portal.hidden = true; this.props.addDocument?.(portal); this.props.addLiveTextDocument(summary); MarqueeOptionsMenu.Instance.fadeOut(true); - }; + }); @action - background = (e: KeyboardEvent | React.PointerEvent | undefined) => { - const newCollection = this.getCollection([], undefined, undefined); - this.props.addDocument?.(newCollection); - MarqueeOptionsMenu.Instance.fadeOut(true); - this.hideMarquee(); - setTimeout(() => this.props.selectDocuments([newCollection])); - }; - - @undoBatch - marqueeCommand = action((e: KeyboardEvent) => { + marqueeCommand = (e: KeyboardEvent) => { if (this._commandExecuted || (e as any).propagationIsStopped) { return; } @@ -547,7 +547,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque this.delete(e, e.key === 'h'); e.stopPropagation(); } - if ('cbtsSpg'.indexOf(e.key) !== -1) { + if ('ctsSpg'.indexOf(e.key) !== -1) { this._commandExecuted = true; e.stopPropagation(); e.preventDefault(); @@ -555,7 +555,6 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque if (e.key === 'g') this.collection(e, true); if (e.key === 'c' || e.key === 't') this.collection(e); if (e.key === 's' || e.key === 'S') this.summary(e); - if (e.key === 'b') this.background(e); if (e.key === 'p') this.pileup(e); this.cleanupInteractions(false); } @@ -565,7 +564,7 @@ export class MarqueeView extends React.Component<SubCollectionViewProps & Marque e.preventDefault(); this._lassoFreehand = !this._lassoFreehand; } - }); + }; touchesLine(r1: { left: number; top: number; width: number; height: number }) { for (const lassoPt of this._lassoPts) { diff --git a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx index efd73a927..65578f214 100644 --- a/src/client/views/collections/collectionLinear/CollectionLinearView.tsx +++ b/src/client/views/collections/collectionLinear/CollectionLinearView.tsx @@ -3,9 +3,10 @@ import { Tooltip } from '@material-ui/core'; import { action, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, HeightSym, Opt, WidthSym } from '../../../../fields/Doc'; +import { Doc, Opt } from '../../../../fields/Doc'; +import { Height, Width } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; -import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; +import { BoolCast, Cast, DocCast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; import { emptyFunction, returnEmptyDoclist, returnTrue, StopEvent, Utils } from '../../../../Utils'; import { CollectionViewType } from '../../../documents/DocumentTypes'; import { DocumentManager } from '../../../util/DocumentManager'; @@ -15,6 +16,7 @@ import { DocumentLinksButton } from '../../nodes/DocumentLinksButton'; import { DocumentView } from '../../nodes/DocumentView'; import { LinkDescriptionPopup } from '../../nodes/LinkDescriptionPopup'; import { StyleProp } from '../../StyleProvider'; +import { UndoStack } from '../../UndoStack'; import { CollectionStackedTimeline } from '../CollectionStackedTimeline'; import { CollectionSubView } from '../CollectionSubView'; import './CollectionLinearView.scss'; @@ -22,7 +24,7 @@ import './CollectionLinearView.scss'; /** * CollectionLinearView is the class for rendering the horizontal collection * of documents, it useful for horizontal menus. It can either be expandable - * or not using the linearViewExpandable field. + * or not using the linearView_Expandable field. * It is used in the following locations: * - It is used in the popup menu on the bottom left (see docButtons() in MainView.tsx) * - It is used for the context sensitive toolbar at the top (see contMenuButtons() in CollectionMenu.tsx) @@ -39,12 +41,12 @@ export class CollectionLinearView extends CollectionSubView() { this._dropDisposer?.(); this._widthDisposer?.(); this._selectedDisposer?.(); - this.childLayoutPairs.map((pair, ind) => ScriptCast(pair.layout.proto?.onPointerUp)?.script.run({ this: pair.layout.proto }, console.log)); + this.childLayoutPairs.map((pair, ind) => ScriptCast(DocCast(pair.layout.proto)?.onPointerUp)?.script.run({ this: pair.layout.proto }, console.log)); } componentDidMount() { this._widthDisposer = reaction( - () => 5 + NumCast(this.rootDoc.linearBtnWidth, this.dimension()) + (this.layoutDoc.linearViewIsExpanded ? this.childDocs.filter(doc => !doc.hidden).reduce((tot, doc) => (doc[WidthSym]() || this.dimension()) + tot + 4, 0) : 0), + () => 5 + NumCast(this.rootDoc.linearBtnWidth, this.dimension()) + (this.layoutDoc.linearView_IsExpanded ? this.childDocs.filter(doc => !doc.hidden).reduce((tot, doc) => (doc[Width]() || this.dimension()) + tot + 4, 0) : 0), width => this.childDocs.length && (this.layoutDoc._width = width), { fireImmediately: true } ); @@ -60,11 +62,11 @@ export class CollectionLinearView extends CollectionSubView() { if (isSelected) { selected = pair; } else { - ScriptCast(pair.layout.proto?.onPointerUp)?.script.run({ this: pair.layout.proto }, console.log); + ScriptCast(DocCast(pair.layout.proto)?.onPointerUp)?.script.run({ this: pair.layout.proto }, console.log); } }); if (selected && selected.layout) { - ScriptCast(selected.layout.proto?.onPointerDown)?.script.run({ this: selected.layout.proto }, console.log); + ScriptCast(DocCast(selected.layout.proto)?.onPointerDown)?.script.run({ this: selected.layout.proto }, console.log); } }), { fireImmediately: true } @@ -161,11 +163,17 @@ export class CollectionLinearView extends CollectionSubView() { </span> ); }; + getDisplayDoc = (doc: Doc, preview: boolean = false) => { - if (doc.icon === 'linkui') return this.getLinkUI(); - if (doc.icon === 'currentlyplayingui') return this.getCurrentlyPlayingUI(); + // hack to avoid overhead of making UndoStack,etc into DocumentView style Boxes. If the UndoStack is ever intended to become part of the persisten state of the dashboard, then this would have to change. + // prettier-ignore + switch (doc.layout) { + case '<LinkingUI>': return this.getLinkUI(); + case '<CurrentlyPlayingUI>': return this.getCurrentlyPlayingUI(); + case '<UndoStack>': return <UndoStack key={doc[Id]} width={200} height={40} inline={true} />; + } - const nested = doc._viewType === CollectionViewType.Linear; + const nested = doc._type_collection === CollectionViewType.Linear; const hidden = doc.hidden === true; let dref: Opt<HTMLDivElement>; @@ -197,8 +205,8 @@ export class CollectionLinearView extends CollectionSubView() { rootSelected={this.props.isSelected} removeDocument={this.props.removeDocument} ScreenToLocalTransform={docXf} - PanelWidth={doc[WidthSym]} - PanelHeight={nested || doc._height ? doc[HeightSym] : this.dimension} + PanelWidth={doc[Width]} + PanelHeight={nested || doc._height ? doc[Height] : this.dimension} renderDepth={this.props.renderDepth + 1} dontRegisterView={BoolCast(this.rootDoc.childDontRegisterViews)} focus={emptyFunction} @@ -206,8 +214,8 @@ export class CollectionLinearView extends CollectionSubView() { docViewPath={returnEmptyDoclist} whenChildContentsActiveChanged={emptyFunction} bringToFront={emptyFunction} - docFilters={this.props.docFilters} - docRangeFilters={this.props.docRangeFilters} + childFilters={this.props.childFilters} + childFiltersByRanges={this.props.childFiltersByRanges} searchFilterDocs={this.props.searchFilterDocs} hideResizeHandles={true} /> @@ -218,7 +226,7 @@ export class CollectionLinearView extends CollectionSubView() { render() { const flexDir = StrCast(this.Document.flexDirection); // Specify direction of linear view content const flexGap = NumCast(this.Document.flexGap); // Specify the gap between linear view content - const isExpanded = BoolCast(this.layoutDoc.linearViewIsExpanded); + const isExpanded = BoolCast(this.layoutDoc.linearView_IsExpanded); const menuOpener = ( <label @@ -231,9 +239,9 @@ export class CollectionLinearView extends CollectionSubView() { ); return ( - <div className={`collectionLinearView-outer ${this.layoutDoc.linearViewSubMenu}`} style={{ backgroundColor: this.layoutDoc.linearViewIsExpanded ? undefined : 'transparent' }}> + <div className={`collectionLinearView-outer ${this.layoutDoc.linearView_SubMenu}`} style={{ backgroundColor: this.layoutDoc.linearView_IsExpanded ? undefined : 'transparent' }}> <div className="collectionLinearView" ref={this.createDashEventsTarget} onContextMenu={this.myContextMenu} style={{ minHeight: this.dimension() }}> - {!this.props.Document.linearViewExpandable ? null : ( + {!this.props.Document.linearView_Expandable ? null : ( <Tooltip title={<div className="dash-tooltip">{isExpanded ? 'Close' : 'Open'}</div>} placement="top"> {menuOpener} </Tooltip> @@ -251,11 +259,11 @@ export class CollectionLinearView extends CollectionSubView() { scriptContext: this.props.scriptContext, documentView: this.props.DocumentView?.(), }); - this.layoutDoc.linearViewIsExpanded = this.addMenuToggle.current!.checked; + this.layoutDoc.linearView_IsExpanded = this.addMenuToggle.current!.checked; })} /> - {!this.layoutDoc.linearViewIsExpanded ? null : ( + {!this.layoutDoc.linearView_IsExpanded ? null : ( <div className="collectionLinearView-content" style={{ diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx index 78d3d1b6e..34fa0343d 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMulticolumnView.tsx @@ -259,8 +259,8 @@ export class CollectionMulticolumnView extends CollectionSubView() { hideDecorationTitle={this.props.childHideDecorationTitle?.()} fitContentsToBox={this.props.fitContentsToBox} focus={this.props.focus} - docFilters={this.childDocFilters} - docRangeFilters={this.childDocRangeFilters} + childFilters={this.childDocFilters} + childFiltersByRanges={this.childDocRangeFilters} searchFilterDocs={this.searchFilterDocs} dontRegisterView={this.props.dontRegisterView} addDocument={this.props.addDocument} diff --git a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx index 4d61dc272..bab77ed48 100644 --- a/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx +++ b/src/client/views/collections/collectionMulticolumn/CollectionMultirowView.tsx @@ -258,8 +258,8 @@ export class CollectionMultirowView extends CollectionSubView() { hideDecorationTitle={this.props.childHideDecorationTitle?.()} fitContentsToBox={this.props.fitContentsToBox} focus={this.props.focus} - docFilters={this.childDocFilters} - docRangeFilters={this.childDocRangeFilters} + childFilters={this.childDocFilters} + childFiltersByRanges={this.childDocRangeFilters} searchFilterDocs={this.searchFilterDocs} dontRegisterView={this.props.dontRegisterView} addDocument={this.props.addDocument} diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss index a9434fde3..52ebb7763 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaView.scss +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.scss @@ -104,7 +104,7 @@ .schema-header-row { cursor: grab; - justify-content: flex-end; + //justify-content: flex-end; .row-menu { display: flex; @@ -182,8 +182,10 @@ .schema-table-cell, .row-menu { border: 1px solid $medium-gray; - overflow: hidden; + overflow-x: hidden; + overflow-y: auto; padding: 5px; + display: inline-block; } .schema-row { diff --git a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx index a59d7e5a3..15424de98 100644 --- a/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx +++ b/src/client/views/collections/collectionSchema/CollectionSchemaView.tsx @@ -8,17 +8,16 @@ import { Id } from '../../../../fields/FieldSymbols'; import { List } from '../../../../fields/List'; import { listSpec } from '../../../../fields/Schema'; import { BoolCast, Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; -import { emptyFunction, returnDefault, returnEmptyDoclist, returnEmptyString, returnFalse, returnIgnore, returnNever, returnTrue, setupMoveUpEvents, smoothScroll } from '../../../../Utils'; +import { emptyFunction, returnEmptyDoclist, returnEmptyString, returnFalse, returnIgnore, returnNever, returnTrue, setupMoveUpEvents, smoothScroll } from '../../../../Utils'; import { Docs, DocumentOptions, DocUtils, FInfo } from '../../../documents/Documents'; import { DocumentManager } from '../../../util/DocumentManager'; import { DragManager } from '../../../util/DragManager'; import { SelectionManager } from '../../../util/SelectionManager'; -import { undoBatch } from '../../../util/UndoManager'; +import { undoable, undoBatch } from '../../../util/UndoManager'; import { ContextMenu } from '../../ContextMenu'; import { EditableView } from '../../EditableView'; import { Colors } from '../../global/globalEnums'; import { DocFocusOptions, DocumentView } from '../../nodes/DocumentView'; -import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; import { KeyValueBox } from '../../nodes/KeyValueBox'; import { DefaultStyleProvider } from '../../StyleProvider'; import { CollectionSubView } from '../CollectionSubView'; @@ -33,6 +32,7 @@ export enum ColumnType { Date, Image, RTF, + Enumeration, Any, } @@ -43,9 +43,10 @@ export const FInfotoColType: { [key: string]: ColumnType } = { date: ColumnType.Date, image: ColumnType.Image, rtf: ColumnType.RTF, + enumeration: ColumnType.Enumeration, }; -const defaultColumnKeys: string[] = ['title', 'type', 'author', 'creationDate', 'text']; +const defaultColumnKeys: string[] = ['title', 'type', 'author', 'author_date', 'text']; @observer export class CollectionSchemaView extends CollectionSubView() { @@ -55,8 +56,10 @@ export class CollectionSchemaView extends CollectionSubView() { private _makeNewColumn: boolean = false; private _documentOptions: DocumentOptions = new DocumentOptions(); private _tableContentRef: HTMLDivElement | null = null; + private _menuTarget = React.createRef<HTMLDivElement>(); - public static _rowHeight: number = 50; + static _rowHeight: number = 50; + static _rowSingleLineHeight: number = 32; public static _minColWidth: number = 25; public static _rowMenuWidth: number = 60; public static _previewDividerWidth: number = 4; @@ -77,8 +80,13 @@ export class CollectionSchemaView extends CollectionSubView() { @observable _filterSearchValue: string = ''; @observable _selectedCell: [Doc, number] | undefined; + // target HTMLelement portal for showing a popup menu to edit cell values. + public get MenuTarget() { + return this._menuTarget.current; + } + @computed get _selectedDocs() { - return SelectionManager.Docs().filter(doc => Doc.AreProtosEqual(DocCast(doc.context), this.rootDoc)); + return SelectionManager.Docs().filter(doc => Doc.AreProtosEqual(DocCast(doc.embedContainer), this.rootDoc)); } @computed get documentKeys() { @@ -86,7 +94,7 @@ export class CollectionSchemaView extends CollectionSubView() { } @computed get previewWidth() { - return NumCast(this.layoutDoc.schemaPreviewWidth); + return NumCast(this.layoutDoc.schema_previewWidth); } @computed get tableWidth() { @@ -94,12 +102,12 @@ export class CollectionSchemaView extends CollectionSubView() { } @computed get columnKeys() { - return Cast(this.layoutDoc.columnKeys, listSpec('string'), defaultColumnKeys); + return Cast(this.layoutDoc.schema_columnKeys, listSpec('string'), defaultColumnKeys); } @computed get storedColumnWidths() { const widths = NumListCast( - this.layoutDoc.columnWidths, + this.layoutDoc.schema_columnWidths, this.columnKeys.map(() => (this.tableWidth - CollectionSchemaView._rowMenuWidth) / this.columnKeys.length) ); @@ -223,7 +231,7 @@ export class CollectionSchemaView extends CollectionSubView() { @undoBatch @action - setSort = (field: string | undefined, desc: boolean = false) => { + setColumnSort = (field: string | undefined, desc: boolean = false) => { this.layoutDoc.sortField = field; this.layoutDoc.sortDesc = desc; }; @@ -242,7 +250,7 @@ export class CollectionSchemaView extends CollectionSubView() { let currKeys = [...this.columnKeys]; currKeys[index] = newKey; - this.layoutDoc.columnKeys = new List<string>(currKeys); + this.layoutDoc.schema_columnKeys = new List<string>(currKeys); }; @undoBatch @@ -256,11 +264,11 @@ export class CollectionSchemaView extends CollectionSubView() { const currWidths = this.storedColumnWidths.slice(); currWidths.splice(0, 0, newColWidth); const newDesiredTableWidth = currWidths.reduce((w, cw) => w + cw, 0); - this.layoutDoc.columnWidths = new List<number>(currWidths.map(w => (w / newDesiredTableWidth) * (this.tableWidth - CollectionSchemaView._rowMenuWidth))); + this.layoutDoc.schema_columnWidths = new List<number>(currWidths.map(w => (w / newDesiredTableWidth) * (this.tableWidth - CollectionSchemaView._rowMenuWidth))); let currKeys = this.columnKeys.slice(); currKeys.splice(0, 0, key); - this.layoutDoc.columnKeys = new List<string>(currKeys); + this.layoutDoc.schema_columnKeys = new List<string>(currKeys); }; @action @@ -273,11 +281,11 @@ export class CollectionSchemaView extends CollectionSubView() { const currWidths = this.storedColumnWidths.slice(); currWidths.splice(index, 1); const newDesiredTableWidth = currWidths.reduce((w, cw) => w + cw, 0); - this.layoutDoc.columnWidths = new List<number>(currWidths.map(w => (w / newDesiredTableWidth) * (this.tableWidth - CollectionSchemaView._rowMenuWidth))); + this.layoutDoc.schema_columnWidths = new List<number>(currWidths.map(w => (w / newDesiredTableWidth) * (this.tableWidth - CollectionSchemaView._rowMenuWidth))); let currKeys = this.columnKeys.slice(); currKeys.splice(index, 1); - this.layoutDoc.columnKeys = new List<string>(currKeys); + this.layoutDoc.schema_columnKeys = new List<string>(currKeys); }; @action @@ -316,7 +324,7 @@ export class CollectionSchemaView extends CollectionSubView() { @action finishResize = () => { - this.layoutDoc.columnWidths = new List<number>(this._displayColumnWidths); + this.layoutDoc.schema_columnWidths = new List<number>(this._displayColumnWidths); this._displayColumnWidths = undefined; }; @@ -325,20 +333,18 @@ export class CollectionSchemaView extends CollectionSubView() { moveColumn = (fromIndex: number, toIndex: number) => { let currKeys = this.columnKeys.slice(); currKeys.splice(toIndex, 0, currKeys.splice(fromIndex, 1)[0]); - this.layoutDoc.columnKeys = new List<string>(currKeys); + this.layoutDoc.schema_columnKeys = new List<string>(currKeys); let currWidths = this.storedColumnWidths.slice(); currWidths.splice(toIndex, 0, currWidths.splice(fromIndex, 1)[0]); - this.layoutDoc.columnWidths = new List<number>(currWidths); + this.layoutDoc.schema_columnWidths = new List<number>(currWidths); }; @action dragColumn = (e: PointerEvent, index: number) => { const dragData = new DragManager.ColumnDragData(index); const dragEles = [this._colEles[index]]; - this.childDocs.forEach(doc => { - dragEles.push(this._rowEles.get(doc).children[1].children[index]); - }); + this.childDocs.forEach(doc => dragEles.push(this._rowEles.get(doc).children[1].children[index])); DragManager.StartColumnDrag(dragEles, dragData, e.x, e.y); document.removeEventListener('pointermove', this.highlightDropColumn); @@ -352,24 +358,28 @@ export class CollectionSchemaView extends CollectionSubView() { return true; }; - @action - highlightDropColumn = (e: PointerEvent) => { - e.stopPropagation(); - const mouseX = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY)[0]; + findDropIndex = (mouseX: number) => { let index: number | undefined; this.displayColumnWidths.reduce((total, curr, i) => { if (total <= mouseX && total + curr >= mouseX) { - if (mouseX <= total + curr / 2) index = i; + if (mouseX <= total + curr) index = i; else index = i + 1; } return total + curr; }, CollectionSchemaView._rowMenuWidth); + return index; + }; + @action + highlightDropColumn = (e: PointerEvent) => { + e.stopPropagation(); + const mouseX = this.props.ScreenToLocalTransform().transformPoint(e.clientX, e.clientY)[0]; + const index = this.findDropIndex(mouseX); this._colEles.forEach((colRef, i) => { let leftStyle = ''; let rightStyle = ''; - if (i + 1 === index) rightStyle = `solid 2px ${Colors.MEDIUM_BLUE}`; - if (i === index && i === 0) leftStyle = `solid 2px ${Colors.MEDIUM_BLUE}`; + if (i + 1 === index) rightStyle = `solid 12px ${Colors.MEDIUM_BLUE}`; + if (i === index && i === 0) leftStyle = `solid 12px ${Colors.MEDIUM_BLUE}`; colRef.style.borderLeft = leftStyle; colRef.style.borderRight = rightStyle; this.childDocs.forEach(doc => { @@ -426,15 +436,8 @@ export class CollectionSchemaView extends CollectionSubView() { if (de.complete.columnDragData) { e.stopPropagation(); const mouseX = this.props.ScreenToLocalTransform().transformPoint(de.x, de.y)[0]; - let index = de.complete.columnDragData.colIndex; - this.displayColumnWidths.reduce((total, curr, i) => { - if (total <= mouseX && total + curr >= mouseX) { - if (mouseX <= total + curr / 2) index = i; - else index = i + 1; - } - return total + curr; - }, CollectionSchemaView._rowMenuWidth); - this.moveColumn(de.complete.columnDragData.colIndex, index); + const index = this.findDropIndex(mouseX); + this.moveColumn(de.complete.columnDragData.colIndex, index ?? de.complete.columnDragData.colIndex); this._colEles.forEach((colRef, i) => { colRef.style.borderLeft = ''; @@ -479,28 +482,15 @@ export class CollectionSchemaView extends CollectionSubView() { const maxWidth = 1000; const movedWidth = this.props.ScreenToLocalTransform().transformDirection(nativeWidth.right - e.clientX, 0)[0]; const width = movedWidth < minWidth ? minWidth : movedWidth > maxWidth ? maxWidth : movedWidth; - this.layoutDoc.schemaPreviewWidth = width; + this.layoutDoc.schema_previewWidth = width; return false; }; - @action - addNewTextDoc = (value: string, shiftDown?: boolean, forceEmptyNote?: boolean) => { - if (!value && !forceEmptyNote) return false; - const newDoc = Docs.Create.TextDocument(value, { title: value, _autoHeight: true }); - FormattedTextBox.SelectOnLoad = newDoc[Id]; - FormattedTextBox.SelectOnLoadChar = forceEmptyNote ? '' : ' '; - return this.addRow(newDoc) || false; - }; - menuCallback = (x: number, y: number) => { ContextMenu.Instance.clearItems(); - DocUtils.addDocumentCreatorMenuItems(doc => this.addRow(doc), this.addRow, x, y, true); + DocUtils.addDocumentCreatorMenuItems(this.addRow, this.addRow, x, y, true); - ContextMenu.Instance.setDefaultItem('::', (name: string): void => { - Doc.GetProto(this.props.Document)[name] = ''; - this.addRow(Docs.Create.TextDocument('', { title: name, _autoHeight: true })); - }); ContextMenu.Instance.displayMenu(x, y, undefined, true); }; @@ -515,9 +505,9 @@ export class CollectionSchemaView extends CollectionSubView() { if (found) { const rect = found.getBoundingClientRect(); const localRect = this.props.ScreenToLocalTransform().transformBounds(rect.left, rect.top, rect.width, rect.height); - if (localRect.y < CollectionSchemaView._rowHeight || localRect.y + localRect.height > this.props.PanelHeight()) { + if (localRect.y < this.rowHeightFunc() || localRect.y + localRect.height > this.props.PanelHeight()) { let focusSpeed = options.zoomTime ?? 50; - smoothScroll(focusSpeed, this._tableContentRef!, localRect.y + this._tableContentRef!.scrollTop - CollectionSchemaView._rowHeight, options.easeFunc); + smoothScroll(focusSpeed, this._tableContentRef!, localRect.y + this._tableContentRef!.scrollTop - this.rowHeightFunc(), options.easeFunc); return focusSpeed; } } @@ -620,7 +610,7 @@ export class CollectionSchemaView extends CollectionSubView() { this._menuKeys = this.documentKeys.filter(value => value.toLowerCase().includes(this._menuValue.toLowerCase())); }; - getFieldFilters = (field: string) => StrListCast(this.Document._docFilters).filter(filter => filter.split(':')[0] == field); + getFieldFilters = (field: string) => StrListCast(this.Document._childFilters).filter(filter => filter.split(':')[0] == field); removeFieldFilters = (field: string) => { this.getFieldFilters(field).forEach(filter => Doc.setDocFilter(this.Document, field, filter.split(':')[1], 'remove')); @@ -696,6 +686,12 @@ export class CollectionSchemaView extends CollectionSubView() { ); } + onPassiveWheel = (e: WheelEvent) => { + // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this) + if (!this._oldWheel.scrollTop && e.deltaY <= 0) e.preventDefault(); + e.stopPropagation(); + }; + _oldWheel: any; @computed get keysDropdown() { return ( <div className="schema-key-search"> @@ -709,16 +705,11 @@ export class CollectionSchemaView extends CollectionSubView() { </div> <div className="schema-key-list" - ref={r => - r?.addEventListener( - 'wheel', // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this) - (e: WheelEvent) => { - if (!r.scrollTop && e.deltaY <= 0) e.preventDefault(); - e.stopPropagation(); - }, - { passive: false } - ) - }> + ref={r => { + this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel); + this._oldWheel = r; + r?.addEventListener('wheel', this.onPassiveWheel, { passive: false }); + }}> {this._menuKeys.map(key => ( <div className="schema-search-result" @@ -772,7 +763,7 @@ export class CollectionSchemaView extends CollectionSubView() { } }); - const filters = StrListCast(this.Document._docFilters); + const filters = StrListCast(this.Document._childFilters); return keyOptions.map(key => { let bool = false; if (filters !== undefined) { @@ -836,6 +827,7 @@ export class CollectionSchemaView extends CollectionSubView() { }); return { docs }; } + rowHeightFunc = () => (BoolCast(this.layoutDoc._schema_singleLine) ? CollectionSchemaView._rowSingleLineHeight : CollectionSchemaView._rowHeight); sortedDocsFunc = () => this.sortedDocs; isContentActive = () => this.props.isSelected() || this.props.isContentActive(); screenToLocal = () => this.props.ScreenToLocalTransform().translate(-this.tableWidth, 0); @@ -843,6 +835,7 @@ export class CollectionSchemaView extends CollectionSubView() { render() { return ( <div className="collectionSchemaView" ref={(ele: HTMLDivElement | null) => this.createDashEventsTarget(ele)} onDrop={this.onExternalDrop.bind(this)}> + <div ref={this._menuTarget} style={{ background: 'red', top: 0, left: 0, position: 'absolute', zIndex: 10000 }}></div> <div className="schema-table" onWheel={e => this.props.isContentActive() && e.stopPropagation()} @@ -850,7 +843,7 @@ export class CollectionSchemaView extends CollectionSubView() { // prevent wheel events from passively propagating up through containers r?.addEventListener('wheel', (e: WheelEvent) => {}, { passive: false }); }}> - <div className="schema-header-row" style={{ height: CollectionSchemaView._rowHeight }}> + <div className="schema-header-row" style={{ height: this.rowHeightFunc() }}> <div className="row-menu" style={{ width: CollectionSchemaView._rowMenuWidth }}> <div className="schema-header-button" onPointerDown={e => (this._columnMenuIndex === -1 ? this.closeColumnMenu() : this.openColumnMenu(-1, true))}> <FontAwesomeIcon icon="plus" /> @@ -864,19 +857,28 @@ export class CollectionSchemaView extends CollectionSubView() { columnWidths={this.displayColumnWidths} sortField={this.sortField} sortDesc={this.sortDesc} - setSort={this.setSort} + setSort={this.setColumnSort} + rowHeight={this.rowHeightFunc} removeColumn={this.removeColumn} resizeColumn={this.startResize} openContextMenu={this.openContextMenu} dragColumn={this.dragColumn} setColRef={this.setColRef} + isContentActive={this.props.isContentActive} /> ))} </div> {this._columnMenuIndex !== undefined && this.renderColumnMenu} {this._filterColumnIndex !== undefined && this.renderFilterMenu} - <CollectionSchemaViewDocs schema={this} childDocs={this.sortedDocsFunc} setRef={(ref: HTMLDivElement | null) => (this._tableContentRef = ref)} /> - <EditableView GetValue={returnEmptyString} SetValue={this.addNewTextDoc} placeholder={"Type ':' for commands"} contents={'+ New Node'} menuCallback={this.menuCallback} height={CollectionSchemaView._newNodeInputHeight} /> + <CollectionSchemaViewDocs schema={this} childDocs={this.sortedDocsFunc} rowHeight={this.rowHeightFunc} setRef={(ref: HTMLDivElement | null) => (this._tableContentRef = ref)} /> + <EditableView + GetValue={returnEmptyString} + SetValue={undoable(value => (value ? this.addRow(Docs.Create.TextDocument(value, { title: value, _layout_autoHeight: true })) : false), 'add text doc')} + placeholder={"Type ':' for commands"} + contents={'+ New Node'} + menuCallback={this.menuCallback} + height={CollectionSchemaView._newNodeInputHeight} + /> </div> {this.previewWidth > 0 && <div className="schema-preview-divider" style={{ width: CollectionSchemaView._previewDividerWidth }} onPointerDown={this.onDividerDown}></div>} {this.previewWidth > 0 && ( @@ -897,8 +899,8 @@ export class CollectionSchemaView extends CollectionSubView() { isContentActive={returnTrue} isDocumentActive={returnFalse} ScreenToLocalTransform={this.screenToLocal} - docFilters={this.childDocFilters} - docRangeFilters={this.childDocRangeFilters} + childFilters={this.childDocFilters} + childFiltersByRanges={this.childDocRangeFilters} searchFilterDocs={this.searchFilterDocs} styleProvider={DefaultStyleProvider} docViewPath={returnEmptyDoclist} @@ -922,20 +924,20 @@ interface CollectionSchemaViewDocsProps { schema: CollectionSchemaView; setRef: (ref: HTMLDivElement | null) => void; childDocs: () => { docs: Doc[] }; + rowHeight: () => number; } @observer class CollectionSchemaViewDocs extends React.Component<CollectionSchemaViewDocsProps> { tableWidthFunc = () => this.props.schema.tableWidth; - rowHeightFunc = () => CollectionSchemaView._rowHeight; - childScreenToLocal = computedFn((index: number) => () => this.props.schema.props.ScreenToLocalTransform().translate(0, -CollectionSchemaView._rowHeight - index * this.rowHeightFunc())); + childScreenToLocal = computedFn((index: number) => () => this.props.schema.props.ScreenToLocalTransform().translate(0, -this.props.rowHeight() - index * this.props.rowHeight())); render() { return ( - <div className="schema-table-content" ref={this.props.setRef} style={{ height: `calc(100% - ${CollectionSchemaView._newNodeInputHeight + CollectionSchemaView._rowHeight}px)` }}> + <div className="schema-table-content" ref={this.props.setRef} style={{ height: `calc(100% - ${CollectionSchemaView._newNodeInputHeight + this.props.rowHeight()}px)` }}> {this.props.childDocs().docs.map((doc: Doc, index: number) => { const dataDoc = !doc.isTemplateDoc && !doc.isTemplateForField ? undefined : this.props.schema.props.DataDoc; return ( - <div className="schema-row-wrapper" style={{ height: CollectionSchemaView._rowHeight }}> + <div className="schema-row-wrapper" style={{ height: this.props.rowHeight() }}> <DocumentView key={doc[Id]} {...this.props.schema.props} @@ -943,17 +945,18 @@ class CollectionSchemaViewDocs extends React.Component<CollectionSchemaViewDocsP LayoutTemplateString={SchemaRowBox.LayoutString(this.props.schema.props.fieldKey)} Document={doc} DataDoc={dataDoc} + yPadding={index} renderDepth={this.props.schema.props.renderDepth + 1} PanelWidth={this.tableWidthFunc} - PanelHeight={this.rowHeightFunc} + PanelHeight={this.props.rowHeight} styleProvider={DefaultStyleProvider} waitForDoubleClickToClick={returnNever} defaultDoubleClick={returnIgnore} enableDragWhenActive={true} onClickScriptDisable="always" focus={this.props.schema.focusDocument} - docFilters={this.props.schema.childDocFilters} - docRangeFilters={this.props.schema.childDocRangeFilters} + childFilters={this.props.schema.childDocFilters} + childFiltersByRanges={this.props.schema.childDocRangeFilters} searchFilterDocs={this.props.schema.searchFilterDocs} rootSelected={this.props.schema.rootSelected} ScreenToLocalTransform={this.childScreenToLocal(index)} @@ -965,7 +968,7 @@ class CollectionSchemaViewDocs extends React.Component<CollectionSchemaViewDocsP hideTitle={true} hideDocumentButtonBar={true} hideLinkAnchors={true} - fitWidth={returnTrue} + layout_fitWidth={returnTrue} scriptContext={this} canEmbedOnDrag={true} /> diff --git a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx index 243fe0c61..65e47f441 100644 --- a/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx +++ b/src/client/views/collections/collectionSchema/SchemaColumnHeader.tsx @@ -5,8 +5,6 @@ import { observer } from 'mobx-react'; import { emptyFunction, setupMoveUpEvents } from '../../../../Utils'; import { Colors } from '../../global/globalEnums'; import './CollectionSchemaView.scss'; -import { SnappingManager } from '../../../util/SnappingManager'; -import { DragManager } from '../../../util/DragManager'; export interface SchemaColumnHeaderProps { columnKeys: string[]; @@ -14,8 +12,10 @@ export interface SchemaColumnHeaderProps { columnIndex: number; sortField: string; sortDesc: boolean; + isContentActive: (outsideReaction?: boolean | undefined) => boolean | undefined; setSort: (field: string | undefined, desc?: boolean) => void; removeColumn: (index: number) => void; + rowHeight: () => number; resizeColumn: (e: any, index: number) => void; dragColumn: (e: any, index: number) => boolean; openContextMenu: (x: number, y: number, index: number) => void; @@ -45,7 +45,7 @@ export class SchemaColumnHeader extends React.Component<SchemaColumnHeaderProps> @action onPointerDown = (e: React.PointerEvent) => { - setupMoveUpEvents(this, e, e => this.props.dragColumn(e, this.props.columnIndex), emptyFunction, emptyFunction, false); + this.props.isContentActive(true) && setupMoveUpEvents(this, e, e => this.props.dragColumn(e, this.props.columnIndex), emptyFunction, emptyFunction, false); }; render() { diff --git a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx index ca9e0bda0..4f3503751 100644 --- a/src/client/views/collections/collectionSchema/SchemaRowBox.tsx +++ b/src/client/views/collections/collectionSchema/SchemaRowBox.tsx @@ -2,9 +2,12 @@ import React = require('react'); import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { computed } from 'mobx'; import { observer } from 'mobx-react'; +import { computedFn } from 'mobx-utils'; +import { Doc } from '../../../../fields/Doc'; +import { BoolCast } from '../../../../fields/Types'; import { DragManager } from '../../../util/DragManager'; import { SnappingManager } from '../../../util/SnappingManager'; -import { undoBatch } from '../../../util/UndoManager'; +import { undoable } from '../../../util/UndoManager'; import { ViewBoxBaseComponent } from '../../DocComponent'; import { Colors } from '../../global/globalEnums'; import { OpenWhere } from '../../nodes/DocumentView'; @@ -12,8 +15,7 @@ import { FieldView, FieldViewProps } from '../../nodes/FieldView'; import { CollectionSchemaView } from './CollectionSchemaView'; import './CollectionSchemaView.scss'; import { SchemaTableCell } from './SchemaTableCell'; -import { computedFn } from 'mobx-utils'; -import { Doc } from '../../../../fields/Doc'; +import { Transform } from '../../../util/Transform'; @observer export class SchemaRowBox extends ViewBoxBaseComponent<FieldViewProps>() { @@ -58,7 +60,7 @@ export class SchemaRowBox extends ViewBoxBaseComponent<FieldViewProps>() { onPointerMove = (e: any) => { if (!SnappingManager.GetIsDragging()) return; - const dragIsRow = DragManager.docsBeingDragged.some(doc => doc.context === this.schemaDoc); // this.schemaView?._selectedDocs.has(doc) ?? false; + const dragIsRow = DragManager.docsBeingDragged.some(doc => doc.embedContainer === this.schemaDoc); // this.schemaView?._selectedDocs.has(doc) ?? false; if (this._ref && dragIsRow) { const rect = this._ref.getBoundingClientRect(); @@ -95,7 +97,7 @@ export class SchemaRowBox extends ViewBoxBaseComponent<FieldViewProps>() { return ( <div className="schema-row" - style={{ height: CollectionSchemaView._rowHeight, backgroundColor: this.props.isSelected() ? Colors.LIGHT_BLUE : undefined }} + style={{ height: this.props.PanelHeight(), backgroundColor: this.props.isSelected() ? Colors.LIGHT_BLUE : undefined }} onPointerEnter={this.onPointerEnter} onPointerLeave={this.onPointerLeave} ref={(row: HTMLDivElement | null) => { @@ -110,18 +112,18 @@ export class SchemaRowBox extends ViewBoxBaseComponent<FieldViewProps>() { }}> <div className="schema-row-button" - onPointerDown={undoBatch(e => { + onPointerDown={undoable(e => { e.stopPropagation(); this.props.removeDocument?.(this.rootDoc); - })}> + }, 'Delete Row')}> <FontAwesomeIcon icon="times" /> </div> <div className="schema-row-button" - onPointerDown={e => { + onPointerDown={undoable(e => { e.stopPropagation(); this.props.addDocTab(this.rootDoc, OpenWhere.addRight); - }}> + }, 'Open Doc on Right')}> <FontAwesomeIcon icon="external-link-alt" /> </div> </div> @@ -132,13 +134,23 @@ export class SchemaRowBox extends ViewBoxBaseComponent<FieldViewProps>() { Document={this.rootDoc} col={index} fieldKey={key} + allowCRs={false} // to enter text with new lines, must use \n columnWidth={this.columnWidth(index)} + rowHeight={this.schemaView.rowHeightFunc} isRowActive={this.props.isContentActive} getFinfo={this.getFinfo} selectCell={this.selectCell} deselectCell={this.deselectCell} selectedCell={this.selectedCell} setColumnValues={this.setColumnValues} + oneLine={BoolCast(this.schemaDoc?._singleLine)} + menuTarget={this.schemaView.MenuTarget} + transform={() => { + const ind = index === this.schemaView.columnKeys.length - 1 ? this.schemaView.columnKeys.length - 3 : index; + const x = this.schemaView?.displayColumnWidths.reduce((p, c, i) => (i <= ind ? p + c : p), 0); + const y = (this.props.yPadding ?? 0) * this.props.PanelHeight(); + return new Transform(x + CollectionSchemaView._rowMenuWidth, y, 1); + }} /> ))} </div> diff --git a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx index 712bd4491..97264508c 100644 --- a/src/client/views/collections/collectionSchema/SchemaTableCell.tsx +++ b/src/client/views/collections/collectionSchema/SchemaTableCell.tsx @@ -1,26 +1,28 @@ -import React = require('react'); +import * as React from 'react'; +import Select, { MenuPlacement } from 'react-select'; import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import { extname } from 'path'; import DatePicker from 'react-datepicker'; import { DateField } from '../../../../fields/DateField'; import { Doc, DocListCast, Field } from '../../../../fields/Doc'; -import { BoolCast, Cast, DateCast, FieldValue } from '../../../../fields/Types'; +import { RichTextField } from '../../../../fields/RichTextField'; +import { BoolCast, Cast, DateCast, DocCast, FieldValue, StrCast } from '../../../../fields/Types'; import { ImageField } from '../../../../fields/URLField'; import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnFalse, returnZero, Utils } from '../../../../Utils'; import { FInfo } from '../../../documents/Documents'; -import { dropActionType } from '../../../util/DragManager'; +import { DocFocusOrOpen } from '../../../util/DocumentManager'; import { Transform } from '../../../util/Transform'; -import { undoBatch } from '../../../util/UndoManager'; +import { undoable, undoBatch } from '../../../util/UndoManager'; import { EditableView } from '../../EditableView'; import { Colors } from '../../global/globalEnums'; +import { OpenWhere } from '../../nodes/DocumentView'; import { FieldView, FieldViewProps } from '../../nodes/FieldView'; +import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; import { KeyValueBox } from '../../nodes/KeyValueBox'; import { DefaultStyleProvider } from '../../StyleProvider'; import { CollectionSchemaView, ColumnType, FInfotoColType } from './CollectionSchemaView'; import './CollectionSchemaView.scss'; -import { RichTextField } from '../../../../fields/RichTextField'; -import { FormattedTextBox } from '../../nodes/formattedText/FormattedTextBox'; export interface SchemaTableCellProps { Document: Doc; @@ -29,17 +31,27 @@ export interface SchemaTableCellProps { selectCell: (doc: Doc, col: number) => void; selectedCell: () => [Doc, number] | undefined; fieldKey: string; + maxWidth?: () => number; columnWidth: () => number; + rowHeight: () => number; + padding?: number; // default is 5 -- see scss isRowActive: () => boolean | undefined; getFinfo: (fieldKey: string) => FInfo | undefined; setColumnValues: (field: string, value: string) => boolean; + oneLine?: boolean; // whether all input should fit on one line vs allowing textare multiline inputs + allowCRs?: boolean; // allow carriage returns in text input (othewrise CR ends the edit) + finishEdit?: () => void; // notify container that edit is over (eg. to hide view in DashFieldView) + options?: string[]; + menuTarget: HTMLDivElement | null; + transform: () => Transform; } @observer export class SchemaTableCell extends React.Component<SchemaTableCellProps> { - public static colRowHeightFunc() { - return CollectionSchemaView._rowHeight; - } + static addFieldDoc = (doc: Doc, where: OpenWhere) => { + DocFocusOrOpen(doc); + return true; + }; public static renderProps(props: SchemaTableCellProps) { const { Document, fieldKey, getFinfo, columnWidth, isRowActive } = props; let protoCount = 0; @@ -49,14 +61,14 @@ export class SchemaTableCell extends React.Component<SchemaTableCellProps> { break; } protoCount++; - doc = doc.proto; + doc = DocCast(doc.proto); } const parenCount = Math.max(0, protoCount - 1); const color = protoCount === 0 || (fieldKey.startsWith('_') && Document[fieldKey] === undefined) ? 'black' : 'blue'; const textDecoration = color !== 'black' && parenCount ? 'underline' : ''; const fieldProps: FieldViewProps = { - docFilters: returnEmptyFilter, - docRangeFilters: returnEmptyFilter, + childFilters: returnEmptyFilter, + childFiltersByRanges: returnEmptyFilter, searchFilterDocs: returnEmptyDoclist, styleProvider: DefaultStyleProvider, docViewPath: returnEmptyDoclist, @@ -64,19 +76,19 @@ export class SchemaTableCell extends React.Component<SchemaTableCellProps> { isSelected: returnFalse, setHeight: returnFalse, select: emptyFunction, - dropAction: 'alias' as dropActionType, + dropAction: 'embed', bringToFront: emptyFunction, renderDepth: 1, isContentActive: returnFalse, whenChildContentsActiveChanged: emptyFunction, ScreenToLocalTransform: Transform.Identity, focus: emptyFunction, - addDocTab: returnFalse, + addDocTab: SchemaTableCell.addFieldDoc, pinToPres: returnZero, Document, - fieldKey, + fieldKey: fieldKey, PanelWidth: columnWidth, - PanelHeight: SchemaTableCell.colRowHeightFunc, + PanelHeight: props.rowHeight, }; const readOnly = getFinfo(fieldKey)?.readOnly ?? false; const cursor = !readOnly ? 'text' : 'default'; @@ -98,32 +110,37 @@ export class SchemaTableCell extends React.Component<SchemaTableCellProps> { style={{ color, textDecoration, + width: '100%', }}> <EditableView + oneLine={this.props.oneLine} + allowCRs={this.props.allowCRs} contents={<FieldView {...fieldProps} />} editing={this.selected ? undefined : false} GetValue={() => Field.toKeyValueString(this.props.Document, this.props.fieldKey)} - SetValue={undoBatch((value: string, shiftDown?: boolean, enterKey?: boolean) => { + SetValue={undoable((value: string, shiftDown?: boolean, enterKey?: boolean) => { if (shiftDown && enterKey) { this.props.setColumnValues(this.props.fieldKey.replace(/^_/, ''), value); } - return KeyValueBox.SetField(this.props.Document, this.props.fieldKey.replace(/^_/, ''), value); - })} + const ret = KeyValueBox.SetField(this.props.Document, this.props.fieldKey.replace(/^_/, ''), value); + this.props.finishEdit?.(); + return ret; + }, 'edit schema cell')} /> </div> ); } get getCellType() { + const columnTypeStr = this.props.getFinfo(this.props.fieldKey)?.fieldType; const cellValue = this.props.Document[this.props.fieldKey]; if (cellValue instanceof ImageField) return ColumnType.Image; if (cellValue instanceof DateField) return ColumnType.Date; if (cellValue instanceof RichTextField) return ColumnType.RTF; if (typeof cellValue === 'number') return ColumnType.Any; - if (typeof cellValue === 'string') return ColumnType.Any; - if (typeof cellValue === 'boolean') return ColumnType.Any; + if (typeof cellValue === 'string' && columnTypeStr !== 'enumeration') return ColumnType.Any; + if (typeof cellValue === 'boolean') return ColumnType.Boolean; - const columnTypeStr = this.props.getFinfo(this.props.fieldKey)?.fieldType; if (columnTypeStr && columnTypeStr in FInfotoColType) { return FInfotoColType[columnTypeStr]; } @@ -138,6 +155,7 @@ export class SchemaTableCell extends React.Component<SchemaTableCellProps> { case ColumnType.Image: return <SchemaImageCell {...this.props} />; case ColumnType.Boolean: return <SchemaBoolCell {...this.props} />; case ColumnType.RTF: return <SchemaRTFCell {...this.props} />; + case ColumnType.Enumeration: return <SchemaEnumerationCell {...this.props} options={this.props.getFinfo(this.props.fieldKey)?.values?.map(val => val.toString())} />; case ColumnType.Date: // return <SchemaDateCell {...this.props} />; default: return this.defaultCellContent; } @@ -148,7 +166,7 @@ export class SchemaTableCell extends React.Component<SchemaTableCellProps> { <div className="schema-table-cell" onPointerDown={action(e => !this.selected && this.props.selectCell(this.props.Document, this.props.col))} - style={{ width: this.props.columnWidth(), border: this.selected ? `solid 2px ${Colors.MEDIUM_BLUE}` : undefined }}> + style={{ padding: this.props.padding, maxWidth: this.props.maxWidth?.(), width: this.props.columnWidth() || undefined, border: this.selected ? `solid 2px ${Colors.MEDIUM_BLUE}` : undefined }}> {this.content} </div> ); @@ -211,8 +229,8 @@ export class SchemaImageCell extends React.Component<SchemaTableCellProps> { const aspect = Doc.NativeAspect(this.props.Document); // aspect ratio // let width = Math.max(75, this.props.columnWidth); // get a with that is no smaller than 75px // const height = Math.max(75, width / aspect); // get a height either proportional to that or 75 px - const height = CollectionSchemaView._rowHeight - 10; - const width = height * aspect; // increase the width of the image if necessary to maintain proportionality + const height = this.props.rowHeight() ? this.props.rowHeight() - (this.props.padding || 6) * 2 : undefined; + const width = height ? height * aspect : undefined; // increase the width of the image if necessary to maintain proportionality return <img src={this.url} width={width} height={height} style={{}} draggable="false" onPointerEnter={this.showHoverPreview} onPointerMove={this.moveHoverPreview} onPointerLeave={this.removeHoverPreview} />; } @@ -254,7 +272,7 @@ export class SchemaRTFCell extends React.Component<SchemaTableCellProps> { fieldProps.isContentActive = this.selectedFunc; return ( <div className="schemaRTFCell" style={{ display: 'flex', fontStyle: this.selected ? undefined : 'italic', width: '100%', height: '100%', position: 'relative', color, textDecoration, cursor, pointerEvents }}> - {this.selected ? <FormattedTextBox {...fieldProps} /> : (field => (field ? Field.toString(field) : ''))(FieldValue(fieldProps.Document[fieldProps.fieldKey]))} + {this.selected ? <FormattedTextBox allowScroll={true} {...fieldProps} /> : (field => (field ? Field.toString(field) : ''))(FieldValue(fieldProps.Document[fieldProps.fieldKey]))} </div> ); } @@ -288,10 +306,47 @@ export class SchemaBoolCell extends React.Component<SchemaTableCellProps> { if (shiftDown && enterKey) { this.props.setColumnValues(this.props.fieldKey.replace(/^_/, ''), value); } - return KeyValueBox.SetField(this.props.Document, this.props.fieldKey.replace(/^_/, ''), value); + const set = KeyValueBox.SetField(this.props.Document, this.props.fieldKey.replace(/^_/, ''), value); + this.props.finishEdit?.(); + return set; })} /> </div> ); } } +@observer +export class SchemaEnumerationCell extends React.Component<SchemaTableCellProps> { + @computed get selected() { + const selected: [Doc, number] | undefined = this.props.selectedCell(); + return this.props.isRowActive() && selected?.[0] === this.props.Document && selected[1] === this.props.col; + } + render() { + const { color, textDecoration, fieldProps, cursor, pointerEvents } = SchemaTableCell.renderProps(this.props); + const options = this.props.options?.map(facet => ({ value: facet, label: facet })); + return ( + <div className="schemaSelectionCell" style={{ display: 'flex', color, textDecoration, cursor, pointerEvents }}> + <div style={{ width: '100%' }}> + <Select + styles={{ + menuPortal: base => ({ + ...base, + left: 0, + top: 0, + transform: `translate(${this.props.transform().TranslateX}px, ${this.props.transform().TranslateY}px)`, + width: Number(base.width) * this.props.transform().Scale, + zIndex: 9999, + }), + }} + menuPortalTarget={this.props.menuTarget} + menuPosition={'absolute'} + placeholder={StrCast(this.props.Document[this.props.fieldKey], 'select...')} + options={options} + isMulti={false} + onChange={val => KeyValueBox.SetField(this.props.Document, this.props.fieldKey.replace(/^_/, ''), `"${val?.value ?? ''}"`)} + /> + </div> + </div> + ); + } +} diff --git a/src/client/views/global/globalCssVariables.scss b/src/client/views/global/globalCssVariables.scss index 3496bb835..422dae15b 100644 --- a/src/client/views/global/globalCssVariables.scss +++ b/src/client/views/global/globalCssVariables.scss @@ -70,6 +70,10 @@ $DFLT_IMAGE_NATIVE_DIM: 900px; $LEFT_MENU_WIDTH: 60px; $TREE_BULLET_WIDTH: 20px; +$CAROUSEL3D_CENTER_SCALE: 1.3; +$CAROUSEL3D_SIDE_SCALE: 0.3; +$CAROUSEL3D_TOP: 15; + :export { contextMenuZindex: $contextMenu-zindex; SCHEMA_DIVIDER_WIDTH: $SCHEMA_DIVIDER_WIDTH; @@ -84,4 +88,7 @@ $TREE_BULLET_WIDTH: 20px; TREE_BULLET_WIDTH: $TREE_BULLET_WIDTH; INK_MASK_SIZE: $INK_MASK_SIZE; MEDIUM_GRAY: $medium-gray; + CAROUSEL3D_CENTER_SCALE: $CAROUSEL3D_CENTER_SCALE; + CAROUSEL3D_SIDE_SCALE: $CAROUSEL3D_SIDE_SCALE; + CAROUSEL3D_TOP: $CAROUSEL3D_TOP; } diff --git a/src/client/views/global/globalCssVariables.scss.d.ts b/src/client/views/global/globalCssVariables.scss.d.ts index 537ea1e5d..efb702564 100644 --- a/src/client/views/global/globalCssVariables.scss.d.ts +++ b/src/client/views/global/globalCssVariables.scss.d.ts @@ -12,6 +12,9 @@ interface IGlobalScss { TREE_BULLET_WIDTH: string; INK_MASK_SIZE: number; MEDIUM_GRAY: string; + CAROUSEL3D_CENTER_SCALE: string; + CAROUSEL3D_SIDE_SCALE: string; + CAROUSEL3D_TOP: string; } declare const globalCssVariables: IGlobalScss; diff --git a/src/client/views/linking/LinkMenu.tsx b/src/client/views/linking/LinkMenu.tsx index 3f6369898..65d13a6c3 100644 --- a/src/client/views/linking/LinkMenu.tsx +++ b/src/client/views/linking/LinkMenu.tsx @@ -49,7 +49,7 @@ export class LinkMenu extends React.Component<Props> { <LinkMenuGroup key={group[0]} itemHandler={this.props.itemHandler} docView={this.props.docView} sourceDoc={this.props.docView.props.Document} group={group[1]} groupType={group[0]} clearLinkEditor={this.clear} /> )); - return linkItems.length ? linkItems : this.props.style ? [<></>] : [<p key="">No links have been created yet. Drag the linking button onto another document to create a link.</p>]; + return linkItems.length ? linkItems : this.props.style ? [] : [<p key="none">No links have been created yet. Drag the linking button onto another document to create a link.</p>]; }; render() { diff --git a/src/client/views/linking/LinkMenuGroup.tsx b/src/client/views/linking/LinkMenuGroup.tsx index d02a1c4eb..8324a97d9 100644 --- a/src/client/views/linking/LinkMenuGroup.tsx +++ b/src/client/views/linking/LinkMenuGroup.tsx @@ -24,12 +24,12 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> { private _menuRef = React.createRef<HTMLDivElement>(); getBackgroundColor = (): string => { - const linkRelationshipList = StrListCast(Doc.UserDoc().linkRelationshipList); - const linkColorList = StrListCast(Doc.UserDoc().linkColorList); + const link_relationshipList = StrListCast(Doc.UserDoc().link_relationshipList); + const linkColorList = StrListCast(Doc.UserDoc().link_ColorList); let color = 'white'; // if this link's relationship property is not default "link", set its color - if (linkRelationshipList) { - const relationshipIndex = linkRelationshipList.indexOf(this.props.groupType); + if (link_relationshipList) { + const relationshipIndex = link_relationshipList.indexOf(this.props.groupType); const RGBcolor: string = linkColorList[relationshipIndex]; if (RGBcolor) { //set opacity to 0.25 by modifiying the rgb string @@ -47,17 +47,18 @@ export class LinkMenuGroup extends React.Component<LinkMenuGroupProps> { const sourceDoc = this.props.docView.anchorViewDoc ?? (this.props.docView.rootDoc.type === DocumentType.LINK // - ? this.props.docView.props.LayoutTemplateString?.includes('anchor1') - ? DocCast(linkDoc.anchor1) - : DocCast(linkDoc.anchor2) + ? this.props.docView.props.LayoutTemplateString?.includes('link_anchor_1') + ? DocCast(linkDoc.link_anchor_1) + : DocCast(linkDoc.link_anchor_2) : this.props.sourceDoc); const destDoc = !sourceDoc ? undefined : this.props.docView.rootDoc.type === DocumentType.LINK - ? this.props.docView.props.LayoutTemplateString?.includes('anchor1') - ? DocCast(linkDoc.anchor2) - : DocCast(linkDoc.anchor1) - : LinkManager.getOppositeAnchor(linkDoc, sourceDoc) || LinkManager.getOppositeAnchor(linkDoc, Cast(linkDoc.anchor2, Doc, null).annotationOn === sourceDoc ? Cast(linkDoc.anchor2, Doc, null) : Cast(linkDoc.anchor1, Doc, null)); + ? this.props.docView.props.LayoutTemplateString?.includes('link_anchor_1') + ? DocCast(linkDoc.link_anchor_2) + : DocCast(linkDoc.link_anchor_1) + : LinkManager.getOppositeAnchor(linkDoc, sourceDoc) || + LinkManager.getOppositeAnchor(linkDoc, Cast(linkDoc.link_anchor_2, Doc, null).annotationOn === sourceDoc ? Cast(linkDoc.link_anchor_2, Doc, null) : Cast(linkDoc.link_anchor_1, Doc, null)); return !destDoc || !sourceDoc ? null : ( <LinkMenuItem key={linkDoc[Id]} diff --git a/src/client/views/linking/LinkMenuItem.tsx b/src/client/views/linking/LinkMenuItem.tsx index 19d6c2ae2..5af05e491 100644 --- a/src/client/views/linking/LinkMenuItem.tsx +++ b/src/client/views/linking/LinkMenuItem.tsx @@ -8,15 +8,13 @@ import { Cast, DocCast, StrCast } from '../../../fields/Types'; import { WebField } from '../../../fields/URLField'; import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../Utils'; import { DocumentType } from '../../documents/DocumentTypes'; -import { DocumentManager } from '../../util/DocumentManager'; import { DragManager } from '../../util/DragManager'; import { LinkFollower } from '../../util/LinkFollower'; import { LinkManager } from '../../util/LinkManager'; import { SelectionManager } from '../../util/SelectionManager'; import { SettingsManager } from '../../util/SettingsManager'; import { undoBatch } from '../../util/UndoManager'; -import { MainView } from '../MainView'; -import { DocumentView, OpenWhere } from '../nodes/DocumentView'; +import { DocumentView, DocumentViewInternal, OpenWhere } from '../nodes/DocumentView'; import { LinkDocPreview } from '../nodes/LinkDocPreview'; import './LinkMenuItem.scss'; import React = require('react'); @@ -32,7 +30,7 @@ interface LinkMenuItemProps { itemHandler?: (doc: Doc) => void; } -// drag links and drop link targets (aliasing them if needed) +// drag links and drop link targets (embedding them if needed) export async function StartLinkTargetsDrag(dragEle: HTMLElement, docView: DocumentView, downX: number, downY: number, sourceDoc: Doc, specificLinks?: Doc[]) { const draggedDocs = (specificLinks ? specificLinks : LinkManager.Links(sourceDoc)).map(link => LinkManager.getOppositeAnchor(link, sourceDoc)).filter(l => l) as Doc[]; @@ -45,7 +43,7 @@ export async function StartLinkTargetsDrag(dragEle: HTMLElement, docView: Docume const dragData = new DragManager.DocumentDragData(moddrag.length ? moddrag : draggedDocs); dragData.canEmbed = true; - dragData.dropAction = 'alias'; + dragData.dropAction = 'embed'; DragManager.StartDocumentDrag([dragEle], dragData, downX, downY, undefined); } @@ -65,9 +63,9 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { @computed get sourceAnchor() { const ldoc = this.props.linkDoc; - if (this.props.sourceDoc !== ldoc.anchor1 && this.props.sourceDoc !== ldoc.anchor2) { - if (Doc.AreProtosEqual(DocCast(DocCast(ldoc.anchor1).annotationOn), this.props.sourceDoc)) return DocCast(ldoc.anchor1); - if (Doc.AreProtosEqual(DocCast(DocCast(ldoc.anchor2).annotationOn), this.props.sourceDoc)) return DocCast(ldoc.anchor2); + if (this.props.sourceDoc !== ldoc.link_anchor_1 && this.props.sourceDoc !== ldoc.link_anchor_2) { + if (Doc.AreProtosEqual(DocCast(DocCast(ldoc.link_anchor_1).annotationOn), this.props.sourceDoc)) return DocCast(ldoc.link_anchor_1); + if (Doc.AreProtosEqual(DocCast(DocCast(ldoc.link_anchor_2).annotationOn), this.props.sourceDoc)) return DocCast(ldoc.link_anchor_2); } return this.props.sourceDoc; } @@ -79,7 +77,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { this, e, e => { - const dragData = new DragManager.DocumentDragData([this.props.linkDoc], 'alias'); + const dragData = new DragManager.DocumentDragData([this.props.linkDoc], 'embed'); dragData.removeDropProperties = ['hidden']; DragManager.StartDocumentDrag([this._editRef.current!], dragData, e.x, e.y); return true; @@ -89,7 +87,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { const trail = DocCast(this.props.docView.rootDoc.presTrail); if (trail) { Doc.ActivePresentation = trail; - MainView.addDocTabFunc(trail, OpenWhere.replaceRight); + DocumentViewInternal.addDocTabFunc(trail, OpenWhere.replaceRight); } else { SelectionManager.SelectView(this.props.docView, false); if ((SettingsManager.propertiesWidth ?? 0) < 100) { @@ -118,10 +116,10 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { this.props.itemHandler?.(this.props.linkDoc); } else { const focusDoc = - Cast(this.props.linkDoc.anchor1, Doc, null)?.annotationOn === this.props.sourceDoc - ? Cast(this.props.linkDoc.anchor1, Doc, null) - : Cast(this.props.linkDoc.anchor2, Doc, null)?.annotationOn === this.props.sourceDoc - ? Cast(this.props.linkDoc.anchor12, Doc, null) + Cast(this.props.linkDoc.link_anchor_1, Doc, null)?.annotationOn === this.props.sourceDoc + ? Cast(this.props.linkDoc.link_anchor_1, Doc, null) + : Cast(this.props.linkDoc.link_anchor_2, Doc, null)?.annotationOn === this.props.sourceDoc + ? Cast(this.props.linkDoc.link_anchor_12, Doc, null) : undefined; if (focusDoc) this.props.docView.props.focus(focusDoc, { instant: true }); @@ -149,7 +147,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { return ( <div className="linkMenu-item" style={{ background: LinkManager.currentLink === this.props.linkDoc ? 'lightBlue' : undefined }}> - <div className={'linkMenu-item-content expand-two'}> + <div className="linkMenu-item-content expand-two"> <div ref={this._drag} className="linkMenu-name" //title="drag to view target. click to customize." @@ -190,7 +188,7 @@ export class LinkMenuItem extends React.Component<LinkMenuItemProps> { {this.props.linkDoc.linksToAnnotation && Cast(this.props.destinationDoc.data, WebField)?.url.href === this.props.linkDoc.annotationUri ? 'Annotation in' : ''} {StrCast(title)} </p> </div> - {!this.props.linkDoc.description ? null : <p className="linkMenu-description">{StrCast(this.props.linkDoc.description)}</p>} + {!this.props.linkDoc.link_description ? null : <p className="linkMenu-description">{StrCast(this.props.linkDoc.link_description)}</p>} </div> <div className="linkMenu-item-buttons"> diff --git a/src/client/views/linking/LinkPopup.tsx b/src/client/views/linking/LinkPopup.tsx index 5f2d4a7b6..664a0fa4f 100644 --- a/src/client/views/linking/LinkPopup.tsx +++ b/src/client/views/linking/LinkPopup.tsx @@ -89,8 +89,8 @@ export class LinkPopup extends React.Component<LinkPopupProps> { docViewPath={returnEmptyDoclist} whenChildContentsActiveChanged={emptyFunction} bringToFront={emptyFunction} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} /> </div> diff --git a/src/client/views/linking/LinkRelationshipSearch.tsx b/src/client/views/linking/LinkRelationshipSearch.tsx index 53da880e4..9662b2fea 100644 --- a/src/client/views/linking/LinkRelationshipSearch.tsx +++ b/src/client/views/linking/LinkRelationshipSearch.tsx @@ -1,8 +1,8 @@ -import { observer } from "mobx-react"; +import { observer } from 'mobx-react'; import './LinkEditor.scss'; -import React = require("react"); +import React = require('react'); -interface LinkRelationshipSearchProps { +interface link_relationshipSearchProps { results: string[] | undefined; display: string; //callback fn to set rel + hide dropdown upon setting @@ -10,48 +10,48 @@ interface LinkRelationshipSearchProps { toggleSearch: () => void; } @observer -export class LinkRelationshipSearch extends React.Component<LinkRelationshipSearchProps> { - +export class link_relationshipSearch extends React.Component<link_relationshipSearchProps> { handleResultClick = (e: React.MouseEvent) => { const relationship = (e.target as HTMLParagraphElement).textContent; if (relationship) { this.props.handleRelationshipSearchChange(relationship); } - } + }; handleMouseEnter = () => { this.props.toggleSearch(); - } + }; handleMouseLeave = () => { this.props.toggleSearch(); - } + }; /** - * Render an empty div to increase the height of LinkEditor to accommodate 2+ results - */ + * Render an empty div to increase the height of LinkEditor to accommodate 2+ results + */ emptyDiv = () => { - if (this.props.results && this.props.results.length > 2 && this.props.display === "block") { - return <div style={{ height: "50px" }} />; + if (this.props.results && this.props.results.length > 2 && this.props.display === 'block') { + return <div style={{ height: '50px' }} />; } - } + }; render() { return ( <div className="linkEditor-relationship-dropdown-container"> - <div className="linkEditor-relationship-dropdown" - style={{ display: this.props.display }} - onMouseEnter={this.handleMouseEnter} - onMouseLeave={this.handleMouseLeave} - > - { // return a dropdown of relationship results if there exist results - this.props.results - ? this.props.results.map(result => { - return <p key={result} onClick={this.handleResultClick}> - {result} - </p>; + <div className="linkEditor-relationship-dropdown" style={{ display: this.props.display }} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave}> + { + // return a dropdown of relationship results if there exist results + this.props.results ? ( + this.props.results.map(result => { + return ( + <p key={result} onClick={this.handleResultClick}> + {result} + </p> + ); }) - : <p>No matching relationships</p> + ) : ( + <p>No matching relationships</p> + ) } </div> @@ -60,4 +60,4 @@ export class LinkRelationshipSearch extends React.Component<LinkRelationshipSear </div> ); } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/AudioBox.tsx b/src/client/views/nodes/AudioBox.tsx index 68fb19208..6558d215a 100644 --- a/src/client/views/nodes/AudioBox.tsx +++ b/src/client/views/nodes/AudioBox.tsx @@ -76,7 +76,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp return DateCast(this.dataDoc[this.fieldKey + '-recordingStart'])?.date.getTime(); } @computed get rawDuration() { - return NumCast(this.dataDoc[`${this.fieldKey}-duration`]); + return NumCast(this.dataDoc[`${this.fieldKey}_duration`]); } // bcz: shouldn't be needed since it's computed from audio element // mehek: not 100% sure but i think due to the order in which things are loaded this is necessary ^^ // if you get rid of it and set the value to 0 the timeline and waveform will set their bounds incorrectly @@ -123,12 +123,12 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } getLinkData(l: Doc) { - let la1 = l.anchor1 as Doc; - let la2 = l.anchor2 as Doc; + let la1 = l.link_anchor_1 as Doc; + let la2 = l.link_anchor_2 as Doc; const linkTime = this.timeline?.anchorStart(la2) || this.timeline?.anchorStart(la1) || 0; if (Doc.AreProtosEqual(la1, this.dataDoc)) { - la1 = l.anchor2 as Doc; - la2 = l.anchor1 as Doc; + la1 = l.link_anchor_2 as Doc; + la2 = l.link_anchor_1 as Doc; } return { la1, la2, linkTime }; } @@ -139,7 +139,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this.rootDoc, this.dataDoc, this.annotationKey, - this._ele?.currentTime || Cast(this.props.Document._currentTimecode, 'number', null) || (this.mediaState === media_state.Recording ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined), + this._ele?.currentTime || Cast(this.props.Document._layout_currentTimecode, 'number', null) || (this.mediaState === media_state.Recording ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined), undefined, undefined, addAsAnnotation @@ -155,12 +155,12 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this.links .map(l => this.getLinkData(l)) .forEach(({ la1, la2, linkTime }) => { - if (linkTime > NumCast(this.layoutDoc._currentTimecode) && linkTime < this._ele!.currentTime) { + if (linkTime > NumCast(this.layoutDoc._layout_currentTimecode) && linkTime < this._ele!.currentTime) { Doc.linkFollowHighlight(la1); } }); - this.layoutDoc._currentTimecode = this._ele.currentTime; - this.timeline?.scrollToTime(NumCast(this.layoutDoc._currentTimecode)); + this.layoutDoc._layout_currentTimecode = this._ele.currentTime; + this.timeline?.scrollToTime(NumCast(this.layoutDoc._layout_currentTimecode)); } }; @@ -221,7 +221,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp if (this.mediaState === media_state.Recording) { setTimeout(this.updateRecordTime, 30); if (!this._paused) { - this.layoutDoc._currentTimecode = (new Date().getTime() - this._recordStart - this._pausedTime) / 1000; + this.layoutDoc._layout_currentTimecode = (new Date().getTime() - this._recordStart - this._pausedTime) / 1000; } } }; @@ -233,7 +233,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this.dataDoc[this.fieldKey + '-recordingStart'] = new DateField(); DocUtils.ActiveRecordings.push(this); this._recorder.ondataavailable = async (e: any) => { - const [{ result }] = await Networking.UploadFilesToServer(e.data); + const [{ result }] = await Networking.UploadFilesToServer({file: e.data}); if (!(result instanceof Error)) { this.props.Document[this.fieldKey] = new AudioField(result.accessPaths.agnostic.client); } @@ -253,7 +253,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this._recorder = undefined; const now = new Date().getTime(); this._paused && (this._pausedTime += now - this._pauseStart); - this.dataDoc[this.fieldKey + '-duration'] = (now - this._recordStart - this._pausedTime) / 1000; + this.dataDoc[this.fieldKey + '_duration'] = (now - this._recordStart - this._pausedTime) / 1000; this.mediaState = media_state.Paused; this._stream?.getAudioTracks()[0].stop(); const ind = DocUtils.ActiveRecordings.indexOf(this); @@ -362,10 +362,10 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp Doc.GetProto(newDoc).recordingSource = this.dataDoc; Doc.GetProto(newDoc).recordingStart = ComputedField.MakeFunction(`self.recordingSource["${this.fieldKey}-recordingStart"]`); Doc.GetProto(newDoc).mediaState = ComputedField.MakeFunction('self.recordingSource.mediaState'); - if (DocListCast(Doc.MyOverlayDocs?.data).includes(this.rootDoc)) { + if (Doc.IsInMyOverlay(this.rootDoc)) { newDoc.overlayX = this.rootDoc.x; newDoc.overlayY = NumCast(this.rootDoc.y) + NumCast(this.rootDoc._height); - Doc.AddDocToList(Doc.MyOverlayDocs, undefined, newDoc); + Doc.AddToMyOverlay(newDoc); } else { this.props.addDocument?.(newDoc); } @@ -422,11 +422,11 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp if (!this.layoutDoc.dontAutoPlayFollowedLinks) { this.playFrom(this.timeline?.anchorStart(link) || 0, this.timeline?.anchorEnd(link)); } else { - this._ele!.currentTime = this.layoutDoc._currentTimecode = this.timeline?.anchorStart(link) || 0; + this._ele!.currentTime = this.layoutDoc._layout_currentTimecode = this.timeline?.anchorStart(link) || 0; } } else { this.links - .filter(l => l.anchor1 === link || l.anchor2 === link) + .filter(l => l.link_anchor_1 === link || l.link_anchor_2 === link) .forEach(l => { const { la1, la2 } = this.getLinkData(l); const startTime = this.timeline?.anchorStart(la1) || this.timeline?.anchorStart(la2); @@ -435,7 +435,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp if (!this.layoutDoc.dontAutoPlayFollowedLinks) { this.playFrom(startTime, endTime); } else { - this._ele!.currentTime = this.layoutDoc._currentTimecode = startTime; + this._ele!.currentTime = this.layoutDoc._layout_currentTimecode = startTime; } } }); @@ -447,7 +447,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp timelineScreenToLocal = () => this.props.ScreenToLocalTransform().translate(0, -AudioBox.topControlsHeight); - setPlayheadTime = (time: number) => (this._ele!.currentTime = this.layoutDoc._currentTimecode = time); + setPlayheadTime = (time: number) => (this._ele!.currentTime /*= this.layoutDoc._layout_currentTimecode*/ = time); playing = () => this.mediaState === media_state.Playing; @@ -547,7 +547,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp <div className="record-button" onPointerDown={this._paused ? this.recordPlay : this.recordPause}> <FontAwesomeIcon size="2x" icon={this._paused ? 'play' : 'pause'} /> </div> - <div className="record-timecode">{formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode)))}</div> + <div className="record-timecode">{formatTime(Math.round(NumCast(this.layoutDoc._layout_currentTimecode)))}</div> </div> ) : ( <div className="audiobox-start-record" onPointerDown={this.Record}> @@ -623,7 +623,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp {this.audio} <div className="audiobox-timecodes"> - <div className="timecode-current">{this.timeline && formatTime(Math.round(NumCast(this.layoutDoc._currentTimecode) - NumCast(this.timeline.clipStart)))}</div> + <div className="timecode-current">{this.timeline && formatTime(Math.round(NumCast(this.layoutDoc._layout_currentTimecode) - NumCast(this.timeline.clipStart)))}</div> {this.miniPlayer ? ( <div>/</div> ) : ( @@ -656,6 +656,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp ref={action((r: any) => (this._stackedTimeline = r))} {...this.props} CollectionFreeFormDocumentView={undefined} + dataFieldKey={this.fieldKey} fieldKey={this.annotationKey} dictationKey={this.fieldKey + '-dictation'} mediaPath={this.path} @@ -689,7 +690,7 @@ export class AudioBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp <audio ref={this.setRef} className={`audiobox-control${this.props.isContentActive() ? '-interactive' : ''}`} - onLoadedData={action(e => this._ele?.duration && this._ele?.duration !== Infinity && (this.dataDoc[this.fieldKey + '-duration'] = this._ele.duration))}> + onLoadedData={action(e => this._ele?.duration && this._ele?.duration !== Infinity && (this.dataDoc[this.fieldKey + '_duration'] = this._ele.duration))}> <source src={this.path} type="audio/mpeg" /> Not supported. </audio> diff --git a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx index a8c8a0d2e..6710cee63 100644 --- a/src/client/views/nodes/CollectionFreeFormDocumentView.tsx +++ b/src/client/views/nodes/CollectionFreeFormDocumentView.tsx @@ -12,12 +12,11 @@ import { SelectionManager } from '../../util/SelectionManager'; import { Transform } from '../../util/Transform'; import { CollectionFreeFormView } from '../collections/collectionFreeForm/CollectionFreeFormView'; import { DocComponent } from '../DocComponent'; +import { InkingStroke } from '../InkingStroke'; import { StyleProp } from '../StyleProvider'; import './CollectionFreeFormDocumentView.scss'; import { DocumentView, DocumentViewProps, OpenWhere } from './DocumentView'; import React = require('react'); -import { DocumentType } from '../../documents/DocumentTypes'; -import { InkingStroke } from '../InkingStroke'; export interface CollectionFreeFormDocumentViewProps extends DocumentViewProps { dataProvider?: (doc: Doc, replica: string) => { x: number; y: number; zIndex?: number; rotation?: number; color?: string; backgroundColor?: string; opacity?: number; highlight?: boolean; z: number; transition?: string } | undefined; @@ -37,13 +36,13 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF { key: 'x' }, { key: 'y' }, { key: '_rotation', val: 0 }, - { key: '_scrollTop' }, + { key: '_layout_scrollTop' }, { key: 'opacity', val: 1 }, { key: '_currentFrame' }, - { key: 'viewScale', val: 1 }, - { key: 'viewScale', val: 1 }, - { key: 'panX' }, - { key: 'panY' }, + { key: 'freeform_scale', val: 1 }, + { key: 'freeform_scale', val: 1 }, + { key: 'freeform_panX' }, + { key: 'freeform_panY' }, ]; // fields that are configured to be animatable using animation frames public static animStringFields = ['backgroundColor', 'color', 'fillColor']; // fields that are configured to be animatable using animation frames public static animDataFields = (doc: Doc) => (Doc.LayoutFieldKey(doc) ? [Doc.LayoutFieldKey(doc)] : []); // fields that are configured to be animatable using animation frames @@ -155,8 +154,8 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF CollectionFreeFormDocumentView.animStringFields.forEach(val => (doc[val] = ComputedField.MakeInterpolatedString(val, 'activeFrame', doc, currTimecode))); CollectionFreeFormDocumentView.animDataFields(doc).forEach(val => (doc[val] = ComputedField.MakeInterpolatedDataField(val, 'activeFrame', doc, currTimecode))); const targetDoc = doc; // data fields, like rtf 'text' exist on the data doc, so - //doc !== targetDoc && (targetDoc.context = doc.context); // the computed fields don't see the layout doc -- need to copy the context to the data doc (HACK!!!) and set the activeFrame on the data doc (HACK!!!) - targetDoc.activeFrame = ComputedField.MakeFunction('self.context?._currentFrame||0'); + //doc !== targetDoc && (targetDoc.embedContainer = doc.embedContainer); // the computed fields don't see the layout doc -- need to copy the embedContainer to the data doc (HACK!!!) and set the activeFrame on the data doc (HACK!!!) + targetDoc.activeFrame = ComputedField.MakeFunction('self.embedContainer?._currentFrame||0'); targetDoc.dataTransition = 'inherit'; }); } @@ -203,7 +202,7 @@ export class CollectionFreeFormDocumentView extends DocComponent<CollectionFreeF PanelWidth: this.panelWidth, PanelHeight: this.panelHeight, }; - const isInk = StrCast(this.layoutDoc.layout).includes(InkingStroke.name) && !this.props.LayoutTemplateString && !this.layoutDoc._isInkMask; + const isInk = StrCast(this.layoutDoc.layout).includes(InkingStroke.name) && !this.props.LayoutTemplateString && !this.layoutDoc._stroke_isInkMask; return ( <div className="collectionFreeFormDocumentView-container" diff --git a/src/client/views/nodes/ColorBox.tsx b/src/client/views/nodes/ColorBox.tsx index 70ba7e182..aae759702 100644 --- a/src/client/views/nodes/ColorBox.tsx +++ b/src/client/views/nodes/ColorBox.tsx @@ -2,7 +2,8 @@ import React = require('react'); import { action } from 'mobx'; import { observer } from 'mobx-react'; import { ColorState, SketchPicker } from 'react-color'; -import { Doc, HeightSym, WidthSym } from '../../../fields/Doc'; +import { Doc } from '../../../fields/Doc'; +import { Height, Width } from '../../../fields/DocSymbols'; import { InkTool } from '../../../fields/InkField'; import { StrCast } from '../../../fields/Types'; import { DocumentType } from '../../documents/DocumentTypes'; @@ -48,7 +49,7 @@ export class ColorBox extends ViewBoxBaseComponent<FieldViewProps>() { } render() { - const scaling = Math.min(this.layoutDoc.fitWidth ? 10000 : this.props.PanelHeight() / this.rootDoc[HeightSym](), this.props.PanelWidth() / this.rootDoc[WidthSym]()); + const scaling = Math.min(this.layoutDoc.layout_fitWidth ? 10000 : this.props.PanelHeight() / this.rootDoc[Height](), this.props.PanelWidth() / this.rootDoc[Width]()); return ( <div className={`colorBox-container${this.props.isContentActive() ? '-interactive' : ''}`} @@ -72,7 +73,7 @@ export class ColorBox extends ViewBoxBaseComponent<FieldViewProps>() { SetActiveInkWidth(e.target.value); SelectionManager.Views() .filter(i => StrCast(i.rootDoc.type) === DocumentType.INK) - .map(i => (i.rootDoc.strokeWidth = Number(e.target.value))); + .map(i => (i.rootDoc.stroke_width = Number(e.target.value))); }} /> </div> diff --git a/src/client/views/nodes/ComparisonBox.tsx b/src/client/views/nodes/ComparisonBox.tsx index ace388c57..1cc09a63c 100644 --- a/src/client/views/nodes/ComparisonBox.tsx +++ b/src/client/views/nodes/ComparisonBox.tsx @@ -1,5 +1,5 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; -import { action, observable } from 'mobx'; +import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import { Doc, Opt } from '../../../fields/Doc'; import { Cast, NumCast, StrCast } from '../../../fields/Types'; @@ -26,6 +26,13 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl @observable _animating = ''; + @computed get clipWidth() { + return NumCast(this.layoutDoc[this.clipWidthKey], 50); + } + get clipWidthKey() { + return '_' + this.props.fieldKey + '_clipWidth'; + } + componentDidMount() { this.props.setContentView?.(this); } @@ -34,18 +41,17 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl this._disposers[disposerId]?.(); if (ele) { // create disposers identified by disposerId to remove drag & drop listeners - this._disposers[disposerId] = DragManager.MakeDropTarget(ele, (e, dropEvent) => this.dropHandler(e, dropEvent, fieldKey), this.layoutDoc); + this._disposers[disposerId] = DragManager.MakeDropTarget(ele, (e, dropEvent) => this.internalDrop(e, dropEvent, fieldKey), this.layoutDoc); } }; @undoBatch - private dropHandler = (event: Event, dropEvent: DragManager.DropEvent, fieldKey: string) => { + private internalDrop = (event: Event, dropEvent: DragManager.DropEvent, fieldKey: string) => { if (dropEvent.complete.docDragData) { event.stopPropagation(); // prevent parent Doc from registering new position so that it snaps back into place const droppedDocs = dropEvent.complete.docDragData?.droppedDocuments; - if (droppedDocs?.length) { - this.dataDoc[fieldKey] = droppedDocs[0]; - } + droppedDocs.lastElement().embedContainer = this.dataDoc; + this.dataDoc[fieldKey] = droppedDocs.lastElement(); } }; @@ -59,7 +65,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl action(() => { // on click, animate slider movement to the targetWidth this._animating = 'all 200ms'; - this.layoutDoc._clipWidth = (targetWidth * 100) / this.props.PanelWidth(); + this.layoutDoc[this.clipWidthKey] = (targetWidth * 100) / this.props.PanelWidth(); setTimeout( action(() => (this._animating = '')), 200 @@ -71,15 +77,20 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl @action private onPointerMove = ({ movementX }: PointerEvent) => { - const width = movementX * this.props.ScreenToLocalTransform().Scale + (NumCast(this.layoutDoc._clipWidth) / 100) * this.props.PanelWidth(); + const width = movementX * this.props.ScreenToLocalTransform().Scale + (this.clipWidth / 100) * this.props.PanelWidth(); if (width && width > 5 && width < this.props.PanelWidth()) { - this.layoutDoc._clipWidth = (width * 100) / this.props.PanelWidth(); + this.layoutDoc[this.clipWidthKey] = (width * 100) / this.props.PanelWidth(); } return false; }; getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { - const anchor = Docs.Create.ImageanchorDocument({ title: 'ImgAnchor:' + this.rootDoc.title, presTransition: 1000, unrendered: true, annotationOn: this.rootDoc }); + const anchor = Docs.Create.ComparisonConfigDocument({ + title: 'ImgAnchor:' + this.rootDoc.title, + // set presentation timing properties for restoring view + presTransition: 1000, + annotationOn: this.rootDoc, + }); if (anchor) { if (!addAsAnnotation) anchor.backgroundColor = 'transparent'; /* addAsAnnotation &&*/ this.addDocument(anchor); @@ -101,7 +112,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl }; render() { - const clipWidth = NumCast(this.layoutDoc._clipWidth) + '%'; const clearButton = (which: string) => { return ( <div @@ -119,9 +129,6 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl return whichDoc ? ( <> <DocumentView - ref={r => { - //whichDoc !== targetDoc && r?.focus(whichDoc, { instant: true }); - }} {...this.props} NativeWidth={returnZero} NativeHeight={returnZero} @@ -143,7 +150,7 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl }; const displayBox = (which: string, index: number, cover: number) => { return ( - <div className={`${which}Box-cont`} key={which} style={{ width: this.props.PanelWidth() }} onPointerDown={e => this.registerSliding(e, cover)} ref={ele => this.createDropTarget(ele, which, index)}> + <div className={`${index === 0 ? 'before' : 'after'}Box-cont`} key={which} style={{ width: this.props.PanelWidth() }} onPointerDown={e => this.registerSliding(e, cover)} ref={ele => this.createDropTarget(ele, which, index)}> {displayDoc(which)} </div> ); @@ -151,16 +158,16 @@ export class ComparisonBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl return ( <div className={`comparisonBox${this.props.isContentActive() || SnappingManager.GetIsDragging() ? '-interactive' : ''}` /* change className to easily disable/enable pointer events in CSS */}> - {displayBox(this.fieldKey === 'data' ? 'compareBox-after' : `${this.fieldKey}2`, 1, this.props.PanelWidth() - 3)} - <div className="clip-div" style={{ width: clipWidth, transition: this._animating, background: StrCast(this.layoutDoc._backgroundColor, 'gray') }}> - {displayBox(this.fieldKey === 'data' ? 'compareBox-before' : `${this.fieldKey}1`, 0, 0)} + {displayBox(`${this.fieldKey}_2`, 1, this.props.PanelWidth() - 3)} + <div className="clip-div" style={{ width: this.clipWidth + '%', transition: this._animating, background: StrCast(this.layoutDoc._backgroundColor, 'gray') }}> + {displayBox(`${this.fieldKey}_1`, 0, 0)} </div> <div className="slide-bar" style={{ - left: `calc(${clipWidth} - 0.5px)`, - cursor: NumCast(this.layoutDoc._clipWidth) < 5 ? 'e-resize' : NumCast(this.layoutDoc._clipWidth) / 100 > (this.props.PanelWidth() - 5) / this.props.PanelWidth() ? 'w-resize' : undefined, + left: `calc(${this.clipWidth + '%'} - 0.5px)`, + cursor: this.clipWidth < 5 ? 'e-resize' : this.clipWidth / 100 > (this.props.PanelWidth() - 5) / this.props.PanelWidth() ? 'w-resize' : undefined, }} onPointerDown={e => this.registerSliding(e, this.props.PanelWidth() / 2)} /* if clicked, return slide-bar to center */ > diff --git a/src/client/views/nodes/DataVizBox/DataVizBox.tsx b/src/client/views/nodes/DataVizBox/DataVizBox.tsx index eb25d3264..baa45e278 100644 --- a/src/client/views/nodes/DataVizBox/DataVizBox.tsx +++ b/src/client/views/nodes/DataVizBox/DataVizBox.tsx @@ -3,7 +3,7 @@ import { observer } from 'mobx-react'; import * as React from 'react'; import { Doc, StrListCast } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; -import { Cast, NumCast, StrCast } from '../../../../fields/Types'; +import { Cast, CsvCast, NumCast, StrCast } from '../../../../fields/Types'; import { CsvField } from '../../../../fields/URLField'; import { Docs } from '../../../documents/Documents'; import { ViewBoxAnnotatableComponent } from '../../DocComponent'; @@ -29,7 +29,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { // @observable private pairs: { [key: string]: FieldResult }[] = []; static pairSet = new ObservableMap<string, { [key: string]: string }[]>(); @computed.struct get pairs() { - return DataVizBox.pairSet.get(StrCast(this.rootDoc.fileUpload)); + return DataVizBox.pairSet.get(CsvCast(this.rootDoc[this.fieldKey]).url.href); } private _chartRenderer: LineChart | undefined; // // another way would be store a schema that defines the type of data we are expecting from an imported doc @@ -61,7 +61,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { @action restoreView = (data: Doc) => { const changedView = this.dataVizView !== data.presDataVizView && (this.layoutDoc._dataVizView = data.presDataVizView); - const changedAxes = this.axes.join('') !== StrListCast(data.presDataVizAxes).join('') && (this.layoutDoc._dataVizAxes = new List<string>(StrListCast(data.presDataVizAxes))); + const changedAxes = this.axes.join('') !== StrListCast(data.presDataVizAxes).join('') && (this.layoutDoc._data_vizAxes = new List<string>(StrListCast(data.presDataVizAxes))); const func = () => this._chartRenderer?.restoreView(data); if (changedView || changedAxes) { setTimeout(func, 100); @@ -73,8 +73,7 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { getAnchor = (addAsAnnotation?: boolean, pinProps?: PinProps) => { const anchor = this._chartRenderer?.getAnchor(pinProps) ?? - Docs.Create.TextanchorDocument({ - unrendered: true, + Docs.Create.DataVizConfigDocument({ // when we clear selection -> we should have it so chartBox getAnchor returns undefined // this is for when we want the whole doc (so when the chartBox getAnchor returns without a marker) /*put in some options*/ @@ -88,14 +87,14 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { }; @computed.struct get axes() { - return StrListCast(this.layoutDoc.dataVizAxes); + return StrListCast(this.layoutDoc.data_vizAxes); } - selectAxes = (axes: string[]) => (this.layoutDoc.dataVizAxes = new List<string>(axes)); + selectAxes = (axes: string[]) => (this.layoutDoc.data_vizAxes = new List<string>(axes)); @computed get selectView() { const width = this.props.PanelWidth() * 0.9; const height = (this.props.PanelHeight() - 32) /* height of 'change view' button */ * 0.9; - const margin = { top: 10, right: 25, bottom: 50, left:25}; + const margin = { top: 10, right: 25, bottom: 50, left: 25 }; if (!this.pairs) return 'no data'; // prettier-ignore switch (this.dataVizView) { @@ -113,10 +112,10 @@ export class DataVizBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { } fetchData() { - if (DataVizBox.pairSet.has(StrCast(this.rootDoc.fileUpload))) return; - DataVizBox.pairSet.set(StrCast(this.rootDoc.fileUpload), []); + if (DataVizBox.pairSet.has(CsvCast(this.rootDoc[this.fieldKey]).url.href)) return; + DataVizBox.pairSet.set(CsvCast(this.rootDoc[this.fieldKey]).url.href, []); fetch('/csvData?uri=' + this.dataUrl?.url.href) // - .then(res => res.json().then(action(res => !res.errno && DataVizBox.pairSet.set(StrCast(this.rootDoc.fileUpload), res)))); + .then(res => res.json().then(action(res => !res.errno && DataVizBox.pairSet.set(CsvCast(this.rootDoc[this.fieldKey]).url.href, res)))); } // handle changing the view using a button diff --git a/src/client/views/nodes/DataVizBox/components/LineChart.tsx b/src/client/views/nodes/DataVizBox/components/LineChart.tsx index 777bf2f66..661061d51 100644 --- a/src/client/views/nodes/DataVizBox/components/LineChart.tsx +++ b/src/client/views/nodes/DataVizBox/components/LineChart.tsx @@ -56,8 +56,8 @@ export class LineChart extends React.Component<LineChartProps> { } @computed get incomingLinks() { return LinkManager.Instance.getAllRelatedLinks(this.props.rootDoc) // out of all links - .filter(link => link.anchor1 !== this.props.rootDoc) // get links where this chart doc is the target of the link - .map(link => DocCast(link.anchor1)); // then return the source of the link + .filter(link => link.link_anchor_1 !== this.props.rootDoc) // get links where this chart doc is the target of the link + .map(link => DocCast(link.link_anchor_1)); // then return the source of the link } @computed get incomingSelected() { return this.incomingLinks // all links that are pointing to this node @@ -87,7 +87,7 @@ export class LineChart extends React.Component<LineChartProps> { { fireImmediately: true } ); this._disposers.annos = reaction( - () => DocListCast(this.props.dataDoc[this.props.fieldKey + '-annotations']), + () => DocListCast(this.props.dataDoc[this.props.fieldKey + '_annotations']), annotations => { // modify how d3 renders so that anything in this annotations list would be potentially highlighted in some way // could be blue colored to make it look like anchor @@ -123,7 +123,7 @@ export class LineChart extends React.Component<LineChartProps> { element.classList.remove('selected'); } }; - // gets called whenever the "data-annotations" fields gets updated + // gets called whenever the "data_annotations" fields gets updated drawAnnotations = (dataX: number, dataY: number, selected?: boolean) => { // TODO: nda - can optimize this by having some sort of mapping of the x and y values to the individual circle elements // loop through all html elements with class .circle-d1 and find the one that has "data-x" and "data-y" attributes that match the dataX and dataY @@ -163,7 +163,10 @@ export class LineChart extends React.Component<LineChartProps> { // create a document anchor that stores whatever is needed to reconstruct the viewing state (selection,zoom,etc) getAnchor = (pinProps?: PinProps) => { - const anchor = Docs.Create.TextanchorDocument({ title: 'line doc selection' + this._currSelected?.x, unrendered: true }); + const anchor = Docs.Create.LineChartConfigDocument({ + // + title: 'line doc selection' + this._currSelected?.x, + }); PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: pinProps?.pinData }, this.props.dataDoc); anchor.presDataVizSelection = this._currSelected ? new List<number>([this._currSelected.x, this._currSelected.y]) : undefined; return anchor; @@ -228,15 +231,15 @@ export class LineChart extends React.Component<LineChartProps> { // creating the x and y scales const xScale = scaleCreatorNumerical(xMin, xMax, 0, width); - const yScale = scaleCreatorNumerical(0, yMax,height, 0); + const yScale = scaleCreatorNumerical(0, yMax, height, 0); // adding svg const margin = this.props.margin; const svg = (this._lineChartSvg = d3 .select(this._lineChartRef.current) .append('svg') - .attr('width', `${width +margin.left + margin.right}`) - .attr('height', `${height + margin.top + margin.bottom }`) + .attr('width', `${width + margin.left + margin.right}`) + .attr('height', `${height + margin.top + margin.bottom}`) .append('g') .attr('transform', `translate(${margin.left}, ${margin.top})`)); diff --git a/src/client/views/nodes/DataVizBox/components/TableBox.tsx b/src/client/views/nodes/DataVizBox/components/TableBox.tsx index 0d69ac890..d84e34d52 100644 --- a/src/client/views/nodes/DataVizBox/components/TableBox.tsx +++ b/src/client/views/nodes/DataVizBox/components/TableBox.tsx @@ -1,7 +1,7 @@ import { action, computed } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { AnimationSym, Doc } from '../../../../../fields/Doc'; +import { Doc } from '../../../../../fields/Doc'; import { Id } from '../../../../../fields/FieldSymbols'; import { List } from '../../../../../fields/List'; import { emptyFunction, returnFalse, setupMoveUpEvents, Utils } from '../../../../../Utils'; @@ -47,17 +47,17 @@ export class TableBox extends React.Component<TableBoxProps> { e => { const sourceAnchorCreator = () => this.props.docView?.()!.rootDoc!; const targetCreator = (annotationOn: Doc | undefined) => { - const alias = Doc.MakeAlias(this.props.docView?.()!.rootDoc!); - alias._dataVizView = DataVizView.LINECHART; - alias._dataVizAxes = new List<string>([col, col]); - alias.annotationOn = annotationOn; //this.props.docView?.()!.rootDoc!; - return alias; + const embedding = Doc.MakeEmbedding(this.props.docView?.()!.rootDoc!); + embedding._dataVizView = DataVizView.LINECHART; + embedding._data_vizAxes = new List<string>([col, col]); + embedding.annotationOn = annotationOn; //this.props.docView?.()!.rootDoc!; + return embedding; }; if (this.props.docView?.() && !Utils.isClick(e.clientX, e.clientY, downX, downY, Date.now())) { DragManager.StartAnchorAnnoDrag([header.current!], new DragManager.AnchorAnnoDragData(this.props.docView()!, sourceAnchorCreator, targetCreator), downX, downY, { dragComplete: e => { if (!e.aborted && e.annoDragData && e.annoDragData.linkSourceDoc && e.annoDragData.dropDocument && e.linkDocument) { - e.linkDocument.linkDisplay = true; + e.linkDocument.link_displayLine = true; // e.annoDragData.linkSourceDoc.followLinkToggle = e.annoDragData.dropDocument.annotationOn === this.props.rootDoc; // e.annoDragData.linkSourceDoc.followLinkZoom = false; } diff --git a/src/client/views/nodes/DocumentContentsView.tsx b/src/client/views/nodes/DocumentContentsView.tsx index 76a5ce7b3..e954d0484 100644 --- a/src/client/views/nodes/DocumentContentsView.tsx +++ b/src/client/views/nodes/DocumentContentsView.tsx @@ -1,6 +1,7 @@ import { computed } from 'mobx'; import { observer } from 'mobx-react'; -import { AclPrivate, Doc, Opt } from '../../../fields/Doc'; +import { Doc, Opt } from '../../../fields/Doc'; +import { AclPrivate } from '../../../fields/DocSymbols'; import { ScriptField } from '../../../fields/ScriptField'; import { Cast, StrCast } from '../../../fields/Types'; import { GetEffectiveAcl, TraceMobx } from '../../../fields/util'; @@ -35,6 +36,7 @@ import { LinkBox } from './LinkBox'; import { LoadingBox } from './LoadingBox'; import { MapBox } from './MapBox/MapBox'; import { PDFBox } from './PDFBox'; +import { PhysicsSimulationBox } from './PhysicsBox/PhysicsSimulationBox'; import { RecordingBox } from './RecordingBox'; import { ScreenshotBox } from './ScreenshotBox'; import { ScriptingBox } from './ScriptingBox'; @@ -120,15 +122,15 @@ export class DocumentContentsView extends React.Component< select: (ctrl: boolean) => void; NativeDimScaling?: () => number; setHeight?: (height: number) => void; - layoutKey: string; + layout_fieldKey: string; } > { @computed get layout(): string { TraceMobx(); if (this.props.LayoutTemplateString) return this.props.LayoutTemplateString; if (!this.layoutDoc) return '<p>awaiting layout</p>'; - if (this.props.layoutKey === 'layout_keyValue') return StrCast(this.props.Document.layout_keyValue, KeyValueBox.LayoutString()); - const layout = Cast(this.layoutDoc[this.layoutDoc === this.props.Document && this.props.layoutKey ? this.props.layoutKey : StrCast(this.layoutDoc.layoutKey, 'layout')], 'string'); + if (this.props.layout_fieldKey === 'layout_keyValue') return StrCast(this.props.Document.layout_keyValue, KeyValueBox.LayoutString()); + const layout = Cast(this.layoutDoc[this.layoutDoc === this.props.Document && this.props.layout_fieldKey ? this.props.layout_fieldKey : StrCast(this.layoutDoc.layout_fieldKey, 'layout')], 'string'); if (layout === undefined) return this.props.Document.data ? "<FieldView {...props} fieldKey='data' />" : KeyValueBox.LayoutString(); if (typeof layout === 'string') return layout; return '<p>Loading layout</p>'; @@ -140,12 +142,12 @@ export class DocumentContentsView extends React.Component< } get layoutDoc() { // bcz: replaced this with below : is it correct? change was made to accommodate passing fieldKey's from a layout script - // const template: Doc = this.props.LayoutTemplate?.() || Doc.Layout(this.props.Document, this.props.layoutKey ? Cast(this.props.Document[this.props.layoutKey], Doc, null) : undefined); + // const template: Doc = this.props.LayoutTemplate?.() || Doc.Layout(this.props.Document, this.props.layout_fieldKey ? Cast(this.props.Document[this.props.layout_fieldKey], Doc, null) : undefined); const template: Doc = this.props.LayoutTemplate?.() || (this.props.LayoutTemplateString && this.props.Document) || - (this.props.layoutKey && StrCast(this.props.Document[this.props.layoutKey]) && this.props.Document) || - Doc.Layout(this.props.Document, this.props.layoutKey ? Cast(this.props.Document[this.props.layoutKey], Doc, null) : undefined); + (this.props.layout_fieldKey && StrCast(this.props.Document[this.props.layout_fieldKey]) && this.props.Document) || + Doc.Layout(this.props.Document, this.props.layout_fieldKey ? Cast(this.props.Document[this.props.layout_fieldKey], Doc, null) : undefined); return Doc.expandTemplateLayout(template, this.props.Document); } @@ -265,6 +267,7 @@ export class DocumentContentsView extends React.Component< HTMLtag, ComparisonBox, LoadingBox, + PhysicsSimulationBox, SchemaRowBox, }} bindings={bindings} diff --git a/src/client/views/nodes/DocumentIcon.tsx b/src/client/views/nodes/DocumentIcon.tsx index 56de2d1fc..6e2ed72b8 100644 --- a/src/client/views/nodes/DocumentIcon.tsx +++ b/src/client/views/nodes/DocumentIcon.tsx @@ -1,23 +1,39 @@ - -import { observer } from "mobx-react"; -import * as React from "react"; -import { DocumentView } from "./DocumentView"; -import { DocumentManager } from "../../util/DocumentManager"; -import { Transformer, ts } from "../../util/Scripting"; -import { Field } from "../../../fields/Doc"; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { DocumentView } from './DocumentView'; +import { DocumentManager } from '../../util/DocumentManager'; +import { Transformer, ts } from '../../util/Scripting'; +import { Field } from '../../../fields/Doc'; +import { Tooltip } from '@material-ui/core'; +import { action, observable } from 'mobx'; +import { Id } from '../../../fields/FieldSymbols'; +import { factory } from 'typescript'; +import { LightboxView } from '../LightboxView'; @observer -export class DocumentIcon extends React.Component<{ view: DocumentView, index: number }> { +export class DocumentIcon extends React.Component<{ view: DocumentView; index: number }> { + @observable _hovered = false; + static get DocViews() { + return LightboxView.LightboxDoc ? DocumentManager.Instance.DocumentViews.filter(v => LightboxView.IsLightboxDocView(v.props.docViewPath())) : DocumentManager.Instance.DocumentViews; + } render() { const view = this.props.view; const { left, top, right, bottom } = view.getBounds() || { left: 0, top: 0, right: 0, bottom: 0 }; return ( - <div className="documentIcon-outerDiv" style={{ - position: "absolute", - transform: `translate(${(left + right) / 2}px, ${top}px)`, - }}> - <p>d{this.props.index}</p> + <div + className="documentIcon-outerDiv" + onPointerEnter={action(e => (this._hovered = true))} + onPointerLeave={action(e => (this._hovered = false))} + style={{ + pointerEvents: 'all', + opacity: this._hovered ? 0.3 : 1, + position: 'absolute', + transform: `translate(${(left + right) / 2}px, ${top}px)`, + }}> + <Tooltip title={<>{this.props.view.rootDoc.title}</>}> + <p>d{this.props.index}</p> + </Tooltip> </div> ); } @@ -41,7 +57,9 @@ export class DocumentIconContainer extends React.Component { const match = node.text.match(/d([0-9]+)/); if (match) { const m = parseInt(match[1]); + const doc = DocumentIcon.DocViews[m].rootDoc; usedDocuments.add(m); + return factory.createIdentifier(`idToDoc("${doc[Id]}")`); } } } @@ -52,14 +70,14 @@ export class DocumentIconContainer extends React.Component { }; }, getVars() { - const docs = Array.from(DocumentManager.Instance.DocumentViews); + const docs = DocumentIcon.DocViews; const capturedVariables: { [name: string]: Field } = {}; - usedDocuments.forEach(index => capturedVariables[`d${index}`] = docs[index].props.Document); - return { capturedVariables }; - } + usedDocuments.forEach(index => (capturedVariables[`d${index}`] = docs.length > index ? docs[index].props.Document : `d${index}`)); + return capturedVariables; + }, }; } render() { - return Array.from(DocumentManager.Instance.DocumentViews).map((dv, i) => <DocumentIcon key={i} index={i} view={dv} />); + return DocumentIcon.DocViews.map((dv, i) => <DocumentIcon key={i} index={i} view={dv} />); } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/DocumentLinksButton.tsx b/src/client/views/nodes/DocumentLinksButton.tsx index 47705d53d..7723a088d 100644 --- a/src/client/views/nodes/DocumentLinksButton.tsx +++ b/src/client/views/nodes/DocumentLinksButton.tsx @@ -9,7 +9,7 @@ import { DocUtils } from '../../documents/Documents'; import { DragManager } from '../../util/DragManager'; import { Hypothesis } from '../../util/HypothesisUtils'; import { LinkManager } from '../../util/LinkManager'; -import { undoBatch, UndoManager } from '../../util/UndoManager'; +import { undoable, undoBatch, UndoManager } from '../../util/UndoManager'; import './DocumentLinksButton.scss'; import { DocumentView } from './DocumentView'; import { LinkDescriptionPopup } from './LinkDescriptionPopup'; @@ -52,7 +52,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp dragComplete: dropEv => { if (this.props.View && dropEv.linkDocument) { // dropEv.linkDocument equivalent to !dropEve.aborted since linkDocument is only assigned on a completed drop - !dropEv.linkDocument.linkRelationship && (Doc.GetProto(dropEv.linkDocument).linkRelationship = 'hyperlink'); + !dropEv.linkDocument.link_relationship && (Doc.GetProto(dropEv.linkDocument).link_relationship = 'hyperlink'); } linkDrag?.end(); }, @@ -71,14 +71,15 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp e, this.onLinkButtonMoved, emptyFunction, - action((e, doubleTap) => doubleTap && DocumentView.showBackLinks(this.props.View.rootDoc)), + action((e, doubleTap) => { + doubleTap && DocumentView.showBackLinks(this.props.View.rootDoc); + }), undefined, undefined, action(() => (DocumentLinksButton.LinkEditorDocView = this.props.View)) ); }; - @undoBatch onLinkButtonDown = (e: React.PointerEvent): void => { setupMoveUpEvents( this, @@ -123,17 +124,14 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp e, returnFalse, emptyFunction, - undoBatch( - action(e => { - DocumentLinksButton.finishLinkClick(e.clientX, e.clientY, DocumentLinksButton.StartLink, this.props.View.props.Document, true, this.props.View); - }) - ) + action(e => DocumentLinksButton.finishLinkClick(e.clientX, e.clientY, DocumentLinksButton.StartLink, this.props.View.props.Document, true, this.props.View)) ); }; - public static finishLinkClick = undoBatch( - action((screenX: number, screenY: number, startLink: Doc, endLink: Doc, startIsAnnotation: boolean, endLinkView?: DocumentView, pinProps?: PinProps) => { - if (startLink === endLink) { + @undoBatch + public static finishLinkClick(screenX: number, screenY: number, startLink: Doc | undefined, endLink: Doc, startIsAnnotation: boolean, endLinkView?: DocumentView, pinProps?: PinProps) { + runInAction(() => { + if (startLink === endLink || !startLink) { DocumentLinksButton.StartLink = undefined; DocumentLinksButton.StartLinkView = undefined; DocumentLinksButton.AnnotationId = undefined; @@ -142,7 +140,7 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp } else if (startLink !== endLink) { endLink = endLinkView?.docView?._componentView?.getAnchor?.(true, pinProps) || endLink; startLink = DocumentLinksButton.StartLinkView?.docView?._componentView?.getAnchor?.(true) || startLink; - const linkDoc = DocUtils.MakeLink(startLink, endLink, { linkRelationship: DocumentLinksButton.AnnotationId ? 'hypothes.is annotation' : undefined }); + const linkDoc = DocUtils.MakeLink(startLink, endLink, { link_relationship: DocumentLinksButton.AnnotationId ? 'hypothes.is annotation' : undefined }); LinkManager.currentLink = linkDoc; @@ -185,8 +183,8 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp ); } } - }) - ); + }); + } @action clearLinks() { DocumentLinksButton.StartLink = undefined; @@ -195,9 +193,9 @@ export class DocumentLinksButton extends React.Component<DocumentLinksButtonProp @computed get filteredLinks() { const results = [] as Doc[]; - const filters = this.props.View.props.docFilters(); + const filters = this.props.View.props.childFilters(); Array.from(new Set<Doc>(this.props.View.allLinks)).forEach(link => { - if (DocUtils.FilterDocs([link], filters, []).length || DocUtils.FilterDocs([link.anchor2 as Doc], filters, []).length || DocUtils.FilterDocs([link.anchor1 as Doc], filters, []).length) { + if (DocUtils.FilterDocs([link], filters, []).length || DocUtils.FilterDocs([link.link_anchor_2 as Doc], filters, []).length || DocUtils.FilterDocs([link.link_anchor_1 as Doc], filters, []).length) { results.push(link); } }); diff --git a/src/client/views/nodes/DocumentView.tsx b/src/client/views/nodes/DocumentView.tsx index b61a468bd..a258de632 100644 --- a/src/client/views/nodes/DocumentView.tsx +++ b/src/client/views/nodes/DocumentView.tsx @@ -3,16 +3,18 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction import { observer } from 'mobx-react'; import { computedFn } from 'mobx-utils'; import { Bounce, Fade, Flip, LightSpeed, Roll, Rotate, Zoom } from 'react-reveal'; -import { AclPrivate, AnimationSym, DataSym, Doc, DocListCast, Field, Opt, StrListCast, WidthSym } from '../../../fields/Doc'; +import { Doc, DocListCast, Field, Opt, StrListCast } from '../../../fields/Doc'; +import { AclPrivate, Animation, DocData, Width } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; +import { RefField } from '../../../fields/RefField'; import { listSpec } from '../../../fields/Schema'; import { ScriptField } from '../../../fields/ScriptField'; import { BoolCast, Cast, DocCast, ImageCast, NumCast, ScriptCast, StrCast } from '../../../fields/Types'; import { AudioField } from '../../../fields/URLField'; import { GetEffectiveAcl, TraceMobx } from '../../../fields/util'; -import { emptyFunction, isTargetChildOf as isParentOf, lightOrDark, returnEmptyString, returnFalse, returnTrue, returnVal, simulateMouseClick, Utils } from '../../../Utils'; +import { emptyFunction, isTargetChildOf as isParentOf, lightOrDark, return18, returnEmptyString, returnFalse, returnTrue, returnVal, returnZero, simulateMouseClick, Utils } from '../../../Utils'; import { GooglePhotos } from '../../apis/google_docs/GooglePhotosClientUtils'; import { DocServer } from '../../DocServer'; import { Docs, DocUtils } from '../../documents/Documents'; @@ -35,8 +37,10 @@ import { ContextMenuProps } from '../ContextMenuItem'; import { DocComponent } from '../DocComponent'; import { EditableView } from '../EditableView'; import { GestureOverlay } from '../GestureOverlay'; +import { InkingStroke } from '../InkingStroke'; import { LightboxView } from '../LightboxView'; import { StyleProp } from '../StyleProvider'; +import { UndoStack } from '../UndoStack'; import { CollectionFreeFormDocumentView } from './CollectionFreeFormDocumentView'; import { DocumentContentsView, ObserverJsxParser } from './DocumentContentsView'; import { DocumentLinksButton } from './DocumentLinksButton'; @@ -47,7 +51,6 @@ import { LinkAnchorBox } from './LinkAnchorBox'; import { PresEffect, PresEffectDirection } from './trails'; import { PinProps, PresBox } from './trails/PresBox'; import React = require('react'); -import { InkingStroke } from '../InkingStroke'; const { Howl } = require('howler'); interface Window { @@ -114,6 +117,7 @@ export interface DocComponentView { brushView?: (view: { width: number; height: number; panX: number; panY: number }) => void; getView?: (doc: Doc) => Promise<Opt<DocumentView>>; // returns a nested DocumentView for the specified doc or undefined addDocTab?: (doc: Doc, where: OpenWhere) => boolean; // determines how to add a document - used in following links to open the target ina local lightbox + addDocument?: (doc: Doc | Doc[], annotationKey?: string) => boolean; // add a document (used only by collections) reverseNativeScaling?: () => boolean; // DocumentView's setup screenToLocal based on the doc having a nativeWidth/Height. However, some content views (e.g., FreeFormView w/ fitContentsToBox set) may ignore the native dimensions so this flags the DocumentView to not do Nativre scaling. shrinkWrap?: () => void; // requests a document to display all of its contents with no white space. currently only implemented (needed?) for freeform views select?: (ctrlKey: boolean, shiftKey: boolean) => void; @@ -127,9 +131,10 @@ export interface DocComponentView { IsPlaying?: () => boolean; // is a media document playing TogglePause?: (keep?: boolean) => void; // toggle media document playing state setFocus?: () => void; // sets input focus to the componentView + setData?: (data: Field | Promise<RefField | undefined>) => boolean; componentUI?: (boundsLeft: number, boundsTop: number) => JSX.Element | null; incrementalRendering?: () => void; - fitWidth?: () => boolean; // whether the component always fits width (eg, KeyValueBox) + layout_fitWidth?: () => boolean; // whether the component always fits width (eg, KeyValueBox) overridePointerEvents?: () => 'all' | 'none' | undefined; // if the conmponent overrides the pointer events for the document fieldKey?: string; annotationKey?: string; @@ -148,13 +153,14 @@ export interface DocumentViewSharedProps { Document: Doc; DataDoc?: Doc; contentBounds?: () => undefined | { x: number; y: number; r: number; b: number }; - fitContentsToBox?: () => boolean; // used by freeformview to fit its contents to its panel. corresponds to _fitContentsToBox property on a Document + fitContentsToBox?: () => boolean; // used by freeformview to fit its contents to its panel. corresponds to _freeform_fitContentsToBox property on a Document suppressSetHeight?: boolean; thumbShown?: () => boolean; setContentView?: (view: DocComponentView) => any; CollectionFreeFormDocumentView?: () => CollectionFreeFormDocumentView; PanelWidth: () => number; PanelHeight: () => number; + shouldNotScale?: () => boolean; docViewPath: () => DocumentView[]; childHideDecorationTitle?: () => boolean; childHideResizeHandles?: () => boolean; @@ -162,11 +168,11 @@ export interface DocumentViewSharedProps { styleProvider: Opt<StyleProviderFunc>; setTitleFocus?: () => void; focus: DocFocusFunc; - fitWidth?: (doc: Doc) => boolean | undefined; - docFilters: () => string[]; - docRangeFilters: () => string[]; + layout_fitWidth?: (doc: Doc) => boolean | undefined; + childFilters: () => string[]; + childFiltersByRanges: () => string[]; searchFilterDocs: () => Doc[]; - showTitle?: () => string; + layout_showTitle?: () => string; whenChildContentsActiveChanged: (isActive: boolean) => void; rootSelected: (outsideReaction?: boolean) => boolean; // whether the root of a template has been selected addDocTab: (doc: Doc, where: OpenWhere) => boolean; @@ -187,7 +193,7 @@ export interface DocumentViewSharedProps { hideCaptions?: boolean; ignoreAutoHeight?: boolean; forceAutoHeight?: boolean; - disableDocBrushing?: boolean; // should highlighting for this view be disabled when same document in another view is hovered over. + disableBrushing?: boolean; // should highlighting for this view be disabled when same document in another view is hovered over. onClickScriptDisable?: 'never' | 'always'; // undefined = only when selected enableDragWhenActive?: boolean; waitForDoubleClickToClick?: () => 'never' | 'always' | undefined; @@ -273,7 +279,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps public get LayoutFieldKey() { return Doc.LayoutFieldKey(this.layoutDoc); } - @computed get ShowTitle() { + @computed get layout_showTitle() { return this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.ShowTitle) as Opt<string>; } @computed get NativeDimScaling() { @@ -303,7 +309,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @computed get headerMargin() { return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin) || 0; } - @computed get showCaption() { + @computed get layout_showCaption() { return this.props?.styleProvider?.(this.layoutDoc, this.props, StyleProp.ShowCaption) || 0; } @computed get titleHeight() { @@ -313,7 +319,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps return this.props.styleProvider?.(this.Document, this.props, StyleProp.PointerEvents + (this.props.isSelected() ? ':selected' : '')); } @computed get finalLayoutKey() { - return StrCast(this.Document.layoutKey, 'layout'); + return StrCast(this.Document.layout_fieldKey, 'layout'); } @computed get nativeWidth() { return this.props.NativeWidth(); @@ -400,7 +406,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps defaultRestoreTargetView = (docView: DocumentView, anchor: Doc, focusSpeed: number, options: DocFocusOptions) => { const targetMatch = Doc.AreProtosEqual(anchor, this.rootDoc) || // anchor is this document, so anchor's properties apply to this document - (DocCast(anchor)?.unrendered && Doc.AreProtosEqual(DocCast(anchor.annotationOn), this.rootDoc)) // the anchor is an unrendered annotation on this document, so anchor properties apply to this document + (DocCast(anchor)?.layout_unrendered && Doc.AreProtosEqual(DocCast(anchor.annotationOn), this.rootDoc)) // the anchor is an layout_unrendered annotation on this document, so anchor properties apply to this document ? true : false; return targetMatch && PresBox.restoreTargetDocView(docView, anchor, focusSpeed) ? focusSpeed : undefined; @@ -408,7 +414,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps // switches text input focus to the title bar of the document (and displays the title bar if it hadn't been) setTitleFocus = () => { - if (!StrCast(this.layoutDoc._showTitle)) this.layoutDoc._showTitle = 'title'; + if (!StrCast(this.layoutDoc._layout_showTitle)) this.layoutDoc._layout_showTitle = 'title'; setTimeout(() => this._titleRef.current?.setIsFocused(true)); // use timeout in case title wasn't shown to allow re-render so that titleref will be defined }; @@ -434,7 +440,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps }, console.log ); UndoManager.RunInBatch(() => (func().result?.select === true ? this.props.select(false) : ''), 'on double click'); } else if (!Doc.IsSystem(this.rootDoc) && (defaultDblclick === undefined || defaultDblclick === 'default')) { - UndoManager.RunInBatch(() => this.props.addDocTab(this.rootDoc, OpenWhere.lightbox), 'double tap'); + UndoManager.RunInBatch(() => LightboxView.AddDocTab(this.rootDoc, OpenWhere.lightbox), 'double tap'); SelectionManager.DeselectAll(); Doc.UnBrushDoc(this.props.Document); } else { @@ -454,27 +460,26 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps // instead of in the global lightbox const oldFunc = DocumentViewInternal.addDocTabFunc; DocumentViewInternal.addDocTabFunc = this.props.addDocTab; - const res = - this.onClickHandler?.script.run( - { - this: this.layoutDoc, - self: this.rootDoc, - _readOnly_: false, - scriptContext: this.props.scriptContext, - documentView: this.props.DocumentView(), - clientX, - clientY, - shiftKey, - altKey, - metaKey, - }, - console.log - ).result?.select === true - ? this.props.select(false) - : ''; + this.onClickHandler?.script.run( + { + this: this.layoutDoc, + self: this.rootDoc, + _readOnly_: false, + scriptContext: this.props.scriptContext, + documentView: this.props.DocumentView(), + clientX, + clientY, + shiftKey, + altKey, + metaKey, + }, + console.log + ).result?.select === true + ? this.props.select(false) + : ''; DocumentViewInternal.addDocTabFunc = oldFunc; }; - clickFunc = () => (this.props.Document.dontUndo ? func() : UndoManager.RunInBatch(func, 'on click')); + clickFunc = () => (this.props.Document.dontUndo ? func() : UndoManager.RunInBatch(func, 'click ' + this.rootDoc.title)); } else { // onDragStart implies a button doc that we don't want to select when clicking. RootDocument & isTemplateForField implies we're clicking on part of a template instance and we want to select the whole template, not the part if ((this.layoutDoc.onDragStart || this.props.Document.rootDocument) && !(e.ctrlKey || e.button > 0)) { @@ -488,7 +493,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if ((clickFunc && waitFordblclick !== 'never') || waitFordblclick === 'always') { this._doubleClickTimeout && clearTimeout(this._doubleClickTimeout); this._doubleClickTimeout = setTimeout(this._singleClickFunc, 300); - } else { + } else if (!DocumentView.LongPress) { this._singleClickFunc(); this._singleClickFunc = undefined; } @@ -500,7 +505,15 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @action onPointerDown = (e: React.PointerEvent): void => { - this._longPressSelector = setTimeout(() => DocumentView.LongPress && this.props.select(false), 1000); + this._longPressSelector = setTimeout(() => { + if (DocumentView.LongPress) { + if (this.rootDoc.dontUndo) { + runInAction(() => (UndoStack.HideInline = !UndoStack.HideInline)); + } else { + this.props.select(false); + } + } + }, 1000); if (!GestureOverlay.DownDocView) GestureOverlay.DownDocView = this.props.DocumentView(); this._downX = e.clientX; @@ -516,11 +529,11 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps !this.Document.ignoreClick && e.button === 0 && this.pointerEvents !== 'none' && - !DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc) + !Doc.IsInMyOverlay(this.layoutDoc) ) { e.stopPropagation(); // don't preventDefault anymore. Goldenlayout, PDF text selection and RTF text selection all need it to go though - //if (this.props.isSelected(true) && this.rootDoc.type !== DocumentType.PDF && this.layoutDoc._viewType !== CollectionViewType.Docking) e.preventDefault(); + //if (this.props.isSelected(true) && this.rootDoc.type !== DocumentType.PDF && this.layoutDoc._type_collection !== CollectionViewType.Docking) e.preventDefault(); // listen to move events if document content isn't active or document is draggable if (!this.layoutDoc._lockedPosition && (!this.isContentActive() || this.props.enableDragWhenActive || this.rootDoc.enableDragWhenActive)) { @@ -537,7 +550,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if (!Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, Date.now())) { this.cleanupPointerEvents(); - this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && 'alias') || ((this.Document.dropAction || this.props.dropAction || undefined) as dropActionType)); + this.startDragging(this._downX, this._downY, ((e.ctrlKey || e.altKey) && 'embed') || ((this.Document.dropAction || this.props.dropAction || undefined) as dropActionType)); } }; @@ -555,8 +568,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if (this.onPointerUpHandler?.script) { this.onPointerUpHandler.script.run({ self: this.rootDoc, this: this.layoutDoc }, console.log); } else if (e.button === 0 && Utils.isClick(e.clientX, e.clientY, this._downX, this._downY, this._downTime)) { - this._doubleTap = Date.now() - this._lastTap < Utils.CLICK_TIME; - if (!this.props.isSelected(true)) this._lastTap = Date.now(); // don't want to process the start of a double tap if the doucment is selected + this._doubleTap = (this.onDoubleClickHandler?.script || this.rootDoc.defaultDoubleClick !== 'ignore') && Date.now() - this._lastTap < Utils.CLICK_TIME; + if (!this.isContentActive()) this._lastTap = Date.now(); // don't want to process the start of a double tap if the doucment is selected } if (DocumentView.LongPress) e.preventDefault(); }; @@ -587,7 +600,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @undoBatch deleteClicked = () => this.props.removeDocument?.(this.props.Document); @undoBatch setToggleDetail = () => (this.Document.onClick = ScriptField.MakeScript( - `toggleDetail(documentView, "${StrCast(this.Document.layoutKey) + `toggleDetail(documentView, "${StrCast(this.Document.layout_fieldKey) .replace('layout_', '') .replace(/^layout$/, 'detail')}")`, { documentView: 'any' } @@ -609,7 +622,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if (de.complete.annoDragData && !de.complete.annoDragData.dropDocument) { de.complete.annoDragData.dropDocument = de.complete.annoDragData.dropDocCreator(undefined); } - if (de.complete.annoDragData || this.rootDoc !== linkdrag.linkSourceDoc.context) { + if (de.complete.annoDragData || this.rootDoc !== linkdrag.linkSourceDoc.embedContainer) { const dropDoc = de.complete.annoDragData?.dropDocument ?? this._componentView?.getAnchor?.(true) ?? this.rootDoc; de.complete.linkDocument = DocUtils.MakeLink(linkdrag.linkSourceDoc, dropDoc, {}, undefined, [de.x, de.y - 50]); } @@ -620,12 +633,12 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @undoBatch @action makeIntoPortal = () => { - const portalLink = this.allLinks.find(d => d.anchor1 === this.props.Document && d.linkRelationship === 'portal to:portal from'); + const portalLink = this.allLinks.find(d => d.link_anchor_1 === this.props.Document && d.link_relationship === 'portal to:portal from'); if (!portalLink) { DocUtils.MakeLink( this.props.Document, - Docs.Create.FreeformDocument([], { _width: NumCast(this.layoutDoc._width) + 10, _height: NumCast(this.layoutDoc._height), _isLightbox: true, _fitWidth: true, title: StrCast(this.props.Document.title) + ' [Portal]' }), - { linkRelationship: 'portal to:portal from' } + Docs.Create.FreeformDocument([], { _width: NumCast(this.layoutDoc._width) + 10, _height: NumCast(this.layoutDoc._height), _isLightbox: true, _layout_fitWidth: true, title: StrCast(this.props.Document.title) + ' [Portal]' }), + { link_relationship: 'portal to:portal from' } ); } this.Document.followLinkLocation = OpenWhere.lightbox; @@ -652,7 +665,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @action onContextMenu = (e?: React.MouseEvent, pageX?: number, pageY?: number) => { - if (e && this.rootDoc._hideContextMenu && Doc.noviceMode) { + if (e && this.rootDoc._layout_hideContextMenu && Doc.noviceMode) { e.preventDefault(); e.stopPropagation(); //!this.props.isSelected(true) && SelectionManager.SelectView(this.props.DocumentView(), false); @@ -697,13 +710,13 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps .forEach(item => item.label && cm.addItem({ description: item.label, event: () => item.script.script.run({ this: this.layoutDoc, scriptContext: this.props.scriptContext, self: this.rootDoc }), icon: item.icon as IconProp })); if (!this.props.Document.isFolder) { - const templateDoc = Cast(this.props.Document[StrCast(this.props.Document.layoutKey)], Doc, null); + const templateDoc = Cast(this.props.Document[StrCast(this.props.Document.layout_fieldKey)], Doc, null); const appearance = cm.findByDescription('UI Controls...'); const appearanceItems: ContextMenuProps[] = appearance && 'subitems' in appearance ? appearance.subitems : []; !Doc.noviceMode && templateDoc && appearanceItems.push({ description: 'Open Template ', event: () => this.props.addDocTab(templateDoc, OpenWhere.addRight), icon: 'eye' }); !appearance && appearanceItems.length && cm.addItem({ description: 'UI Controls...', subitems: appearanceItems, icon: 'compass' }); - if (!Doc.IsSystem(this.rootDoc) && this.rootDoc.type !== DocumentType.PRES && ![CollectionViewType.Docking, CollectionViewType.Tree].includes(this.rootDoc._viewType as any)) { + if (!Doc.IsSystem(this.rootDoc) && this.rootDoc.type !== DocumentType.PRES && ![CollectionViewType.Docking, CollectionViewType.Tree].includes(this.rootDoc._type_collection as any)) { const existingOnClick = cm.findByDescription('OnClick...'); const onClicks: ContextMenuProps[] = existingOnClick && 'subitems' in existingOnClick ? existingOnClick.subitems : []; @@ -723,24 +736,26 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps onClicks.push({ description: 'Enter Portal', event: this.makeIntoPortal, icon: 'window-restore' }); !Doc.noviceMode && onClicks.push({ description: 'Toggle Detail', event: this.setToggleDetail, icon: 'concierge-bell' }); - if (!this.Document.annotationOn) { - const options = cm.findByDescription('Options...'); - const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : []; - !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'compass' }); - - onClicks.push({ description: this.onClickHandler ? 'Remove Click Behavior' : 'Follow Link', event: () => this.toggleFollowLink(false, false), icon: 'link' }); - onClicks.push({ description: 'Edit onClick Script', event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, 'onClick'), 'edit onClick'), icon: 'terminal' }); - !existingOnClick && cm.addItem({ description: 'OnClick...', noexpand: true, subitems: onClicks, icon: 'mouse-pointer' }); - } else if (LinkManager.Links(this.Document).length) { - onClicks.push({ description: 'Select on Click', event: () => this.noOnClick(), icon: 'link' }); - onClicks.push({ description: 'Follow Link on Click', event: () => this.followLinkOnClick(), icon: 'link' }); - !existingOnClick && cm.addItem({ description: 'OnClick...', subitems: onClicks, icon: 'mouse-pointer' }); + if (!this.props.treeViewDoc) { + if (!this.Document.annotationOn) { + const options = cm.findByDescription('Options...'); + const optionItems: ContextMenuProps[] = options && 'subitems' in options ? options.subitems : []; + !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'compass' }); + + onClicks.push({ description: this.onClickHandler ? 'Remove Click Behavior' : 'Follow Link', event: () => this.toggleFollowLink(false, false), icon: 'link' }); + onClicks.push({ description: 'Edit onClick Script', event: () => UndoManager.RunInBatch(() => DocUtils.makeCustomViewClicked(this.props.Document, undefined, 'onClick'), 'edit onClick'), icon: 'terminal' }); + !existingOnClick && cm.addItem({ description: 'OnClick...', noexpand: true, subitems: onClicks, icon: 'mouse-pointer' }); + } else if (LinkManager.Links(this.Document).length) { + onClicks.push({ description: 'Select on Click', event: () => this.noOnClick(), icon: 'link' }); + onClicks.push({ description: 'Follow Link on Click', event: () => this.followLinkOnClick(), icon: 'link' }); + !existingOnClick && cm.addItem({ description: 'OnClick...', subitems: onClicks, icon: 'mouse-pointer' }); + } } } const funcs: ContextMenuProps[] = []; if (!Doc.noviceMode && this.layoutDoc.onDragStart) { - funcs.push({ description: 'Drag an Alias', icon: 'edit', event: () => this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getAlias(this.dragFactory)')) }); + funcs.push({ description: 'Drag an Embedding', icon: 'edit', event: () => this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getEmbedding(this.dragFactory)')) }); funcs.push({ description: 'Drag a Copy', icon: 'edit', event: () => this.Document.dragFactory && (this.layoutDoc.onDragStart = ScriptField.MakeFunction('getCopy(this.dragFactory, true)')) }); funcs.push({ description: 'Drag Document', icon: 'edit', event: () => (this.layoutDoc.onDragStart = undefined) }); cm.addItem({ description: 'OnDrag...', noexpand: true, subitems: funcs, icon: 'asterisk' }); @@ -769,7 +784,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps if (!Doc.IsSystem(this.rootDoc)) { constantItems.push({ description: 'Export as Zip file', icon: 'download', event: async () => Doc.Zip(this.props.Document) }); constantItems.push({ description: 'Import Zipped file', icon: 'upload', event: ({ x, y }) => this.importDocument() }); - (this.rootDoc._viewType !== CollectionViewType.Docking || !Doc.noviceMode) && constantItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: 'users' }); + (this.rootDoc._type_collection !== CollectionViewType.Docking || !Doc.noviceMode) && constantItems.push({ description: 'Share', event: () => SharingManager.Instance.open(this.props.DocumentView()), icon: 'users' }); if (this.props.removeDocument && Doc.ActiveDashboard !== this.props.Document) { // need option to gray out menu items ... preferably with a '?' that explains why they're grayed out (eg., no permissions) constantItems.push({ description: 'Close', event: this.deleteClicked, icon: 'times' }); @@ -781,7 +796,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps const helpItems: ContextMenuProps[] = help && 'subitems' in help ? help.subitems : []; !Doc.noviceMode && helpItems.push({ description: 'Text Shortcuts Ctrl+/', event: () => this.props.addDocTab(Docs.Create.PdfDocument('/assets/cheat-sheet.pdf', { _width: 300, _height: 300 }), OpenWhere.addRight), icon: 'keyboard' }); !Doc.noviceMode && helpItems.push({ description: 'Print Document in Console', event: () => console.log(this.props.Document), icon: 'hand-point-right' }); - !Doc.noviceMode && helpItems.push({ description: 'Print DataDoc in Console', event: () => console.log(this.props.Document[DataSym]), icon: 'hand-point-right' }); + !Doc.noviceMode && helpItems.push({ description: 'Print DataDoc in Console', event: () => console.log(this.props.Document[DocData]), icon: 'hand-point-right' }); let documentationDescription: string | undefined = undefined; let documentationLink: string | undefined = undefined; @@ -816,7 +831,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps break; } // Add link to help documentation - if (documentationDescription && documentationLink) { + if (!this.props.treeViewDoc && documentationDescription && documentationLink) { helpItems.push({ description: documentationDescription, event: () => window.open(documentationLink, '_blank'), @@ -861,12 +876,12 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps LightboxView.LightboxDoc !== this.rootDoc && this.thumb && !Doc.AreProtosEqual(DocumentLinksButton.StartLink, this.rootDoc) && - ((!childHighlighted() && !childOverlayed() && !Doc.isBrushedHighlightedDegree(this.rootDoc)) || this.rootDoc._viewType === CollectionViewType.Docking) && + ((!childHighlighted() && !childOverlayed() && !Doc.isBrushedHighlightedDegree(this.rootDoc)) || this.rootDoc._type_collection === CollectionViewType.Docking) && !this._componentView?.isAnyChildContentActive?.() ? true : false; }; - docFilters = () => [...this.props.docFilters(), ...StrListCast(this.layoutDoc.docFilters)]; + childFilters = () => [...this.props.childFilters(), ...StrListCast(this.layoutDoc.childFilters)]; contentPointerEvents = () => (!this.disableClickScriptFunc && this.onClickHandler ? 'none' : this.pointerEvents); @computed get contents() { TraceMobx(); @@ -898,7 +913,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps docViewPath={this.props.viewPath} thumbShown={this.thumbShown} setContentView={this.setContentView} - docFilters={this.docFilters} + childFilters={this.childFilters} NativeDimScaling={this.props.NativeDimScaling} PanelHeight={this.panelHeight} setHeight={!this.props.suppressSetHeight ? this.setHeight : undefined} @@ -908,9 +923,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps onClick={this.onClickFunc} focus={this.props.focus} setTitleFocus={this.setTitleFocus} - layoutKey={this.finalLayoutKey} + layout_fieldKey={this.finalLayoutKey} /> - {this.layoutDoc.hideAllLinks ? null : this.allLinkEndpoints} + {this.layoutDoc.layout_hideAllLinks ? null : this.allLinkEndpoints} </div> ); } @@ -927,7 +942,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps return this.props.styleProvider?.(doc, props, property); }; // We need to use allrelatedLinks to get not just links to the document as a whole, but links to - // anchors that are not rendered as DocumentViews (marked as 'unrendered' with their 'annotationOn' set to this document). e.g., + // anchors that are not rendered as DocumentViews (marked as 'layout_unrendered' with their 'annotationOn' set to this document). e.g., // - PDF text regions are rendered as an Annotations without generating a DocumentView, ' // - RTF selections are rendered via Prosemirror and have a mark which contains the Document ID for the annotation link // - and links to PDF/Web docs at a certain scroll location never create an explicit view. @@ -936,22 +951,22 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps TraceMobx(); return LinkManager.Instance.getAllRelatedLinks(this.rootDoc).filter( link => - Doc.AreProtosEqual(link.anchor1 as Doc, this.rootDoc) || - Doc.AreProtosEqual(link.anchor2 as Doc, this.rootDoc) || - ((link.anchor1 as Doc)?.unrendered && Doc.AreProtosEqual((link.anchor1 as Doc)?.annotationOn as Doc, this.rootDoc)) || - ((link.anchor2 as Doc)?.unrendered && Doc.AreProtosEqual((link.anchor2 as Doc)?.annotationOn as Doc, this.rootDoc)) + Doc.AreProtosEqual(link.link_anchor_1 as Doc, this.rootDoc) || + Doc.AreProtosEqual(link.link_anchor_2 as Doc, this.rootDoc) || + ((link.link_anchor_1 as Doc)?.layout_unrendered && Doc.AreProtosEqual((link.link_anchor_1 as Doc)?.annotationOn as Doc, this.rootDoc)) || + ((link.link_anchor_2 as Doc)?.layout_unrendered && Doc.AreProtosEqual((link.link_anchor_2 as Doc)?.annotationOn as Doc, this.rootDoc)) ); } @computed get allLinks() { TraceMobx(); return LinkManager.Instance.getAllRelatedLinks(this.rootDoc); } - hideLink = computedFn((link: Doc) => () => (link.linkDisplay = false)); + hideLink = computedFn((link: Doc) => () => (link.link_displayLine = false)); @computed get allLinkEndpoints() { // the small blue dots that mark the endpoints of links TraceMobx(); - if (this.props.hideLinkAnchors || this.layoutDoc.hideLinkAnchors || this.props.dontRegisterView || this.layoutDoc.unrendered) return null; - const filtered = DocUtils.FilterDocs(this.directLinks, this.props.docFilters?.() ?? [], []).filter(d => d.linkDisplay); + if (this.props.hideLinkAnchors || this.layoutDoc.layout_hideLinkAnchors || this.props.dontRegisterView || this.layoutDoc.layout_unrendered) return null; + const filtered = DocUtils.FilterDocs(this.directLinks, this.props.childFilters?.() ?? [], []).filter(d => d.link_displayLine); return filtered.map(link => ( <div className="documentView-anchorCont" key={link[Id]}> <DocumentView @@ -962,14 +977,14 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps PanelWidth={this.anchorPanelWidth} PanelHeight={this.anchorPanelHeight} dontRegisterView={false} - showTitle={returnEmptyString} + layout_showTitle={returnEmptyString} hideCaptions={true} hideLinkAnchors={true} - fitWidth={returnTrue} + layout_fitWidth={returnTrue} removeDocument={this.hideLink(link)} styleProvider={this.anchorStyleProvider} LayoutTemplate={undefined} - LayoutTemplateString={LinkAnchorBox.LayoutString(`anchor${Doc.LinkEndpoint(link, this.rootDoc)}`)} + LayoutTemplateString={LinkAnchorBox.LayoutString(`link_anchor_${Doc.LinkEndpoint(link, this.rootDoc)}`)} /> </div> )); @@ -999,7 +1014,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps gumStream = stream; recorder = new MediaRecorder(stream); recorder.ondataavailable = async (e: any) => { - const [{ result }] = await Networking.UploadFilesToServer(e.data); + const [{ result }] = await Networking.UploadFilesToServer({ file: e.data }); if (!(result instanceof Error)) { const audioField = new AudioField(result.accessPaths.agnostic.client); const audioAnnos = Cast(dataDoc[field + '-audioAnnotations'], listSpec(AudioField), null); @@ -1044,9 +1059,9 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps @computed get innards() { TraceMobx(); const ffscale = () => this.props.DocumentView().props.CollectionFreeFormDocumentView?.().props.ScreenToLocalTransform().Scale || 1; - const showTitle = this.ShowTitle?.split(':')[0]; - const showTitleHover = this.ShowTitle?.includes(':hover'); - const captionView = !this.showCaption ? null : ( + const layout_showTitle = this.layout_showTitle?.split(':')[0]; + const layout_showTitleHover = this.layout_showTitle?.includes(':hover'); + const captionView = !this.layout_showCaption ? null : ( <div className="documentView-captionWrapper" style={{ @@ -1058,7 +1073,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps {...this.props} yPadding={10} xPadding={10} - fieldKey={this.showCaption} + fieldKey={this.layout_showCaption} fontSize={12 * Math.max(1, (2 * ffscale()) / 3)} styleProvider={this.captionStyleProvider} dontRegisterView={true} @@ -1069,27 +1084,26 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps /> </div> ); - const targetDoc = showTitle?.startsWith('_') ? this.layoutDoc : this.rootDoc; + const targetDoc = layout_showTitle?.startsWith('_') ? this.layoutDoc : this.rootDoc; const background = StrCast( SharingManager.Instance.users.find(users => users.user.email === this.dataDoc.author)?.sharingDoc.userColor, - Doc.UserDoc().showTitle && [DocumentType.RTF, DocumentType.COL].includes(this.rootDoc.type as any) ? StrCast(Doc.SharingDoc().userColor) : 'rgba(0,0,0,0.4)' + Doc.UserDoc().layout_showTitle && [DocumentType.RTF, DocumentType.COL].includes(this.rootDoc.type as any) ? StrCast(Doc.SharingDoc().userColor) : 'rgba(0,0,0,0.4)' ); - const sidebarWidthPercent = +StrCast(this.layoutDoc.sidebarWidthPercent).replace('%', ''); const titleView = !showTitle ? null : ( <div - className={`documentView-titleWrapper${showTitleHover ? '-hover' : ''}`} + className={`documentView-titleWrapper${layout_showTitleHover ? '-hover' : ''}`} key="title" style={{ position: this.headerMargin ? 'relative' : 'absolute', height: this.titleHeight, - width: !this.headerMargin ? `calc(${sidebarWidthPercent || 100}% - 18px)` : (sidebarWidthPercent || 100) + '%', // leave room for annotation button + width: !this.headerMargin ? `calc(100% - 18px)` : '100%', // leave room for annotation button color: lightOrDark(background), background, pointerEvents: (!this.disableClickScriptFunc && this.onClickHandler) || this.Document.ignoreClick ? 'none' : this.isContentActive() || this.props.isDocumentActive?.() ? 'all' : undefined, }}> <EditableView ref={this._titleRef} - contents={showTitle + contents={layout_showTitle .split(';') .map(field => field.trim()) .map(field => targetDoc[field]?.toString()) @@ -1098,27 +1112,27 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps fontSize={10} GetValue={() => { this.props.select(false); - return showTitle.split(';').length === 1 ? showTitle + '=' + Field.toString(targetDoc[showTitle.split(';')[0]] as any as Field) : '#' + showTitle; + return layout_showTitle.split(';').length === 1 ? layout_showTitle + '=' + Field.toString(targetDoc[layout_showTitle.split(';')[0]] as any as Field) : '#' + layout_showTitle; }} SetValue={undoBatch((input: string) => { if (input?.startsWith('#')) { - if (this.props.showTitle) { - this.rootDoc._showTitle = input?.substring(1) ? input.substring(1) : undefined; + if (this.props.layout_showTitle) { + this.rootDoc._layout_showTitle = input?.substring(1) ? input.substring(1) : undefined; } else { - Doc.UserDoc().showTitle = input?.substring(1) ? input.substring(1) : 'creationDate'; + Doc.UserDoc().layout_showTitle = input?.substring(1) ? input.substring(1) : 'author_date'; } } else { - var value = input.replace(new RegExp(showTitle + '='), '') as string | number; - if (showTitle !== 'title' && Number(value).toString() === value) value = Number(value); - if (showTitle.includes('Date') || showTitle === 'author') return true; - Doc.SetInPlace(targetDoc, showTitle, value, true); + var value = input.replace(new RegExp(layout_showTitle + '='), '') as string | number; + if (layout_showTitle !== 'title' && Number(value).toString() === value) value = Number(value); + if (layout_showTitle.includes('Date') || layout_showTitle === 'author') return true; + Doc.SetInPlace(targetDoc, layout_showTitle, value, true); } return true; })} /> </div> ); - return this.props.hideTitle || (!showTitle && !this.showCaption) ? ( + return this.props.hideTitle || (!layout_showTitle && !this.layout_showCaption) ? ( this.contents ) : ( <div className="documentView-styleWrapper"> @@ -1133,7 +1147,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps renderDoc = (style: object) => { TraceMobx(); - return !DocCast(this.Document) || GetEffectiveAcl(this.Document[DataSym]) === AclPrivate + return !DocCast(this.Document) || GetEffectiveAcl(this.Document[DocData]) === AclPrivate ? null : this.docContents ?? ( <div @@ -1145,8 +1159,8 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps opacity: this.opacity, cursor: Doc.ActiveTool === InkTool.None ? 'grab' : 'crosshair', color: StrCast(this.layoutDoc.color, 'inherit'), - fontFamily: StrCast(this.Document._fontFamily, 'inherit'), - fontSize: Cast(this.Document._fontSize, 'string', null), + fontFamily: StrCast(this.Document._text_fontFamily, 'inherit'), + fontSize: Cast(this.Document._text_fontSize, 'string', null), transform: this._animateScalingTo ? `scale(${this._animateScalingTo})` : undefined, transition: !this._animateScalingTo ? StrCast(this.Document.dataTransition) : `transform ${this.animateScaleTime / 1000}s ease-${this._animateScalingTo < 1 ? 'in' : 'out'}`, }}> @@ -1218,7 +1232,7 @@ export class DocumentViewInternal extends DocComponent<DocumentViewInternalProps pointerEvents: this.pointerEvents === 'visiblePainted' ? 'none' : this.pointerEvents, }}> <> - {DocumentViewInternal.AnimationEffect(renderDoc, this.rootDoc[AnimationSym], this.rootDoc)} + {DocumentViewInternal.AnimationEffect(renderDoc, this.rootDoc[Animation], this.rootDoc)} {borderPath?.jsx} </> </div> @@ -1252,8 +1266,8 @@ export class DocumentView extends React.Component<DocumentViewProps> { public setAnimEffect = (presEffect: Doc, timeInMs: number, afterTrans?: () => void) => { this.AnimEffectTimer && clearTimeout(this.AnimEffectTimer); - this.rootDoc[AnimationSym] = presEffect; - this.AnimEffectTimer = setTimeout(() => (this.rootDoc[AnimationSym] = undefined), timeInMs); + this.rootDoc[Animation] = presEffect; + this.AnimEffectTimer = setTimeout(() => (this.rootDoc[Animation] = undefined), timeInMs); }; public setViewTransition = (transProp: string, timeInMs: number, afterTrans?: () => void, dataTrans = false) => { this.rootDoc._viewTransition = `${transProp} ${timeInMs}ms`; @@ -1282,13 +1296,13 @@ export class DocumentView extends React.Component<DocumentViewProps> { } // shows a stacking view collection (by default, but the user can change) of all documents linked to the source - public static showBackLinks(linkSource: Doc) { - const docId = Doc.CurrentUserEmail + Doc.GetProto(linkSource)[Id] + '-pivotish'; + public static showBackLinks(linkAnchor: Doc) { + const docId = Doc.CurrentUserEmail + Doc.GetProto(linkAnchor)[Id] + '-pivotish'; // prettier-ignore - DocServer.GetRefField(docId).then(docx => docx instanceof Doc && + DocServer.GetRefField(docId).then(docx => LightboxView.SetLightboxDoc( - docx || // reuse existing pivot view of documents, or else create a new collection - Docs.Create.StackingDocument([], { title: linkSource.title + '-pivot', _width: 500, _height: 500, linkSource, updateContentsScript: ScriptField.MakeScript('updateLinkCollection(self)') }, docId) + (docx as Doc) ?? // reuse existing pivot view of documents, or else create a new collection + Docs.Create.StackingDocument([], { title: linkAnchor.title + '-pivot', _width: 500, _height: 500, target: linkAnchor, updateContentsScript: ScriptField.MakeScript('updateLinkCollection(self, self.target)') }, docId) ) ); } @@ -1317,14 +1331,14 @@ export class DocumentView extends React.Component<DocumentViewProps> { get LayoutFieldKey() { return this.docView?.LayoutFieldKey || 'layout'; } - @computed get fitWidth() { - return this.docView?._componentView?.fitWidth?.() ?? this.props.fitWidth?.(this.rootDoc) ?? this.layoutDoc?.fitWidth; + @computed get layout_fitWidth() { + return this.docView?._componentView?.layout_fitWidth?.() ?? this.props.layout_fitWidth?.(this.rootDoc) ?? this.layoutDoc?.layout_fitWidth; } @computed get anchorViewDoc() { - return this.props.LayoutTemplateString?.includes('anchor2') ? DocCast(this.rootDoc['anchor2']) : this.props.LayoutTemplateString?.includes('anchor1') ? DocCast(this.rootDoc['anchor1']) : undefined; + return this.props.LayoutTemplateString?.includes('link_anchor_2') ? DocCast(this.rootDoc['link_anchor_2']) : this.props.LayoutTemplateString?.includes('link_anchor_1') ? DocCast(this.rootDoc['link_anchor_1']) : undefined; } @computed get hideLinkButton() { - return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HideLinkButton + (this.isSelected() ? ':selected' : '')); + return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HideLinkBtn + (this.isSelected() ? ':selected' : '')); } @computed get linkCountView() { const hideCount = this.props.renderDepth === -1 || SnappingManager.GetIsDragging() || (this.isSelected() && this.props.renderDepth) || !this._isHovering || this.hideLinkButton; @@ -1337,13 +1351,13 @@ export class DocumentView extends React.Component<DocumentViewProps> { return Doc.Layout(this.Document, this.props.LayoutTemplate?.()); } @computed get nativeWidth() { - return this.docView?._componentView?.reverseNativeScaling?.() ? 0 : returnVal(this.props.NativeWidth?.(), Doc.NativeWidth(this.layoutDoc, this.props.DataDoc, !this.fitWidth)); + return this.docView?._componentView?.reverseNativeScaling?.() ? 0 : returnVal(this.props.NativeWidth?.(), Doc.NativeWidth(this.layoutDoc, this.props.DataDoc, !this.layout_fitWidth)); } @computed get nativeHeight() { - return this.docView?._componentView?.reverseNativeScaling?.() ? 0 : returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this.props.DataDoc, !this.fitWidth)); + return this.docView?._componentView?.reverseNativeScaling?.() ? 0 : returnVal(this.props.NativeHeight?.(), Doc.NativeHeight(this.layoutDoc, this.props.DataDoc, !this.layout_fitWidth)); } @computed get shouldNotScale() { - return (this.fitWidth && !this.nativeWidth) || [CollectionViewType.Docking].includes(this.Document._viewType as any); + return this.props.shouldNotScale?.() || (this.layout_fitWidth && !this.nativeWidth) || [CollectionViewType.Docking].includes(this.Document._type_collection as any); } @computed get effectiveNativeWidth() { return this.shouldNotScale ? 0 : this.nativeWidth || NumCast(this.layoutDoc.width); @@ -1354,8 +1368,8 @@ export class DocumentView extends React.Component<DocumentViewProps> { @computed get nativeScaling() { if (this.shouldNotScale) return 1; const minTextScale = this.Document.type === DocumentType.RTF ? 0.1 : 0; - if (this.fitWidth || this.props.PanelHeight() / (this.effectiveNativeHeight || 1) > this.props.PanelWidth() / (this.effectiveNativeWidth || 1)) { - return Math.max(minTextScale, this.props.PanelWidth() / (this.effectiveNativeWidth || 1)); // width-limited or fitWidth + if (this.layout_fitWidth || this.props.PanelHeight() / (this.effectiveNativeHeight || 1) > this.props.PanelWidth() / (this.effectiveNativeWidth || 1)) { + return Math.max(minTextScale, this.props.PanelWidth() / (this.effectiveNativeWidth || 1)); // width-limited or layout_fitWidth } return Math.max(minTextScale, this.props.PanelHeight() / (this.effectiveNativeHeight || 1)); // height-limited or unscaled } @@ -1363,7 +1377,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { return this.effectiveNativeWidth ? this.effectiveNativeWidth * this.nativeScaling : this.props.PanelWidth(); } @computed get panelHeight() { - if (this.effectiveNativeHeight && (!this.fitWidth || !this.layoutDoc.nativeHeightUnfrozen)) { + if (this.effectiveNativeHeight && (!this.layout_fitWidth || !this.layoutDoc.nativeHeightUnfrozen)) { return Math.min(this.props.PanelHeight(), this.effectiveNativeHeight * this.nativeScaling); } return this.props.PanelHeight(); @@ -1372,7 +1386,10 @@ export class DocumentView extends React.Component<DocumentViewProps> { return this.effectiveNativeWidth ? Math.max(0, (this.props.PanelWidth() - this.effectiveNativeWidth * this.nativeScaling) / 2) : 0; } @computed get Yshift() { - return this.effectiveNativeWidth && this.effectiveNativeHeight && Math.abs(this.Xshift) < 0.001 && (!this.layoutDoc.nativeHeightUnfrozen || (!this.fitWidth && this.effectiveNativeHeight * this.nativeScaling <= this.props.PanelHeight())) + return this.effectiveNativeWidth && + this.effectiveNativeHeight && + Math.abs(this.Xshift) < 0.001 && + (!this.layoutDoc.nativeHeightUnfrozen || (!this.layout_fitWidth && this.effectiveNativeHeight * this.nativeScaling <= this.props.PanelHeight())) ? Math.max(0, (this.props.PanelHeight() - this.effectiveNativeHeight * this.nativeScaling) / 2) : 0; } @@ -1408,10 +1425,10 @@ export class DocumentView extends React.Component<DocumentViewProps> { finished?.(); this.docView && (this.docView._animateScaleTime = animTime); }); - const layoutKey = Cast(this.Document.layoutKey, 'string', null); - if (layoutKey !== 'layout_icon') { + const layout_fieldKey = Cast(this.Document.layout_fieldKey, 'string', null); + if (layout_fieldKey !== 'layout_icon') { this.switchViews(true, 'icon', finalFinished); - if (layoutKey && layoutKey !== 'layout' && layoutKey !== 'layout_icon') this.Document.deiconifyLayout = layoutKey.replace('layout_', ''); + if (layout_fieldKey && layout_fieldKey !== 'layout' && layout_fieldKey !== 'layout_icon') this.Document.deiconifyLayout = layout_fieldKey.replace('layout_', ''); } else { const deiconifyLayout = Cast(this.Document.deiconifyLayout, 'string', null); this.switchViews(deiconifyLayout ? true : false, deiconifyLayout, finalFinished); @@ -1431,7 +1448,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { setTimeout( action(() => { if (useExistingLayout && custom && this.rootDoc['layout_' + view]) { - this.rootDoc.layoutKey = 'layout_' + view; + this.rootDoc.layout_fieldKey = 'layout_' + view; } else { this.setCustomView(custom, view); } @@ -1473,8 +1490,8 @@ export class DocumentView extends React.Component<DocumentViewProps> { // increase max auto height if document has been resized to be greater than current max () => NumCast(this.layoutDoc._height), action(height => { - const docMax = NumCast(this.layoutDoc.docMaxAutoHeight); - if (docMax && docMax < height) this.layoutDoc.docMaxAutoHeight = height; + const docMax = NumCast(this.layoutDoc.layout_maxAutoHeight); + if (docMax && docMax < height) this.layoutDoc.layout_maxAutoHeight = height; }) ); !BoolCast(this.props.Document.dontRegisterView, this.props.dontRegisterView) && DocumentManager.Instance.AddView(this); @@ -1518,7 +1535,7 @@ export class DocumentView extends React.Component<DocumentViewProps> { width: xshift ?? `${(100 * (this.props.PanelWidth() - this.Xshift * 2)) / this.props.PanelWidth()}%`, height: this.props.forceAutoHeight ? undefined - : yshift ?? (this.fitWidth ? `${this.panelHeight}px` : `${(((100 * this.effectiveNativeHeight) / this.effectiveNativeWidth) * this.props.PanelWidth()) / this.props.PanelHeight()}%`), + : yshift ?? (this.layout_fitWidth ? `${this.panelHeight}px` : `${(((100 * this.effectiveNativeHeight) / this.effectiveNativeWidth) * this.props.PanelWidth()) / this.props.PanelHeight()}%`), }}> <DocumentViewInternal {...this.props} @@ -1556,26 +1573,27 @@ ScriptingGlobals.add(function deiconifyViewToLightbox(documentView: DocumentView }); ScriptingGlobals.add(function toggleDetail(dv: DocumentView, detailLayoutKeySuffix: string) { - if (dv.Document.layoutKey === 'layout_' + detailLayoutKeySuffix) dv.switchViews(false, 'layout'); + if (dv.Document.layout_fieldKey === 'layout_' + detailLayoutKeySuffix) dv.switchViews(false, 'layout'); else dv.switchViews(true, detailLayoutKeySuffix, undefined, true); }); -ScriptingGlobals.add(function updateLinkCollection(linkCollection: Doc) { - const linkSource = Cast(linkCollection.linkSource, Doc, null); +ScriptingGlobals.add(function updateLinkCollection(linkCollection: Doc, linkSource: Doc) { const collectedLinks = DocListCast(Doc.GetProto(linkCollection).data); - let wid = linkSource[WidthSym](); + let wid = linkSource[Width](); + let embedding: Doc | undefined; const links = LinkManager.Links(linkSource); links.forEach(link => { const other = LinkManager.getOppositeAnchor(link, linkSource); - const otherdoc = !other ? undefined : other.annotationOn ? Cast(other.annotationOn, Doc, null) : other; + const otherdoc = DocCast(other?.annotationOn ?? other); if (otherdoc && !collectedLinks?.some(d => Doc.AreProtosEqual(d, otherdoc))) { - const alias = Doc.MakeAlias(otherdoc); - alias.x = wid; - alias.y = 0; - alias._lockedPosition = false; - wid += otherdoc[WidthSym](); - Doc.AddDocToList(Doc.GetProto(linkCollection), 'data', alias); + embedding = Doc.MakeEmbedding(otherdoc); + embedding.x = wid; + embedding.y = 0; + embedding._lockedPosition = false; + wid += otherdoc[Width](); + Doc.AddDocToList(Doc.GetProto(linkCollection), 'data', embedding); } }); + embedding && DocServer.UPDATE_SERVER_CACHE(); // if a new embedding was made, update the client's server cache so that it will not come back as a promise return links; }); diff --git a/src/client/views/nodes/EquationBox.tsx b/src/client/views/nodes/EquationBox.tsx index 163c5a9ed..8d45c5724 100644 --- a/src/client/views/nodes/EquationBox.tsx +++ b/src/client/views/nodes/EquationBox.tsx @@ -2,7 +2,7 @@ import EquationEditor from 'equation-editor-react'; import { action, reaction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { WidthSym } from '../../../fields/Doc'; +import { Width } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { NumCast, StrCast } from '../../../fields/Types'; import { TraceMobx } from '../../../fields/util'; @@ -67,7 +67,7 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { } if (e.key === 'Tab') { const graph = Docs.Create.FunctionPlotDocument([this.rootDoc], { - x: NumCast(this.layoutDoc.x) + this.layoutDoc[WidthSym](), + x: NumCast(this.layoutDoc.x) + this.layoutDoc[Width](), y: NumCast(this.layoutDoc.y), _width: 400, _height: 300, @@ -90,7 +90,7 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { }; render() { TraceMobx(); - const scale = (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1); + const scale = (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1); return ( <div ref={r => this.updateSize()} @@ -101,10 +101,10 @@ export class EquationBox extends ViewBoxBaseComponent<FieldViewProps>() { width: 'fit-content', // `${100 / scale}%`, height: `${100 / scale}%`, pointerEvents: !this.props.isSelected() ? 'none' : undefined, - fontSize: StrCast(this.rootDoc._fontSize), + fontSize: StrCast(this.rootDoc._text_fontSize), }} onKeyDown={e => e.stopPropagation()}> - <EquationEditor ref={this._ref} value={this.dataDoc.text || 'x'} spaceBehavesLikeTab={true} onChange={this.onChange} autoCommands="pi theta sqrt sum prod alpha beta gamma rho" autoOperatorNames="sin cos tan" /> + <EquationEditor ref={this._ref} value={StrCast(this.dataDoc.text, 'x')} spaceBehavesLikeTab={true} onChange={this.onChange} autoCommands="pi theta sqrt sum prod alpha beta gamma rho" autoOperatorNames="sin cos tan" /> </div> ); } diff --git a/src/client/views/nodes/FieldView.tsx b/src/client/views/nodes/FieldView.tsx index 86779e0dd..85dd779fc 100644 --- a/src/client/views/nodes/FieldView.tsx +++ b/src/client/views/nodes/FieldView.tsx @@ -50,50 +50,18 @@ export class FieldView extends React.Component<FieldViewProps> { @computed get field(): FieldResult { - const { Document, fieldKey } = this.props; + const { Document, fieldKey: fieldKey } = this.props; return Document[fieldKey]; } render() { const field = this.field; - if (field === undefined) { - return <p>{'<null>'}</p>; - } - // if (typeof field === "string") { - // return <p>{field}</p>; - // } - // else if (field instanceof RichTextField) { - // return <FormattedTextBox {...this.props} />; - // } - // else if (field instanceof ImageField) { - // return <ImageBox {...this.props} />; - // } - // else if (field instaceof PresBox) { - // return <PresBox {...this.props} />; - // } - // else if (field instanceof VideoField) { - // return <VideoBox {...this.props} />; - // } - // else if (field instanceof AudioField) { - // return <AudioBox {...this.props} />; - //} - else if (field instanceof DateField) { - return <p>{field.date.toLocaleString()}</p>; - } else if (field instanceof Doc) { - return ( - <p> - <b>{field.title?.toString()}</b> - </p> - ); - } else if (field instanceof List) { - return <div> {field.length ? field.map(f => Field.toString(f)).join(', ') : ''} </div>; - } - // bcz: this belongs here, but it doesn't render well so taking it out for now - else if (field instanceof WebField) { - return <p>{Field.toString(field.url.href)}</p>; - } else if (!(field instanceof Promise)) { - return <p>{Field.toString(field)}</p>; - } else { - return <p> {'Waiting for server...'} </p>; - } + // prettier-ignore + if (field instanceof Doc) return <p> <b>{field.title?.toString()}</b></p>; + if (field === undefined) return <p>{'<null>'}</p>; + if (field instanceof DateField) return <p>{field.date.toLocaleString()}</p>; + if (field instanceof List) return <div> {field.map(f => Field.toString(f)).join(', ')} </div>; + if (field instanceof WebField) return <p>{Field.toString(field.url.href)}</p>; + if (!(field instanceof Promise)) return <p>{Field.toString(field)}</p>; + return <p> {'Waiting for server...'} </p>; } } diff --git a/src/client/views/nodes/FunctionPlotBox.tsx b/src/client/views/nodes/FunctionPlotBox.tsx index b43e359ff..1a78583f9 100644 --- a/src/client/views/nodes/FunctionPlotBox.tsx +++ b/src/client/views/nodes/FunctionPlotBox.tsx @@ -43,7 +43,10 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps> ); } getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { - const anchor = Docs.Create.TextanchorDocument({ annotationOn: this.rootDoc, unrendered: true }); + const anchor = Docs.Create.FunctionPlotConfigDocument({ + // + annotationOn: this.rootDoc, + }); PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), datarange: true } }, this.rootDoc); anchor.presXRange = new List<number>(Array.from(this._plot.options.xAxis.domain)); anchor.presYRange = new List<number>(Array.from(this._plot.options.yAxis.domain)); @@ -90,7 +93,7 @@ export class FunctionPlotBox extends ViewBoxAnnotatableComponent<FieldViewProps> if (ele) { this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.layoutDoc); } - // if (this.autoHeight) this.tryUpdateScrollHeight(); + // if (this.layout_autoHeight) this.tryUpdateScrollHeight(); }; @computed get theGraph() { return <div id={`${this._plotId}`} ref={r => r && this.createGraph(r)} style={{ position: 'absolute', width: '100%', height: '100%' }} onPointerDown={e => e.stopPropagation()} />; diff --git a/src/client/views/nodes/ImageBox.tsx b/src/client/views/nodes/ImageBox.tsx index c9be10d3a..5b302e7ce 100644 --- a/src/client/views/nodes/ImageBox.tsx +++ b/src/client/views/nodes/ImageBox.tsx @@ -3,7 +3,8 @@ import { Tooltip } from '@material-ui/core'; import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import { extname } from 'path'; -import { DataSym, Doc, DocListCast, Opt, WidthSym } from '../../../fields/Doc'; +import { Doc, DocListCast, Opt } from '../../../fields/Doc'; +import { DocData, Width } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; @@ -77,13 +78,12 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { const anchor = this._getAnchor?.(this._savedAnnotations, false) ?? // use marquee anchor, otherwise, save zoom/pan as anchor - Docs.Create.ImageanchorDocument({ + Docs.Create.ImageConfigDocument({ title: 'ImgAnchor:' + this.rootDoc.title, - presPanX: NumCast(this.layoutDoc._panX), - presPanY: NumCast(this.layoutDoc._panY), - presViewScale: Cast(this.layoutDoc._viewScale, 'number', null), + presPanX: NumCast(this.layoutDoc._freeform_panX), + presPanY: NumCast(this.layoutDoc._freeform_panY), + presViewScale: Cast(this.layoutDoc._freeform_scale, 'number', null), presTransition: 1000, - unrendered: true, annotationOn: this.rootDoc, }); if (anchor) { @@ -99,7 +99,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this._disposers.sizer = reaction( () => ({ forceFull: this.props.renderDepth < 1 || this.layoutDoc._showFullRes, - scrSize: (this.props.ScreenToLocalTransform().inverse().transformDirection(this.nativeSize.nativeWidth, this.nativeSize.nativeHeight)[0] / this.nativeSize.nativeWidth) * NumCast(this.rootDoc._viewScale, 1), + scrSize: (this.props.ScreenToLocalTransform().inverse().transformDirection(this.nativeSize.nativeWidth, this.nativeSize.nativeHeight)[0] / this.nativeSize.nativeWidth) * NumCast(this.rootDoc._freeform_scale, 1), selected: this.props.isSelected(), }), ({ forceFull, scrSize, selected }) => (this._curSuffix = selected ? '_o' : this.fieldKey === 'icon' ? '_m' : forceFull ? '_o' : scrSize < 0.25 ? '_s' : scrSize < 0.5 ? '_m' : scrSize < 0.8 ? '_l' : '_o'), @@ -107,7 +107,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp ); const layoutDoc = this.layoutDoc; this._disposers.path = reaction( - () => ({ nativeSize: this.nativeSize, width: this.layoutDoc[WidthSym]() }), + () => ({ nativeSize: this.nativeSize, width: this.layoutDoc[Width]() }), ({ nativeSize, width }) => { if (layoutDoc === this.layoutDoc || !this.layoutDoc._height) { this.layoutDoc._height = (width * nativeSize.nativeHeight) / nativeSize.nativeWidth; @@ -116,7 +116,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp { fireImmediately: true } ); this._disposers.scroll = reaction( - () => this.layoutDoc._scrollTop, + () => this.layoutDoc.layout_scrollTop, s_top => { this._forcedScroll = true; !this._ignoreScroll && this._mainCont.current && (this._mainCont.current.scrollTop = NumCast(s_top)); @@ -144,14 +144,14 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp de.complete.docDragData.droppedDocuments.forEach( action((drop: Doc) => { Doc.AddDocToList(this.dataDoc, this.fieldKey + '-alternates', drop); - this.rootDoc[this.fieldKey + '-usePath'] = 'alternate:hover'; + this.rootDoc[this.fieldKey + '_usePath'] = 'alternate:hover'; e.stopPropagation(); }) ); } else if (de.altKey || !this.dataDoc[this.fieldKey]) { const layoutDoc = de.complete.docDragData?.draggedDocuments[0]; const targetField = Doc.LayoutFieldKey(layoutDoc); - const targetDoc = layoutDoc[DataSym]; + const targetDoc = layoutDoc[DocData]; if (targetDoc[targetField] instanceof ImageField) { this.dataDoc[this.fieldKey] = ObjectField.MakeCopy(targetDoc[targetField] as ImageField); Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(targetDoc), this.fieldKey); @@ -167,28 +167,28 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @undoBatch setNativeSize = action(() => { - const scaling = (this.props.DocumentView?.().props.ScreenToLocalTransform().Scale || 1) / NumCast(this.rootDoc._viewScale, 1); + const scaling = (this.props.DocumentView?.().props.ScreenToLocalTransform().Scale || 1) / NumCast(this.rootDoc._freeform_scale, 1); const nscale = NumCast(this.props.PanelWidth()) / scaling; - const nh = nscale / NumCast(this.dataDoc[this.fieldKey + '-nativeHeight']); - const nw = nscale / NumCast(this.dataDoc[this.fieldKey + '-nativeWidth']); - this.dataDoc[this.fieldKey + '-nativeHeight'] = NumCast(this.dataDoc[this.fieldKey + '-nativeHeight']) * nh; - this.dataDoc[this.fieldKey + '-nativeWidth'] = NumCast(this.dataDoc[this.fieldKey + '-nativeWidth']) * nh; - this.rootDoc._panX = nh * NumCast(this.rootDoc._panX); - this.rootDoc._panY = nh * NumCast(this.rootDoc._panY); - this.dataDoc._panXMax = this.dataDoc._panXMax ? nh * NumCast(this.dataDoc._panXMax) : undefined; - this.dataDoc._panXMin = this.dataDoc._panXMin ? nh * NumCast(this.dataDoc._panXMin) : undefined; - this.dataDoc._panYMax = this.dataDoc._panYMax ? nw * NumCast(this.dataDoc._panYMax) : undefined; - this.dataDoc._panYMin = this.dataDoc._panYMin ? nw * NumCast(this.dataDoc._panYMin) : undefined; + const nh = nscale / NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']); + const nw = nscale / NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']); + this.dataDoc[this.fieldKey + '_nativeHeight'] = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']) * nh; + this.dataDoc[this.fieldKey + '_nativeWidth'] = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']) * nh; + this.rootDoc._freeform_panX = nh * NumCast(this.rootDoc._freeform_panX); + this.rootDoc._freeform_panY = nh * NumCast(this.rootDoc._freeform_panY); + this.dataDoc._freeform_panXMax = this.dataDoc._freeform_panXMax ? nh * NumCast(this.dataDoc._freeform_panXMax) : undefined; + this.dataDoc._freeform_panXMin = this.dataDoc._freeform_panXMin ? nh * NumCast(this.dataDoc._freeform_panXMin) : undefined; + this.dataDoc._freeform_panYMax = this.dataDoc._freeform_panYMax ? nw * NumCast(this.dataDoc._freeform_panYMax) : undefined; + this.dataDoc._freeform_panYMin = this.dataDoc._freeform_panYMin ? nw * NumCast(this.dataDoc._freeform_panYMin) : undefined; }); @undoBatch rotate = action(() => { - const nw = NumCast(this.dataDoc[this.fieldKey + '-nativeWidth']); - const nh = NumCast(this.dataDoc[this.fieldKey + '-nativeHeight']); + const nw = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth']); + const nh = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight']); const w = this.layoutDoc._width; const h = this.layoutDoc._height; this.dataDoc[this.fieldKey + '-rotation'] = (NumCast(this.dataDoc[this.fieldKey + '-rotation']) + 90) % 360; - this.dataDoc[this.fieldKey + '-nativeWidth'] = nh; - this.dataDoc[this.fieldKey + '-nativeHeight'] = nw; + this.dataDoc[this.fieldKey + '_nativeWidth'] = nh; + this.dataDoc[this.fieldKey + '_nativeHeight'] = nw; this.layoutDoc._width = h; this.layoutDoc._height = w; }); @@ -204,7 +204,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp const anchy = NumCast(cropping.y); const anchw = NumCast(cropping._width); const anchh = NumCast(cropping._height); - const viewScale = NumCast(this.rootDoc[this.fieldKey + '-nativeWidth']) / anchw; + const viewScale = NumCast(this.rootDoc[this.fieldKey + '_nativeWidth']) / anchw; cropping.title = 'crop: ' + this.rootDoc.title; cropping.x = NumCast(this.rootDoc.x) + NumCast(this.rootDoc._width); cropping.y = NumCast(this.rootDoc.y); @@ -213,25 +213,25 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp cropping.onClick = undefined; const croppingProto = Doc.GetProto(cropping); croppingProto.annotationOn = undefined; - croppingProto.isPrototype = true; + croppingProto.isDataDoc = true; croppingProto.backgroundColor = undefined; croppingProto.proto = Cast(this.rootDoc.proto, Doc, null)?.proto; // set proto of cropping's data doc to be IMAGE_PROTO croppingProto.type = DocumentType.IMG; croppingProto.layout = ImageBox.LayoutString('data'); croppingProto.data = ObjectField.MakeCopy(this.rootDoc[this.fieldKey] as ObjectField); - croppingProto['data-nativeWidth'] = anchw; - croppingProto['data-nativeHeight'] = anchh; - croppingProto.viewScale = viewScale; - croppingProto.viewScaleMin = viewScale; - croppingProto.panX = anchx / viewScale; - croppingProto.panY = anchy / viewScale; - croppingProto.panXMin = anchx / viewScale; - croppingProto.panXMax = anchw / viewScale; - croppingProto.panYMin = anchy / viewScale; - croppingProto.panYMax = anchh / viewScale; + croppingProto['data_nativeWidth'] = anchw; + croppingProto['data_nativeHeight'] = anchh; + croppingProto.freeform_scale = viewScale; + croppingProto.freeform_scaleMin = viewScale; + croppingProto.freeform_panX = anchx / viewScale; + croppingProto.freeform_panY = anchy / viewScale; + croppingProto.freeform_panXMin = anchx / viewScale; + croppingProto.freeform_panXMax = anchw / viewScale; + croppingProto.freeform_panYMin = anchy / viewScale; + croppingProto.freeform_panYMax = anchh / viewScale; if (addCrop) { - DocUtils.MakeLink(region, cropping, { linkRelationship: 'cropped image' }); - cropping.x = NumCast(this.rootDoc.x) + this.rootDoc[WidthSym](); + DocUtils.MakeLink(region, cropping, { link_relationship: 'cropped image' }); + cropping.x = NumCast(this.rootDoc.x) + this.rootDoc[Width](); cropping.y = NumCast(this.rootDoc.y); this.props.addDocTab(cropping, OpenWhere.inParent); } @@ -303,7 +303,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } considerGooglePhotosLink = () => { - const remoteUrl = this.dataDoc.googlePhotosUrl; + const remoteUrl = StrCast(this.dataDoc.googlePhotosUrl); // bcz: StrCast or URLCast??? return !remoteUrl ? null : <img draggable={false} style={{ transformOrigin: 'bottom right' }} id={'google-photos'} src={'/assets/google_photos.png'} onClick={() => window.open(remoteUrl)} />; }; @@ -312,7 +312,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp return !tags ? null : <img id={'google-tags'} src={'/assets/google_tags.png'} />; }; - getScrollHeight = () => (this.props.fitWidth?.(this.rootDoc) !== false && NumCast(this.rootDoc._viewScale, 1) === NumCast(this.rootDoc._viewScaleMin, 1) ? this.nativeSize.nativeHeight : undefined); + getScrollHeight = () => (this.props.layout_fitWidth?.(this.rootDoc) !== false && NumCast(this.rootDoc._freeform_scale, 1) === NumCast(this.rootDoc._freeform_scaleMin, 1) ? this.nativeSize.nativeHeight : undefined); @computed private get considerDownloadIcon() { @@ -358,13 +358,13 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @computed get nativeSize() { TraceMobx(); - const nativeWidth = NumCast(this.dataDoc[this.fieldKey + '-nativeWidth'], NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth'], 500)); - const nativeHeight = NumCast(this.dataDoc[this.fieldKey + '-nativeHeight'], NumCast(this.layoutDoc[this.fieldKey + '-nativeHeight'], 500)); + const nativeWidth = NumCast(this.dataDoc[this.fieldKey + '_nativeWidth'], NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth'], 500)); + const nativeHeight = NumCast(this.dataDoc[this.fieldKey + '_nativeHeight'], NumCast(this.layoutDoc[this.fieldKey + '_nativeHeight'], 500)); const nativeOrientation = NumCast(this.dataDoc[this.fieldKey + '-nativeOrientation'], 1); return { nativeWidth, nativeHeight, nativeOrientation }; } @computed get overlayImageIcon() { - const usePath = this.rootDoc[`_${this.fieldKey}-usePath`]; + const usePath = this.rootDoc[`_${this.fieldKey}_usePath`]; return ( <Tooltip title={ @@ -385,7 +385,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp <div className="imageBox-alternateDropTarget" ref={this._overlayIconRef} - onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, e => (this.rootDoc[`_${this.fieldKey}-usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : undefined))} + onPointerDown={e => setupMoveUpEvents(e.target, e, returnFalse, emptyFunction, e => (this.rootDoc[`_${this.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : undefined))} style={{ display: (SnappingManager.GetIsDragging() && DragManager.DocDragData?.canEmbed) || DocListCast(this.dataDoc[this.fieldKey + '-alternates']).length ? 'block' : 'none', width: 'min(10%, 25px)', @@ -431,7 +431,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp transformOrigin = 'right top'; transform = `translate(-100%, 0%) rotate(${rotation}deg) scale(${aspect})`; } - const usePath = this.rootDoc[`_${this.fieldKey}-usePath`]; + const usePath = this.rootDoc[`_${this.fieldKey}_usePath`]; return ( <div className="imageBox-cont" onPointerEnter={action(() => (this._isHovering = true))} onPointerLeave={action(() => (this._isHovering = false))} key={this.layoutDoc[Id]} ref={this.createDropTarget} onPointerDown={this.marqueeDown}> @@ -459,9 +459,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp TraceMobx(); return <div className="imageBox-annotationLayer" style={{ height: this.props.PanelHeight() }} ref={this._annotationLayer} />; } - screenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._scrollTop) * this.props.ScreenToLocalTransform().Scale); + screenToLocalTransform = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._layout_scrollTop) * this.props.ScreenToLocalTransform().Scale); marqueeDown = (e: React.PointerEvent) => { - if (!e.altKey && e.button === 0 && NumCast(this.rootDoc._viewScale, 1) <= NumCast(this.rootDoc.viewScaleMin, 1) && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { + if (!e.altKey && e.button === 0 && NumCast(this.rootDoc._freeform_scale, 1) <= NumCast(this.rootDoc.freeform_scaleMin, 1) && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { setupMoveUpEvents( this, e, @@ -497,9 +497,9 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp ref={this._mainCont} onScroll={action(e => { if (!this._forcedScroll) { - if (this.layoutDoc._scrollTop || this._mainCont.current?.scrollTop) { + if (this.layoutDoc._layout_scrollTop || this._mainCont.current?.scrollTop) { this._ignoreScroll = true; - this.layoutDoc._scrollTop = this._mainCont.current?.scrollTop; + this.layoutDoc._layout_scrollTop = this._mainCont.current?.scrollTop; this._ignoreScroll = false; } } @@ -510,7 +510,7 @@ export class ImageBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp height: this.props.PanelWidth() ? undefined : `100%`, pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined, borderRadius, - overflow: this.layoutDoc.fitWidth || this.props.fitWidth?.(this.rootDoc) ? 'auto' : undefined, + overflow: this.layoutDoc.layout_fitWidth || this.props.layout_fitWidth?.(this.rootDoc) ? 'auto' : undefined, }}> <CollectionFreeFormView ref={this._ffref} diff --git a/src/client/views/nodes/KeyValueBox.tsx b/src/client/views/nodes/KeyValueBox.tsx index e317de11e..5b6b0b5a7 100644 --- a/src/client/views/nodes/KeyValueBox.tsx +++ b/src/client/views/nodes/KeyValueBox.tsx @@ -20,6 +20,10 @@ import { ImageBox } from './ImageBox'; import './KeyValueBox.scss'; import { KeyValuePair } from './KeyValuePair'; import React = require('react'); +import { DocumentManager } from '../../util/DocumentManager'; +import { ScriptingGlobals } from '../../util/ScriptingGlobals'; +import { ScriptingRepl } from '../ScriptingRepl'; +import { DocumentIconContainer } from './DocumentIcon'; export type KVPScript = { script: CompiledScript; @@ -42,15 +46,13 @@ export class KeyValueBox extends React.Component<FieldViewProps> { } reverseNativeScaling = returnTrue; able = returnAlways; - fitWidth = returnTrue; + layout_fitWidth = returnTrue; overridePointerEvents = returnAll; onClickScriptDisable = returnAlways; @observable private rows: KeyValuePair[] = []; + @observable _splitPercentage = 50; - @computed get splitPercentage() { - return NumCast(this.props.Document.schemaSplitPercentage, 50); - } get fieldDocToLayout() { return this.props.fieldKey ? DocCast(this.props.Document[this.props.fieldKey], DocCast(this.props.Document)) : this.props.Document; } @@ -75,14 +77,14 @@ export class KeyValueBox extends React.Component<FieldViewProps> { value = dubEq ? value.substring(2) : value; const options: ScriptOptions = { addReturn: true, typecheck: false, params: { this: Doc.name, self: Doc.name, _last_: 'any', _readOnly_: 'boolean' }, editable: true }; if (dubEq) options.typecheck = false; - const script = CompileScript(value, options); + const script = CompileScript(value, { ...options, transformer: DocumentIconContainer.getTransformer() }); return !script.compiled ? undefined : { script, type: dubEq, onDelegate: eq }; } public static ApplyKVPScript(doc: Doc, key: string, kvpScript: KVPScript, forceOnDelegate?: boolean): boolean { const { script, type, onDelegate } = kvpScript; //const target = onDelegate ? Doc.Layout(doc.layout) : Doc.GetProto(doc); // bcz: TODO need to be able to set fields on layout templates - const target = forceOnDelegate || onDelegate || key.startsWith('_') ? doc : doc.proto || doc; + const target = forceOnDelegate || onDelegate || key.startsWith('_') ? doc : DocCast(doc.proto, doc); let field: Field; if (type === 'computed') { field = new ComputedField(script); @@ -143,7 +145,9 @@ export class KeyValueBox extends React.Component<FieldViewProps> { const rows: JSX.Element[] = []; let i = 0; const self = this; - for (const key of Object.keys(ids).slice().sort()) { + const keys = Object.keys(ids).slice(); + //for (const key of [...keys.filter(id => id !== 'layout' && !id.includes('_')).sort(), ...keys.filter(id => id === 'layout' || id.includes('_')).sort()]) { + for (const key of keys.sort()) { rows.push( <KeyValuePair doc={realDoc} @@ -158,7 +162,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { if (el) self.rows.push(el); }; })()} - keyWidth={100 - this.splitPercentage} + keyWidth={100 - this._splitPercentage} rowStyle={'keyValueBox-' + (i++ % 2 ? 'oddRow' : 'evenRow')} key={key} keyName={key} @@ -176,7 +180,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { this._keyInput.current!.select(); e.stopPropagation(); }} - style={{ width: `${100 - this.splitPercentage}%` }}> + style={{ width: `${100 - this._splitPercentage}%` }}> <input style={{ width: '100%' }} ref={this._keyInput} type="text" placeholder="Key" /> </td> <td @@ -185,7 +189,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { this._valInput.current!.select(); e.stopPropagation(); }} - style={{ width: `${this.splitPercentage}%` }}> + style={{ width: `${this._splitPercentage}%` }}> <input style={{ width: '100%' }} ref={this._valInput} type="text" placeholder="Value" onKeyDown={this.onEnterKey} /> </td> </tr> @@ -195,7 +199,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { @action onDividerMove = (e: PointerEvent): void => { const nativeWidth = this._mainCont.current!.getBoundingClientRect(); - this.props.Document.schemaSplitPercentage = Math.max(0, 100 - Math.round(((e.clientX - nativeWidth.left) / nativeWidth.width) * 100)); + this._splitPercentage = Math.max(0, 100 - Math.round(((e.clientX - nativeWidth.left) / nativeWidth.width) * 100)); }; @action onDividerUp = (e: PointerEvent): void => { @@ -212,7 +216,7 @@ export class KeyValueBox extends React.Component<FieldViewProps> { getFieldView = async () => { const rows = this.rows.filter(row => row.isChecked); if (rows.length > 1) { - const parent = Docs.Create.StackingDocument([], { _autoHeight: true, _width: 300, title: `field views for ${DocCast(this.props.Document.data).title}`, _chromeHidden: true }); + const parent = Docs.Create.StackingDocument([], { _layout_autoHeight: true, _width: 300, title: `field views for ${DocCast(this.props.Document.data).title}`, _chromeHidden: true }); for (const row of rows) { const field = this.createFieldView(DocCast(this.props.Document.data), row); field && Doc.AddDocToList(parent, 'data', field); @@ -225,9 +229,9 @@ export class KeyValueBox extends React.Component<FieldViewProps> { createFieldView = (templateDoc: Doc, row: KeyValuePair) => { const metaKey = row.props.keyName; - const fieldTemplate = Doc.IsDelegateField(templateDoc, metaKey) ? Doc.MakeDelegate(templateDoc) : Doc.MakeAlias(templateDoc); + const fieldTemplate = Doc.IsDelegateField(templateDoc, metaKey) ? Doc.MakeDelegate(templateDoc) : Doc.MakeEmbedding(templateDoc); fieldTemplate.title = metaKey; - fieldTemplate.fitWidth = true; + fieldTemplate.layout_fitWidth = true; fieldTemplate._xMargin = 10; fieldTemplate._yMargin = 10; fieldTemplate._width = 100; @@ -280,8 +284,8 @@ export class KeyValueBox extends React.Component<FieldViewProps> { render() { const dividerDragger = - this.splitPercentage === 0 ? null : ( - <div className="keyValueBox-dividerDragger" style={{ transform: `translate(calc(${100 - this.splitPercentage}% - 5px), 0px)` }}> + this._splitPercentage === 0 ? null : ( + <div className="keyValueBox-dividerDragger" style={{ transform: `translate(calc(${100 - this._splitPercentage}% - 5px), 0px)` }}> <div className="keyValueBox-dividerDraggerThumb" onPointerDown={this.onDividerDown} /> </div> ); @@ -291,10 +295,10 @@ export class KeyValueBox extends React.Component<FieldViewProps> { <table className="keyValueBox-table"> <tbody className="keyValueBox-tbody"> <tr className="keyValueBox-header"> - <th className="keyValueBox-key" style={{ width: `${100 - this.splitPercentage}%` }} ref={this._keyHeader} onPointerDown={SetupDrag(this._keyHeader, this.getFieldView)}> + <th className="keyValueBox-key" style={{ width: `${100 - this._splitPercentage}%` }} ref={this._keyHeader} onPointerDown={SetupDrag(this._keyHeader, this.getFieldView)}> Key </th> - <th className="keyValueBox-fields" style={{ width: `${this.splitPercentage}%` }}> + <th className="keyValueBox-fields" style={{ width: `${this._splitPercentage}%` }}> Fields </th> </tr> diff --git a/src/client/views/nodes/KeyValuePair.scss b/src/client/views/nodes/KeyValuePair.scss index 5b660e582..57d36932e 100644 --- a/src/client/views/nodes/KeyValuePair.scss +++ b/src/client/views/nodes/KeyValuePair.scss @@ -1,60 +1,52 @@ -@import "../global/globalCssVariables"; - +@import '../global/globalCssVariables'; .keyValuePair-td-key { - display:inline-block; + display: inline-block; - .keyValuePair-td-key-container{ - width:100%; - height:100%; - display: flex; - flex-direction: row; - flex-wrap: nowrap; - justify-content: space-between; - align-items: center; - .keyValuePair-td-key-delete{ + .keyValuePair-td-key-container { + width: 100%; + height: 100%; + display: flex; + flex-direction: row; + flex-wrap: nowrap; + justify-content: space-between; + .keyValuePair-td-key-delete { position: relative; background-color: transparent; - color:red; + color: red; } .keyValuePair-td-key-check { position: relative; margin: 0; } .keyValuePair-keyField { - width:100%; + width: 100%; margin-left: 20px; - margin-top: -1px; font-family: monospace; - // text-align: center; - align-self: center; position: relative; overflow: auto; + display: inline; } } } .keyValuePair-td-value { - display:inline-block; + display: inline-block; overflow: scroll; font-family: monospace; height: 30px; - .keyValuePair-td-value-container { - display: flex; - align-items: center; - align-content: center; - flex-direction: row; - justify-content: space-between; - flex-wrap: nowrap; - width: 100%; - height: 100%; + .keyValuePair-td-value-container { + display: inline; + justify-content: space-between; + width: 100%; + height: 100%; - img { - max-height: 36px; - width: auto; - } - .videoBox-cont{ - width: auto; - max-height: 36px; - } + img { + max-height: 36px; + width: auto; + } + .videoBox-cont { + width: auto; + max-height: 36px; + } } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/KeyValuePair.tsx b/src/client/views/nodes/KeyValuePair.tsx index 7ea6d42ff..64f25cb22 100644 --- a/src/client/views/nodes/KeyValuePair.tsx +++ b/src/client/views/nodes/KeyValuePair.tsx @@ -13,6 +13,7 @@ import { KeyValueBox } from './KeyValueBox'; import './KeyValueBox.scss'; import './KeyValuePair.scss'; import React = require('react'); +import { DocCast } from '../../../fields/Types'; // Represents one row in a key value plane @@ -56,8 +57,8 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { const props: FieldViewProps = { Document: this.props.doc, DataDoc: this.props.doc, - docFilters: returnEmptyFilter, - docRangeFilters: returnEmptyFilter, + childFilters: returnEmptyFilter, + childFiltersByRanges: returnEmptyFilter, searchFilterDocs: returnEmptyDoclist, styleProvider: DefaultStyleProvider, docViewPath: returnEmptyDoclist, @@ -66,7 +67,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { isSelected: returnFalse, setHeight: returnFalse, select: emptyFunction, - dropAction: 'alias', + dropAction: 'embed', bringToFront: emptyFunction, renderDepth: 1, isContentActive: returnFalse, @@ -87,7 +88,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { break; } protoCount++; - doc = doc.proto; + doc = DocCast(doc.proto); } const parenCount = Math.max(0, protoCount - 1); const keyStyle = protoCount === 0 ? 'black' : 'blue'; @@ -104,12 +105,12 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { onClick={undoBatch(() => { if (Object.keys(props.Document).indexOf(props.fieldKey) !== -1) { delete props.Document[props.fieldKey]; - } else delete props.Document.proto![props.fieldKey]; + } else delete DocCast(props.Document.proto)?.[props.fieldKey]; })}> X </button> - <input className={'keyValuePair-td-key-check'} type="checkbox" style={hover} onChange={this.handleCheck} ref={this.checkbox} /> - <div className="keyValuePair-keyField" style={{ color: keyStyle }}> + <input className="keyValuePair-td-key-check" type="checkbox" style={hover} onChange={this.handleCheck} ref={this.checkbox} /> + <div className="keyValuePair-keyField" style={{ marginLeft: 35 * (props.fieldKey.match(/_/g)?.length || 0), color: keyStyle }}> {'('.repeat(parenCount)} {props.fieldKey} {')'.repeat(parenCount)} @@ -118,13 +119,7 @@ export class KeyValuePair extends React.Component<KeyValuePairProps> { </td> <td className="keyValuePair-td-value" style={{ width: `${100 - this.props.keyWidth}%` }} onContextMenu={this.onContextMenu}> <div className="keyValuePair-td-value-container"> - <EditableView - contents={contents} - maxHeight={36} - height={'auto'} - GetValue={() => Field.toKeyValueString(props.Document, props.fieldKey)} - SetValue={(value: string) => KeyValueBox.SetField(props.Document, props.fieldKey, value)} - /> + <EditableView contents={contents} GetValue={() => Field.toKeyValueString(props.Document, props.fieldKey)} SetValue={(value: string) => KeyValueBox.SetField(props.Document, props.fieldKey, value)} /> </div> </td> </tr> diff --git a/src/client/views/nodes/LabelBox.tsx b/src/client/views/nodes/LabelBox.tsx index 916458dfd..52f3575cb 100644 --- a/src/client/views/nodes/LabelBox.tsx +++ b/src/client/views/nodes/LabelBox.tsx @@ -37,7 +37,7 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps & LabelBoxProp } getTitle() { - return this.rootDoc['title-custom'] ? StrCast(this.rootDoc.title) : this.props.label ? this.props.label : typeof this.rootDoc[this.fieldKey] === 'string' ? StrCast(this.rootDoc[this.fieldKey]) : StrCast(this.rootDoc.title); + return this.rootDoc.title_custom ? StrCast(this.rootDoc.title) : this.props.label ? this.props.label : typeof this.rootDoc[this.fieldKey] === 'string' ? StrCast(this.rootDoc[this.fieldKey]) : StrCast(this.rootDoc.title); } protected createDropTarget = (ele: HTMLDivElement) => { @@ -82,13 +82,13 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps & LabelBoxProp return this._mouseOver ? StrCast(this.layoutDoc._hoverBackgroundColor) : this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.BackgroundColor); } - fitTextToBox = (r: any): any => { + fitTextToBox = (r: any) => { const singleLine = BoolCast(this.rootDoc._singleLine, true); const params = { rotateText: null, fontSizeFactor: 1, - minimumFontSize: NumCast(this.rootDoc._minFontSize, 8), - maximumFontSize: NumCast(this.rootDoc._maxFontSize, 1000), + minimumFontSize: NumCast(this.rootDoc._label_minFontSize, 8), + maximumFontSize: NumCast(this.rootDoc._label_maxFontSize, 1000), limitingDimension: 'both', horizontalAlign: 'center', verticalAlign: 'center', @@ -130,9 +130,9 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps & LabelBoxProp className="labelBox-mainButton" style={{ backgroundColor: this.hoverColor, - fontSize: StrCast(this.layoutDoc._fontSize), + fontSize: StrCast(this.layoutDoc._text_fontSize), color: StrCast(this.layoutDoc._color), - fontFamily: StrCast(this.layoutDoc._fontFamily) || 'inherit', + fontFamily: StrCast(this.layoutDoc._text_fontFamily) || 'inherit', letterSpacing: StrCast(this.layoutDoc.letterSpacing), textTransform: StrCast(this.layoutDoc.textTransform) as any, paddingLeft: NumCast(this.rootDoc._xPadding), @@ -141,9 +141,9 @@ export class LabelBox extends ViewBoxBaseComponent<FieldViewProps & LabelBoxProp paddingBottom: NumCast(this.rootDoc._yPadding), width: this.props.PanelWidth(), height: this.props.PanelHeight(), - whiteSpace: boxParams.singleLine ? 'pre' : 'pre-wrap', + whiteSpace: typeof boxParams !== 'number' && boxParams.singleLine ? 'pre' : 'pre-wrap', }}> - <span style={{ width: boxParams.singleLine ? '' : '100%' }} ref={action((r: any) => this.fitTextToBox(r))}> + <span style={{ width: typeof boxParams !== 'number' && boxParams.singleLine ? '' : '100%' }} ref={action((r: any) => this.fitTextToBox(r))}> {label.startsWith('#') ? null : label.replace(/([^a-zA-Z])/g, '$1\u200b')} </span> </div> diff --git a/src/client/views/nodes/LinkAnchorBox.tsx b/src/client/views/nodes/LinkAnchorBox.tsx index 31f1775e5..e86b881a8 100644 --- a/src/client/views/nodes/LinkAnchorBox.tsx +++ b/src/client/views/nodes/LinkAnchorBox.tsx @@ -46,14 +46,14 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() { const separation = Math.sqrt((pt[0] - e.clientX) * (pt[0] - e.clientX) + (pt[1] - e.clientY) * (pt[1] - e.clientY)); if (separation > 100) { const dragData = new DragManager.DocumentDragData([this.rootDoc]); - dragData.dropAction = 'alias'; - dragData.removeDropProperties = ['anchor1_x', 'anchor1_y', 'anchor2_x', 'anchor2_y', 'onClick']; + dragData.dropAction = 'embed'; + dragData.removeDropProperties = ['link_anchor_1_x', 'link_anchor_1_y', 'link_anchor_2_x', 'link_anchor_2_y', 'onClick']; DragManager.StartDocumentDrag([this._ref.current!], dragData, pt[0], pt[1]); return true; } else { this.rootDoc[this.fieldKey + '_x'] = ((pt[0] - bounds.left) / bounds.width) * 100; this.rootDoc[this.fieldKey + '_y'] = ((pt[1] - bounds.top) / bounds.height) * 100; - this.rootDoc.linkAutoMove = false; + this.rootDoc.link_autoMoveAnchors = false; } } return false; @@ -67,10 +67,14 @@ export class LinkAnchorBox extends ViewBoxBaseComponent<FieldViewProps>() { const x = NumCast(this.rootDoc[this.fieldKey + '_x'], 100); const y = NumCast(this.rootDoc[this.fieldKey + '_y'], 100); const background = this.props.styleProvider?.(this.dataDoc, this.props, StyleProp.BackgroundColor + ':anchor'); - const anchor = this.fieldKey === 'anchor1' ? 'anchor2' : 'anchor1'; - const anchorScale = !this.dataDoc[this.fieldKey + '-useLinkSmallAnchor'] && (x === 0 || x === 100 || y === 0 || y === 100) ? 1 : 0.25; + const anchor = this.fieldKey === 'link_anchor_1' ? 'link_anchor_2' : 'link_anchor_1'; + const anchorScale = !this.dataDoc[this.fieldKey + '_useSmallAnchor'] && (x === 0 || x === 100 || y === 0 || y === 100) ? 1 : 0.25; const targetTitle = StrCast((this.dataDoc[anchor] as Doc)?.title); - const selView = SelectionManager.Views().lastElement()?.props.LayoutTemplateString?.includes('anchor1') ? 'anchor1' : SelectionManager.Views().lastElement()?.props.LayoutTemplateString?.includes('anchor2') ? 'anchor2' : ''; + const selView = SelectionManager.Views().lastElement()?.props.LayoutTemplateString?.includes('link_anchor_1') + ? 'link_anchor_1' + : SelectionManager.Views().lastElement()?.props.LayoutTemplateString?.includes('link_anchor_2') + ? 'link_anchor_2' + : ''; return ( <div ref={this._ref} diff --git a/src/client/views/nodes/LinkBox.tsx b/src/client/views/nodes/LinkBox.tsx index 46ccdecae..710d41471 100644 --- a/src/client/views/nodes/LinkBox.tsx +++ b/src/client/views/nodes/LinkBox.tsx @@ -9,7 +9,7 @@ import './LinkBox.scss'; @observer export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { - public static LayoutString(fieldKey: string) { + public static LayoutString(fieldKey: string = 'link') { return FieldView.LayoutString(LinkBox, fieldKey); } @@ -23,7 +23,7 @@ export class LinkBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className={`linkBox-container${this.props.isContentActive() ? '-interactive' : ''}`} style={{ background: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor) }}> <ComparisonBox {...this.props} - fieldKey="anchor" + fieldKey="link_anchor" setHeight={emptyFunction} dontRegisterView={true} renderDepth={this.props.renderDepth + 1} diff --git a/src/client/views/nodes/LinkDescriptionPopup.tsx b/src/client/views/nodes/LinkDescriptionPopup.tsx index 91bd505c5..c45045a8a 100644 --- a/src/client/views/nodes/LinkDescriptionPopup.tsx +++ b/src/client/views/nodes/LinkDescriptionPopup.tsx @@ -24,7 +24,7 @@ export class LinkDescriptionPopup extends React.Component<{}> { onDismiss = (add: boolean) => { LinkDescriptionPopup.descriptionPopup = false; if (add) { - LinkManager.currentLink && (Doc.GetProto(LinkManager.currentLink).description = this.description); + LinkManager.currentLink && (Doc.GetProto(LinkManager.currentLink).link_description = this.description); } }; diff --git a/src/client/views/nodes/LinkDocPreview.tsx b/src/client/views/nodes/LinkDocPreview.tsx index c58b5dd8c..86191de63 100644 --- a/src/client/views/nodes/LinkDocPreview.tsx +++ b/src/client/views/nodes/LinkDocPreview.tsx @@ -3,11 +3,12 @@ import { Tooltip } from '@material-ui/core'; import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import wiki from 'wikijs'; -import { Doc, DocCastAsync, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; +import { Doc, DocCastAsync, Opt } from '../../../fields/Doc'; +import { Height, Width } from '../../../fields/DocSymbols'; import { Cast, DocCast, NumCast, PromiseValue, StrCast } from '../../../fields/Types'; import { emptyFunction, returnEmptyDoclist, returnEmptyFilter, returnEmptyString, returnFalse, returnNone, setupMoveUpEvents } from '../../../Utils'; import { DocServer } from '../../DocServer'; -import { Docs, DocUtils } from '../../documents/Documents'; +import { Docs } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; import { DragManager } from '../../util/DragManager'; import { LinkFollower } from '../../util/LinkFollower'; @@ -58,13 +59,13 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { var linkTarget = this.props.linkDoc; this._linkSrc = this.props.linkSrc; this._linkDoc = this.props.linkDoc; - const anchor1 = this._linkDoc?.anchor1 as Doc; - const anchor2 = this._linkDoc?.anchor2 as Doc; - if (anchor1 && anchor2) { - linkTarget = Doc.AreProtosEqual(anchor1, this._linkSrc) || Doc.AreProtosEqual(anchor1?.annotationOn as Doc, this._linkSrc) ? anchor2 : anchor1; + const link_anchor_1 = this._linkDoc?.link_anchor_1 as Doc; + const link_anchor_2 = this._linkDoc?.link_anchor_2 as Doc; + if (link_anchor_1 && link_anchor_2) { + linkTarget = Doc.AreProtosEqual(link_anchor_1, this._linkSrc) || Doc.AreProtosEqual(link_anchor_1?.annotationOn as Doc, this._linkSrc) ? link_anchor_2 : link_anchor_1; } if (linkTarget?.annotationOn && linkTarget?.type !== DocumentType.RTF) { - // want to show annotation context document if annotation is not text + // want to show annotation embedContainer document if annotation is not text linkTarget && DocCastAsync(linkTarget.annotationOn).then(action(anno => (this._markerTargetDoc = this._targetDoc = anno))); } else { this._markerTargetDoc = this._targetDoc = linkTarget; @@ -111,7 +112,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { action(anchor => { if (anchor instanceof Doc && LinkManager.Links(anchor).length) { this._linkDoc = this._linkDoc ?? LinkManager.Links(anchor)[0]; - const automaticLink = this._linkDoc.linkRelationship === LinkManager.AutoKeywords; + const automaticLink = this._linkDoc.link_relationship === LinkManager.AutoKeywords; if (automaticLink) { // automatic links specify the target in the link info, not the source const linkTarget = anchor; @@ -123,7 +124,6 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { this._markerTargetDoc = linkTarget; this._targetDoc = /*linkTarget?.type === DocumentType.MARKER &&*/ linkTarget?.annotationOn ? Cast(linkTarget.annotationOn, Doc, null) ?? linkTarget : linkTarget; } - this._toolTipText = 'link to ' + this._targetDoc?.title; if (LinkDocPreview.LinkInfo?.noPreview || this._linkSrc?.followLinkToggle || this._markerTargetDoc?.type === DocumentType.PRES) this.followLink(); } }) @@ -175,7 +175,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { } else if (this.props.hrefs?.length) { const webDoc = Array.from(SearchBox.staticSearchCollection(Doc.MyFilesystem, this.props.hrefs[0]).keys()).lastElement() ?? - Docs.Create.WebDocument(this.props.hrefs[0], { title: this.props.hrefs[0], _nativeWidth: 850, _width: 200, _height: 400, useCors: true }); + Docs.Create.WebDocument(this.props.hrefs[0], { title: this.props.hrefs[0], _nativeWidth: 850, _width: 200, _height: 400, data_useCors: true }); this.props.docProps?.addDocTab(webDoc, OpenWhere.lightbox); } }; @@ -184,17 +184,17 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { width = () => { if (!this._targetDoc) return 225; - if (this._targetDoc[WidthSym]() < this._targetDoc?.[HeightSym]()) { - return (Math.min(225, this._targetDoc[HeightSym]()) * this._targetDoc[WidthSym]()) / this._targetDoc[HeightSym](); + if (this._targetDoc[Width]() < this._targetDoc?.[Height]()) { + return (Math.min(225, this._targetDoc[Height]()) * this._targetDoc[Width]()) / this._targetDoc[Height](); } - return Math.min(225, NumCast(this._targetDoc?.[WidthSym](), 225)); + return Math.min(225, NumCast(this._targetDoc?.[Width](), 225)); }; height = () => { if (!this._targetDoc) return 225; - if (this._targetDoc[WidthSym]() > this._targetDoc?.[HeightSym]()) { - return (Math.min(225, this._targetDoc[WidthSym]()) * this._targetDoc[HeightSym]()) / this._targetDoc[WidthSym](); + if (this._targetDoc[Width]() > this._targetDoc?.[Height]()) { + return (Math.min(225, this._targetDoc[Width]()) * this._targetDoc[Height]()) / this._targetDoc[Width](); } - return Math.min(225, NumCast(this._targetDoc?.[HeightSym](), 225)); + return Math.min(225, NumCast(this._targetDoc?.[Height](), 225)); }; @computed get previewHeader() { return !this._linkDoc || !this._markerTargetDoc || !this._targetDoc || !this._linkSrc ? null : ( @@ -208,7 +208,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { </div> <div className="linkDocPreview-title" style={{ pointerEvents: 'all' }}> {StrCast(this._markerTargetDoc.title).length > 16 ? StrCast(this._markerTargetDoc.title).substr(0, 16) + '...' : StrCast(this._markerTargetDoc.title)} - <p className="linkDocPreview-description"> {StrCast(this._linkDoc.description)}</p> + <p className="linkDocPreview-description"> {StrCast(this._linkDoc.link_description)}</p> </div> <div className="linkDocPreview-buttonBar" style={{ float: 'right' }}> <Tooltip title={<div className="dash-tooltip">Next Link</div>} placement="top"> @@ -263,13 +263,13 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { isDocumentActive={returnFalse} isContentActive={returnFalse} addDocument={returnFalse} - showTitle={returnEmptyString} + layout_showTitle={returnEmptyString} removeDocument={returnFalse} addDocTab={returnFalse} pinToPres={returnFalse} dontRegisterView={true} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} renderDepth={0} suppressSetHeight={true} @@ -278,7 +278,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { pointerEvents={returnNone} focus={emptyFunction} whenChildContentsActiveChanged={returnFalse} - ignoreAutoHeight={true} // need to ignore autoHeight otherwise autoHeight text boxes will expand beyond the preview panel size. + ignoreAutoHeight={true} // need to ignore layout_autoHeight otherwise layout_autoHeight text boxes will expand beyond the preview panel size. bringToFront={returnFalse} NativeWidth={Doc.NativeWidth(this._targetDoc) ? () => Doc.NativeWidth(this._targetDoc) : undefined} NativeHeight={Doc.NativeHeight(this._targetDoc) ? () => Doc.NativeHeight(this._targetDoc) : undefined} @@ -296,7 +296,7 @@ export class LinkDocPreview extends React.Component<LinkDocPreviewProps> { className="linkDocPreview" ref={this._linkDocRef} onPointerDown={this.followLinkPointerDown} - style={{ display: !this._toolTipText ? 'none' : undefined, left: this.props.location[0], top: this.props.location[1], width: this.width() + borders, height: this.height() + borders + (this.props.showHeader ? 37 : 0) }}> + style={{ left: this.props.location[0], top: this.props.location[1], width: this.width() + borders, height: this.height() + borders + (this.props.showHeader ? 37 : 0) }}> {this.docPreview} </div> ); diff --git a/src/client/views/nodes/LoadingBox.tsx b/src/client/views/nodes/LoadingBox.tsx index 8c5255f80..fcbd0128d 100644 --- a/src/client/views/nodes/LoadingBox.tsx +++ b/src/client/views/nodes/LoadingBox.tsx @@ -8,6 +8,7 @@ import { Networking } from '../../Network'; import { ViewBoxAnnotatableComponent } from '../DocComponent'; import { FieldView, FieldViewProps } from './FieldView'; import './LoadingBox.scss'; +import { Id } from '../../../fields/FieldSymbols'; /** * LoadingBox Class represents a placeholder doc for documents that are currently @@ -43,7 +44,7 @@ export class LoadingBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { this.rootDoc.loadingError = 'Upload interrupted, please try again'; } else { const updateFunc = async () => { - const result = await Networking.QueryYoutubeProgress(StrCast(this.rootDoc.title)); + const result = await Networking.QueryYoutubeProgress(StrCast(this.rootDoc[Id])); // We use the guid of the overwriteDoc to track file uploads. runInAction(() => (this.progress = result.progress)); this._timer = setTimeout(updateFunc, 1000); }; diff --git a/src/client/views/nodes/MapBox/MapBox.tsx b/src/client/views/nodes/MapBox/MapBox.tsx index 36be7d257..93e54ffb7 100644 --- a/src/client/views/nodes/MapBox/MapBox.tsx +++ b/src/client/views/nodes/MapBox/MapBox.tsx @@ -4,7 +4,8 @@ import BingMapsReact from 'bingmaps-react'; import { action, computed, IReactionDisposer, observable, ObservableMap, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { Doc, DocListCast, Opt, WidthSym } from '../../../../fields/Doc'; +import { Doc, DocListCast, Opt } from '../../../../fields/Doc'; +import { Width } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { InkTool } from '../../../../fields/InkField'; import { NumCast, StrCast } from '../../../../fields/Types'; @@ -98,7 +99,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return FieldView.LayoutString(MapBox, fieldKey); } public get SidebarKey() { - return this.fieldKey + '-sidebar'; + return this.fieldKey + '_sidebar'; } private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean) => void); @computed get inlineTextAnnotations() { @@ -126,7 +127,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @observable _showSidebar = false; @computed get SidebarShown() { - return this._showSidebar || this.layoutDoc._showSidebar ? true : false; + return this._showSidebar || this.layoutDoc._layout_showSidebar ? true : false; } static _canAnnotate = true; @@ -267,7 +268,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps setTimeout(() => { if (this._loadPending && this._map.getBounds()) { this._loadPending = false; - this.layoutDoc.fitContentsToBox && this.fitBounds(this._map); + this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map); } }, 250); // listener to addmarker event @@ -282,7 +283,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps centered = () => { if (this._loadPending && this._map.getBounds()) { this._loadPending = false; - this.layoutDoc.fitContentsToBox && this.fitBounds(this._map); + this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map); } this.dataDoc.mapLat = this._map.getCenter()?.lat(); this.dataDoc.mapLng = this._map.getCenter()?.lng(); @@ -292,7 +293,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps zoomChanged = () => { if (this._loadPending && this._map.getBounds()) { this._loadPending = false; - this.layoutDoc.fitContentsToBox && this.fitBounds(this._map); + this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map); } this.dataDoc.mapZoom = this._map.getZoom(); }; @@ -327,7 +328,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps */ sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => { console.log('print all sidebar Docs'); - if (!this.layoutDoc._showSidebar) this.toggleSidebar(); + if (!this.layoutDoc._layout_showSidebar) this.toggleSidebar(); const docs = doc instanceof Doc ? [doc] : doc; docs.forEach(doc => { if (doc.lat !== undefined && doc.lng !== undefined) { @@ -351,7 +352,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps * @returns */ sidebarRemoveDocument = (doc: Doc | Doc[], sidebarKey?: string) => { - if (this.layoutDoc._showSidebar) this.toggleSidebar(); + if (this.layoutDoc._layout_showSidebar) this.toggleSidebar(); const docs = doc instanceof Doc ? [doc] : doc; return this.removeDocument(doc, sidebarKey); }; @@ -370,16 +371,16 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps .ScreenToLocalTransform() .scale(this.props.NativeDimScaling?.() || 1) .transformDirection(delta[0], delta[1]); - const fullWidth = this.layoutDoc[WidthSym](); + const fullWidth = this.layoutDoc[Width](); const mapWidth = fullWidth - this.sidebarWidth(); if (this.sidebarWidth() + localDelta[0] > 0) { this._showSidebar = true; this.layoutDoc._width = fullWidth + localDelta[0]; - this.layoutDoc._sidebarWidthPercent = ((100 * (this.sidebarWidth() + localDelta[0])) / (fullWidth + localDelta[0])).toString() + '%'; + this.layoutDoc._layout_sidebarWidthPercent = ((100 * (this.sidebarWidth() + localDelta[0])) / (fullWidth + localDelta[0])).toString() + '%'; } else { this._showSidebar = false; this.layoutDoc._width = mapWidth; - this.layoutDoc._sidebarWidthPercent = '0%'; + this.layoutDoc._layout_sidebarWidthPercent = '0%'; } return false; }), @@ -388,12 +389,12 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps ); }; - sidebarWidth = () => (Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100) * this.props.PanelWidth(); - @computed get sidebarWidthPercent() { - return StrCast(this.layoutDoc._sidebarWidthPercent, '0%'); + sidebarWidth = () => (Number(this.layout_sidebarWidthPercent.substring(0, this.layout_sidebarWidthPercent.length - 1)) / 100) * this.props.PanelWidth(); + @computed get layout_sidebarWidthPercent() { + return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); } @computed get sidebarColor() { - return StrCast(this.layoutDoc.sidebarColor, StrCast(this.layoutDoc[this.props.fieldKey + '-backgroundColor'], '#e4e4e4')); + return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this.props.fieldKey + '_backgroundColor'], '#e4e4e4')); } /** @@ -453,7 +454,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps title="Toggle Sidebar" style={{ display: !this.props.isContentActive() ? 'none' : undefined, - top: StrCast(this.rootDoc._showTitle) === 'title' ? 20 : 5, + top: StrCast(this.rootDoc._layout_showTitle) === 'title' ? 20 : 5, backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, }} onPointerDown={this.sidebarBtnDown}> @@ -467,8 +468,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps toggleSidebar = () => { //1.2 * w * ? = .2 * w .2/1.2 const prevWidth = this.sidebarWidth(); - this.layoutDoc._showSidebar = (this.layoutDoc._sidebarWidthPercent = StrCast(this.layoutDoc._sidebarWidthPercent, '0%') === '0%' ? `${(100 * 0.2) / 1.2}%` : '0%') !== '0%'; - this.layoutDoc._width = this.layoutDoc._showSidebar ? NumCast(this.layoutDoc._width) * 1.2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth); + this.layoutDoc._layout_showSidebar = (this.layoutDoc._layout_sidebarWidthPercent = StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%') === '0%' ? `${(100 * 0.2) / 1.2}%` : '0%') !== '0%'; + this.layoutDoc._width = this.layoutDoc._layout_showSidebar ? NumCast(this.layoutDoc._width) * 1.2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth); }; sidebarDown = (e: React.PointerEvent) => { @@ -476,8 +477,8 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }; sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => { const bounds = this._ref.current!.getBoundingClientRect(); - this.layoutDoc._sidebarWidthPercent = '' + 100 * Math.max(0, 1 - (e.clientX - bounds.left) / bounds.width) + '%'; - this.layoutDoc._showSidebar = this.layoutDoc._sidebarWidthPercent !== '0%'; + this.layoutDoc._layout_sidebarWidthPercent = '' + 100 * Math.max(0, 1 - (e.clientX - bounds.left) / bounds.width) + '%'; + this.layoutDoc._layout_showSidebar = this.layoutDoc._layout_sidebarWidthPercent !== '0%'; e.preventDefault(); return false; }; @@ -548,9 +549,9 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1) - this.sidebarWidth(); panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); - scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._scrollTop)); - transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()]; - opaqueFilter = () => [...this.props.docFilters(), Utils.IsOpaqueFilter()]; + scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._layout_scrollTop)); + transparentFilter = () => [...this.props.childFilters(), Utils.IsTransparentFilter()]; + opaqueFilter = () => [...this.props.childFilters(), Utils.IsOpaqueFilter()]; infoWidth = () => this.props.PanelWidth() / 5; infoHeight = () => this.props.PanelHeight() / 5; anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick; @@ -600,7 +601,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }; bingMapReady = (map: any) => (this._bingMap = map.map); render() { - const renderAnnotations = (docFilters?: () => string[]) => null; + const renderAnnotations = (childFilters?: () => string[]) => null; return ( <div className="mapBox" ref={this._ref}> <div @@ -616,7 +617,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps // zoom: 15, // }); }} - style={{ width: `calc(100% - ${this.sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}> + style={{ width: `calc(100% - ${this.layout_sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}> <div style={{ mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div> {renderAnnotations(this.opaqueFilter)} {SnappingManager.GetIsDragging() ? null : renderAnnotations()} @@ -667,7 +668,7 @@ export class MapBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps )} </div> {/* </LoadScript > */} - <div className="mapBox-sidebar" style={{ position: 'absolute', right: 0, height: '100%', width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> + <div className="mapBox-sidebar" style={{ position: 'absolute', right: 0, height: '100%', width: `${this.layout_sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> <SidebarAnnos ref={this._sidebarRef} {...this.props} diff --git a/src/client/views/nodes/MapBox/MapBox2.tsx b/src/client/views/nodes/MapBox/MapBox2.tsx new file mode 100644 index 000000000..72f37b62c --- /dev/null +++ b/src/client/views/nodes/MapBox/MapBox2.tsx @@ -0,0 +1,642 @@ +import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; +import { Autocomplete, GoogleMap, GoogleMapProps, Marker } from '@react-google-maps/api'; +import { action, computed, IReactionDisposer, observable, ObservableMap, runInAction } from 'mobx'; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import { Doc, DocListCast, Opt } from '../../../../fields/Doc'; +import { Width } from '../../../../fields/DocSymbols'; +import { Id } from '../../../../fields/FieldSymbols'; +import { InkTool } from '../../../../fields/InkField'; +import { NumCast, StrCast } from '../../../../fields/Types'; +import { emptyFunction, returnEmptyString, returnFalse, returnOne, setupMoveUpEvents, Utils } from '../../../../Utils'; +import { Docs } from '../../../documents/Documents'; +import { DragManager } from '../../../util/DragManager'; +import { SnappingManager } from '../../../util/SnappingManager'; +import { UndoManager } from '../../../util/UndoManager'; +import { MarqueeOptionsMenu } from '../../collections/collectionFreeForm'; +import { ViewBoxAnnotatableComponent, ViewBoxAnnotatableProps } from '../../DocComponent'; +import { Colors } from '../../global/globalEnums'; +import { MarqueeAnnotator } from '../../MarqueeAnnotator'; +import { AnchorMenu } from '../../pdf/AnchorMenu'; +import { Annotation } from '../../pdf/Annotation'; +import { SidebarAnnos } from '../../SidebarAnnos'; +import { FieldView, FieldViewProps } from '../FieldView'; +import { PinProps } from '../trails'; +import './MapBox2.scss'; +import { MapBoxInfoWindow } from './MapBoxInfoWindow'; + +/** + * MapBox2 architecture: + * Main component: MapBox2.tsx + * Supporting Components: SidebarAnnos, CollectionStackingView + * + * MapBox2 is a node that extends the ViewBoxAnnotatableComponent. Similar to PDFBox and WebBox, it supports interaction between sidebar content and document content. + * The main body of MapBox2 uses Google Maps API to allow location retrieval, adding map markers, pan and zoom, and open street view. + * Dash Document architecture is integrated with Maps API: When drag and dropping documents with ExifData (gps Latitude and Longitude information) available, + * sidebarAddDocument function checks if the document contains lat & lng information, if it does, then the document is added to both the sidebar and the infowindow (a pop up corresponding to a map marker--pin on map). + * The lat and lng field of the document is filled when importing (spec see ConvertDMSToDD method and processFileUpload method in Documents.ts). + * A map marker is considered a document that contains a collection with stacking view of documents, it has a lat, lng location, which is passed to Maps API's custom marker (red pin) to be rendered on the google maps + */ + +// const _global = (window /* browser */ || global /* node */) as any; + +const mapContainerStyle = { + height: '100%', +}; + +const defaultCenter = { + lat: 42.360081, + lng: -71.058884, +}; + +const mapOptions = { + fullscreenControl: false, +}; + +const apiKey = process.env.GOOGLE_MAPS; + +const script = document.createElement('script'); +script.defer = true; +script.async = true; +script.src = `https://maps.googleapis.com/maps/api/js?key=${apiKey}&libraries=places,drawing`; +console.log(script.src); +document.head.appendChild(script); + +/** + * Consider integrating later: allows for drawing, circling, making shapes on map + */ +// const drawingManager = new window.google.maps.drawing.DrawingManager({ +// drawingControl: true, +// drawingControlOptions: { +// position: google.maps.ControlPosition.TOP_RIGHT, +// drawingModes: [ +// google.maps.drawing.OverlayType.MARKER, +// // currently we are not supporting the following drawing mode on map, a thought for future development +// google.maps.drawing.OverlayType.CIRCLE, +// google.maps.drawing.OverlayType.POLYLINE, +// ], +// }, +// }); + +// options for searchbox in Google Maps Places Autocomplete API +const options = { + fields: ['formatted_address', 'geometry', 'name'], // note: level of details is charged by item per retrieval, not recommended to return all fields + strictBounds: false, + types: ['establishment'], // type pf places, subject of change according to user need +} as google.maps.places.AutocompleteOptions; + +@observer +export class MapBox2 extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps & FieldViewProps & Partial<GoogleMapProps>>() { + private _dropDisposer?: DragManager.DragDropDisposer; + private _disposers: { [name: string]: IReactionDisposer } = {}; + private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); + @observable private _overlayAnnoInfo: Opt<Doc>; + showInfo = action((anno: Opt<Doc>) => (this._overlayAnnoInfo = anno)); + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(MapBox2, fieldKey); + } + public get SidebarKey() { + return this.fieldKey + '_sidebar'; + } + private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean) => void); + @computed get inlineTextAnnotations() { + return this.allMapMarkers.filter(a => a.textInlineAnnotations); + } + + @observable private _map: google.maps.Map = null as unknown as google.maps.Map; + @observable private selectedPlace: Doc | undefined; + @observable private markerMap: { [id: string]: google.maps.Marker } = {}; + @observable private center = navigator.geolocation ? navigator.geolocation.getCurrentPosition : defaultCenter; + @observable private _marqueeing: number[] | undefined; + @observable private _isAnnotating = false; + @observable private inputRef = React.createRef<HTMLInputElement>(); + @observable private searchMarkers: google.maps.Marker[] = []; + @observable private searchBox = new window.google.maps.places.Autocomplete(this.inputRef.current!, options); + @observable private _savedAnnotations = new ObservableMap<number, HTMLDivElement[]>(); + @computed get allSidebarDocs() { + return DocListCast(this.dataDoc[this.SidebarKey]); + } + @computed get allMapMarkers() { + return DocListCast(this.dataDoc[this.annotationKey]); + } + @observable private toggleAddMarker = false; + private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); + + @observable _showSidebar = false; + @computed get SidebarShown() { + return this._showSidebar || this.layoutDoc._layout_showSidebar ? true : false; + } + + static _canAnnotate = true; + static _hadSelection: boolean = false; + private _sidebarRef = React.createRef<SidebarAnnos>(); + private _ref: React.RefObject<HTMLDivElement> = React.createRef(); + + componentDidMount() { + this.props.setContentView?.(this); + } + + @action + private setSearchBox = (searchBox: any) => { + this.searchBox = searchBox; + }; + + // iterate allMarkers to size, center, and zoom map to contain all markers + private fitBounds = (map: google.maps.Map) => { + const curBounds = map.getBounds() ?? new window.google.maps.LatLngBounds(); + const isFitting = this.allMapMarkers.reduce((fits, place) => fits && curBounds?.contains({ lat: NumCast(place.lat), lng: NumCast(place.lng) }), true as boolean); + !isFitting && map.fitBounds(this.allMapMarkers.reduce((bounds, place) => bounds.extend({ lat: NumCast(place.lat), lng: NumCast(place.lng) }), new window.google.maps.LatLngBounds())); + }; + + /** + * Custom control for add marker button + * @param controlDiv + * @param map + */ + private CenterControl = () => { + const controlDiv = document.createElement('div'); + controlDiv.className = 'MapBox2-addMarker'; + // Set CSS for the control border. + const controlUI = document.createElement('div'); + controlUI.style.backgroundColor = '#fff'; + controlUI.style.borderRadius = '3px'; + controlUI.style.cursor = 'pointer'; + controlUI.style.marginTop = '10px'; + controlUI.style.borderRadius = '4px'; + controlUI.style.marginBottom = '22px'; + controlUI.style.textAlign = 'center'; + controlUI.style.position = 'absolute'; + controlUI.style.width = '32px'; + controlUI.style.height = '32px'; + controlUI.title = 'Click to toggle marker mode. In marker mode, click on map to place a marker.'; + + const plIcon = document.createElement('img'); + plIcon.src = 'https://cdn4.iconfinder.com/data/icons/wirecons-free-vector-icons/32/add-256.png'; + plIcon.style.color = 'rgb(25,25,25)'; + plIcon.style.fontFamily = 'Roboto,Arial,sans-serif'; + plIcon.style.fontSize = '16px'; + plIcon.style.lineHeight = '32px'; + plIcon.style.left = '18'; + plIcon.style.top = '15'; + plIcon.style.position = 'absolute'; + plIcon.width = 14; + plIcon.height = 14; + plIcon.innerHTML = 'Add'; + controlUI.appendChild(plIcon); + + // Set CSS for the control interior. + const markerIcon = document.createElement('img'); + markerIcon.src = 'https://cdn0.iconfinder.com/data/icons/small-n-flat/24/678111-map-marker-1024.png'; + markerIcon.style.color = 'rgb(25,25,25)'; + markerIcon.style.fontFamily = 'Roboto,Arial,sans-serif'; + markerIcon.style.fontSize = '16px'; + markerIcon.style.lineHeight = '32px'; + markerIcon.style.left = '-2'; + markerIcon.style.top = '1'; + markerIcon.width = 30; + markerIcon.height = 30; + markerIcon.style.position = 'absolute'; + markerIcon.innerHTML = 'Add'; + controlUI.appendChild(markerIcon); + + // Setup the click event listeners + controlUI.addEventListener('click', () => { + if (this.toggleAddMarker === true) { + this.toggleAddMarker = false; + console.log('add marker button status:' + this.toggleAddMarker); + controlUI.style.backgroundColor = '#fff'; + markerIcon.style.color = 'rgb(25,25,25)'; + } else { + this.toggleAddMarker = true; + console.log('add marker button status:' + this.toggleAddMarker); + controlUI.style.backgroundColor = '#4476f7'; + markerIcon.style.color = 'rgb(255,255,255)'; + } + }); + controlDiv.appendChild(controlUI); + return controlDiv; + }; + + /** + * Place the marker on google maps & store the empty marker as a MapMarker Document in allMarkers list + * @param position - the LatLng position where the marker is placed + * @param map + */ + @action + private placeMarker = (position: google.maps.LatLng, map: google.maps.Map) => { + const marker = new google.maps.Marker({ + position: position, + map: map, + }); + map.panTo(position); + const mapMarker = Docs.Create.MapMarkerDocument(NumCast(position.lat()), NumCast(position.lng()), false, [], {}); + this.addDocument(mapMarker, this.annotationKey); + }; + + _loadPending = true; + /** + * store a reference to google map instance + * setup the drawing manager on the top right corner of map + * fit map bounds to contain all markers + * @param map + */ + @action + private loadHandler = (map: google.maps.Map) => { + this._map = map; + this._loadPending = true; + const centerControlDiv = this.CenterControl(); + map.controls[google.maps.ControlPosition.TOP_RIGHT].push(centerControlDiv); + //drawingManager.setMap(map); + // if (navigator.geolocation) { + // navigator.geolocation.getCurrentPosition( + // (position: Position) => { + // const pos = { + // lat: position.coords.latitude, + // lng: position.coords.longitude, + // }; + // this._map.setCenter(pos); + // } + // ); + // } else { + // alert("Your geolocation is not supported by browser.") + // }; + map.setZoom(NumCast(this.dataDoc.mapZoom, 2.5)); + map.setCenter(new google.maps.LatLng(NumCast(this.dataDoc.mapLat), NumCast(this.dataDoc.mapLng))); + setTimeout(() => { + if (this._loadPending && this._map.getBounds()) { + this._loadPending = false; + this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map); + } + }, 250); + // listener to addmarker event + this._map.addListener('click', (e: MouseEvent) => { + if (this.toggleAddMarker === true) { + this.placeMarker((e as any).latLng, map); + } + }); + }; + + @action + centered = () => { + if (this._loadPending && this._map.getBounds()) { + this._loadPending = false; + this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map); + } + this.dataDoc.mapLat = this._map.getCenter()?.lat(); + this.dataDoc.mapLng = this._map.getCenter()?.lng(); + }; + + @action + zoomChanged = () => { + if (this._loadPending && this._map.getBounds()) { + this._loadPending = false; + this.layoutDoc.freeform_fitContentsToBox && this.fitBounds(this._map); + } + this.dataDoc.mapZoom = this._map.getZoom(); + }; + + /** + * Load and render all map markers + * @param marker + * @param place + */ + @action + private markerLoadHandler = (marker: google.maps.Marker, place: Doc) => { + place[Id] ? (this.markerMap[place[Id]] = marker) : null; + }; + + /** + * on clicking the map marker, set the selected place to the marker document & set infowindowopen to be true + * @param e + * @param place + */ + @action + private markerClickHandler = (e: google.maps.MapMouseEvent, place: Doc) => { + // set which place was clicked + this.selectedPlace = place; + place.infoWindowOpen = true; + }; + + /** + * Called when dragging documents into map sidebar or directly into infowindow; to create a map marker, ref to MapMarkerDocument in Documents.ts + * @param doc + * @param sidebarKey + * @returns + */ + sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => { + console.log('print all sidebar Docs'); + if (!this.layoutDoc._layout_showSidebar) this.toggleSidebar(); + const docs = doc instanceof Doc ? [doc] : doc; + docs.forEach(doc => { + if (doc.lat !== undefined && doc.lng !== undefined) { + const existingMarker = this.allMapMarkers.find(marker => marker.lat === doc.lat && marker.lng === doc.lng); + if (existingMarker) { + Doc.AddDocToList(existingMarker, 'data', doc); + } else { + const marker = Docs.Create.MapMarkerDocument(NumCast(doc.lat), NumCast(doc.lng), false, [doc], {}); + this.addDocument(marker, this.annotationKey); + } + } + }); //add to annotation list + + return this.addDocument(doc, sidebarKey); // add to sidebar list + }; + + /** + * Removing documents from the sidebar + * @param doc + * @param sidebarKey + * @returns + */ + sidebarRemoveDocument = (doc: Doc | Doc[], sidebarKey?: string) => { + if (this.layoutDoc._layout_showSidebar) this.toggleSidebar(); + const docs = doc instanceof Doc ? [doc] : doc; + return this.removeDocument(doc, sidebarKey); + }; + + /** + * Toggle sidebar onclick the tiny comment button on the top right corner + * @param e + */ + sidebarBtnDown = (e: React.PointerEvent) => { + setupMoveUpEvents( + this, + e, + (e, down, delta) => + runInAction(() => { + const localDelta = this.props + .ScreenToLocalTransform() + .scale(this.props.NativeDimScaling?.() || 1) + .transformDirection(delta[0], delta[1]); + const fullWidth = this.layoutDoc[Width](); + const mapWidth = fullWidth - this.sidebarWidth(); + if (this.sidebarWidth() + localDelta[0] > 0) { + this._showSidebar = true; + this.layoutDoc._width = fullWidth + localDelta[0]; + this.layoutDoc._layout_sidebarWidthPercent = ((100 * (this.sidebarWidth() + localDelta[0])) / (fullWidth + localDelta[0])).toString() + '%'; + } else { + this._showSidebar = false; + this.layoutDoc._width = mapWidth; + this.layoutDoc._layout_sidebarWidthPercent = '0%'; + } + return false; + }), + emptyFunction, + () => UndoManager.RunInBatch(this.toggleSidebar, 'toggle sidebar map') + ); + }; + + sidebarWidth = () => (Number(this.layout_sidebarWidthPercent.substring(0, this.layout_sidebarWidthPercent.length - 1)) / 100) * this.props.PanelWidth(); + @computed get layout_sidebarWidthPercent() { + return StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); + } + @computed get sidebarColor() { + return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this.props.fieldKey + '_backgroundColor'], '#e4e4e4')); + } + + /** + * function that reads the place inputed from searchbox, then zoom in on the location that's been autocompleted; + * add a customized temporary marker on the map + */ + @action + private handlePlaceChanged = () => { + const place = this.searchBox.getPlace(); + + if (!place.geometry || !place.geometry.location) { + // user entered the name of a place that wasn't suggested & pressed the enter key, or place details request failed + window.alert("No details available for input: '" + place.name + "'"); + return; + } + + // zoom in on the location of the search result + if (place.geometry.viewport) { + this._map.fitBounds(place.geometry.viewport); + } else { + this._map.setCenter(place.geometry.location); + this._map.setZoom(17); + } + + // customize icon => customized icon for the nature of the location selected + const icon = { + url: place.icon as string, + size: new google.maps.Size(71, 71), + origin: new google.maps.Point(0, 0), + anchor: new google.maps.Point(17, 34), + scaledSize: new google.maps.Size(25, 25), + }; + + // put temporary cutomized marker on searched location + this.searchMarkers.forEach(marker => { + marker.setMap(null); + }); + this.searchMarkers = []; + this.searchMarkers.push( + new window.google.maps.Marker({ + map: this._map, + icon, + title: place.name, + position: place.geometry.location, + }) + ); + }; + + /** + * Handles toggle of sidebar on click the little comment button + */ + @computed get sidebarHandle() { + return ( + <div + className="MapBox2-overlayButton-sidebar" + key="sidebar" + title="Toggle Sidebar" + style={{ + display: !this.props.isContentActive() ? 'none' : undefined, + top: StrCast(this.rootDoc._layout_showTitle) === 'title' ? 20 : 5, + backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, + }} + onPointerDown={this.sidebarBtnDown}> + <FontAwesomeIcon style={{ color: Colors.WHITE }} icon={'comment-alt'} size="sm" /> + </div> + ); + } + + // TODO: Adding highlight box layer to Maps + @action + toggleSidebar = () => { + //1.2 * w * ? = .2 * w .2/1.2 + const prevWidth = this.sidebarWidth(); + this.layoutDoc._layout_showSidebar = (this.layoutDoc._layout_sidebarWidthPercent = StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%') === '0%' ? `${(100 * 0.2) / 1.2}%` : '0%') !== '0%'; + this.layoutDoc._width = this.layoutDoc._layout_showSidebar ? NumCast(this.layoutDoc._width) * 1.2 : Math.max(20, NumCast(this.layoutDoc._width) - prevWidth); + }; + + sidebarDown = (e: React.PointerEvent) => { + setupMoveUpEvents(this, e, this.sidebarMove, emptyFunction, () => setTimeout(this.toggleSidebar), true); + }; + sidebarMove = (e: PointerEvent, down: number[], delta: number[]) => { + const bounds = this._ref.current!.getBoundingClientRect(); + this.layoutDoc._layout_sidebarWidthPercent = '' + 100 * Math.max(0, 1 - (e.clientX - bounds.left) / bounds.width) + '%'; + this.layoutDoc._layout_showSidebar = this.layoutDoc._layout_sidebarWidthPercent !== '0%'; + e.preventDefault(); + return false; + }; + + setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean) => void) => (this._setPreviewCursor = func); + + @action + onMarqueeDown = (e: React.PointerEvent) => { + if (!e.altKey && e.button === 0 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen, InkTool.Write].includes(Doc.ActiveTool)) { + setupMoveUpEvents( + this, + e, + action(e => { + MarqueeAnnotator.clearAnnotations(this._savedAnnotations); + this._marqueeing = [e.clientX, e.clientY]; + return true; + }), + returnFalse, + () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), + false + ); + } + }; + @action finishMarquee = (x?: number, y?: number) => { + this._marqueeing = undefined; + this._isAnnotating = false; + x !== undefined && y !== undefined && this._setPreviewCursor?.(x, y, false, false); + }; + + addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => { + return this.addDocument(doc, annotationKey); + }; + + pointerEvents = () => { + return this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : SnappingManager.GetIsDragging() ? undefined : 'none'; + }; + @computed get annotationLayer() { + return ( + <div className="MapBox2-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer}> + {this.inlineTextAnnotations + .sort((a, b) => NumCast(a.y) - NumCast(b.y)) + .map(anno => ( + <Annotation key={`${anno[Id]}-annotation`} {...this.props} fieldKey={this.annotationKey} pointerEvents={this.pointerEvents} showInfo={this.showInfo} dataDoc={this.dataDoc} anno={anno} /> + ))} + </div> + ); + } + + getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => AnchorMenu.Instance?.GetAnchor(this._savedAnnotations, addAsAnnotation) ?? this.rootDoc; + + /** + * render contents in allMapMarkers (e.g. images with exifData) into google maps as map marker + * @returns + */ + private renderMarkers = () => { + return this.allMapMarkers.map(place => ( + <Marker key={place[Id]} position={{ lat: NumCast(place.lat), lng: NumCast(place.lng) }} onLoad={marker => this.markerLoadHandler(marker, place)} onClick={(e: google.maps.MapMouseEvent) => this.markerClickHandler(e, place)} /> + )); + }; + + // TODO: auto center on select a document in the sidebar + private handleMapCenter = (map: google.maps.Map) => { + // console.log("print the selected views in selectionManager:") + // if (SelectionManager.Views().lastElement()) { + // console.log(SelectionManager.Views().lastElement()); + // } + }; + + panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1) - this.sidebarWidth(); + panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); + scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._layout_scrollTop)); + transparentFilter = () => [...this.props.childFilters(), Utils.IsTransparentFilter()]; + opaqueFilter = () => [...this.props.childFilters(), Utils.IsOpaqueFilter()]; + infoWidth = () => this.props.PanelWidth() / 5; + infoHeight = () => this.props.PanelHeight() / 5; + anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick; + savedAnnotations = () => this._savedAnnotations; + + get MicrosoftMaps() { + return (window as any).Microsoft.Maps; + } + render() { + const renderAnnotations = (childFilters?: () => string[]) => null; + return ( + <div className="MapBox2" ref={this._ref}> + <div + className="MapBox2-wrapper" + onWheel={e => e.stopPropagation()} + onPointerDown={async e => { + e.button === 0 && !e.ctrlKey && e.stopPropagation(); + }} + style={{ width: `calc(100% - ${this.layout_sidebarWidthPercent})`, pointerEvents: this.pointerEvents() }}> + <div style={{ mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div> + {renderAnnotations(this.opaqueFilter)} + {SnappingManager.GetIsDragging() ? null : renderAnnotations()} + {this.annotationLayer} + + <div> + <GoogleMap mapContainerStyle={mapContainerStyle} onZoomChanged={this.zoomChanged} onCenterChanged={this.centered} onLoad={this.loadHandler} options={mapOptions}> + <Autocomplete onLoad={this.setSearchBox} onPlaceChanged={this.handlePlaceChanged}> + <input className="MapBox2-input" ref={this.inputRef} type="text" onKeyDown={e => e.stopPropagation()} placeholder="Enter location" /> + </Autocomplete> + + {this.renderMarkers()} + {this.allMapMarkers + .filter(marker => marker.infoWindowOpen) + .map(marker => ( + <MapBoxInfoWindow + key={marker[Id]} + {...this.props} + setContentView={emptyFunction} + place={marker} + markerMap={this.markerMap} + PanelWidth={this.infoWidth} + PanelHeight={this.infoHeight} + moveDocument={this.moveDocument} + isAnyChildContentActive={this.isAnyChildContentActive} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} + /> + ))} + {/* {this.handleMapCenter(this._map)} */} + </GoogleMap> + </div> + {!this._marqueeing || !this._mainCont.current || !this._annotationLayer.current ? null : ( + <MarqueeAnnotator + rootDoc={this.rootDoc} + anchorMenuClick={this.anchorMenuClick} + scrollTop={0} + down={this._marqueeing} + scaling={returnOne} + addDocument={this.addDocumentWrapper} + docView={this.props.docViewPath().lastElement()} + finishMarquee={this.finishMarquee} + savedAnnotations={this.savedAnnotations} + annotationLayer={this._annotationLayer.current} + selectionText={returnEmptyString} + mainCont={this._mainCont.current} + /> + )} + </div> + {/* </LoadScript > */} + <div className="MapBox2-sidebar" style={{ width: `${this.layout_sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> + <SidebarAnnos + ref={this._sidebarRef} + {...this.props} + fieldKey={this.fieldKey} + rootDoc={this.rootDoc} + layoutDoc={this.layoutDoc} + dataDoc={this.dataDoc} + usePanelWidth={true} + showSidebar={this.SidebarShown} + nativeWidth={NumCast(this.layoutDoc._nativeWidth)} + whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} + PanelWidth={this.sidebarWidth} + sidebarAddDocument={this.sidebarAddDocument} + moveDocument={this.moveDocument} + removeDocument={this.sidebarRemoveDocument} + /> + </div> + {this.sidebarHandle} + </div> + ); + } +} diff --git a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx index 7b437c7de..577101445 100644 --- a/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx +++ b/src/client/views/nodes/MapBox/MapBoxInfoWindow.tsx @@ -32,7 +32,7 @@ export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps & Vi addNoteClick = (e: React.PointerEvent) => { setupMoveUpEvents(this, e, returnFalse, emptyFunction, e => { - const newBox = Docs.Create.TextDocument('Note', { _autoHeight: true }); + const newBox = Docs.Create.TextDocument('Note', { _layout_autoHeight: true }); FormattedTextBox.SelectOnLoad = newBox[Id]; // track the new text box so we can give it a prop that tells it to focus itself when it's displayed Doc.AddDocToList(this.props.place, 'data', newBox); this._stack?.scrollToBottom(); @@ -42,7 +42,7 @@ export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps & Vi }; _stack: CollectionStackingView | CollectionNoteTakingView | null | undefined; - childFitWidth = (doc: Doc) => doc.type === DocumentType.RTF; + childLayoutFitWidth = (doc: Doc) => doc.type === DocumentType.RTF; addDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.AddDocToList(this.props.place, 'data', d), true as boolean); removeDoc = (doc: Doc | Doc[]) => (doc instanceof Doc ? [doc] : doc).reduce((p, d) => p && Doc.RemoveDocFromList(this.props.place, 'data', d), true as boolean); render() { @@ -59,7 +59,7 @@ export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps & Vi fieldKey="data" NativeWidth={returnZero} NativeHeight={returnZero} - docFilters={returnEmptyFilter} + childFilters={returnEmptyFilter} setHeight={emptyFunction} isAnnotationOverlay={false} select={emptyFunction} @@ -69,12 +69,12 @@ export class MapBoxInfoWindow extends React.Component<MapBoxInfoWindowProps & Vi rootSelected={returnFalse} childHideResizeHandles={returnTrue} childHideDecorationTitle={returnTrue} - childFitWidth={this.childFitWidth} + childLayoutFitWidth={this.childLayoutFitWidth} // childDocumentsActive={returnFalse} removeDocument={this.removeDoc} addDocument={this.addDoc} renderDepth={this.props.renderDepth + 1} - viewType={CollectionViewType.Stacking} + type_collection={CollectionViewType.Stacking} pointerEvents={returnAll} /> </div> diff --git a/src/client/views/nodes/PDFBox.tsx b/src/client/views/nodes/PDFBox.tsx index 6aa04e356..c210176b0 100644 --- a/src/client/views/nodes/PDFBox.tsx +++ b/src/client/views/nodes/PDFBox.tsx @@ -3,7 +3,8 @@ import { action, computed, IReactionDisposer, observable, reaction, runInAction import { observer } from 'mobx-react'; import * as Pdfjs from 'pdfjs-dist'; import 'pdfjs-dist/web/pdf_viewer.css'; -import { Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; +import { Doc, DocListCast, Opt } from '../../../fields/Doc'; +import { Height, Width } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { ComputedField } from '../../../fields/ScriptField'; @@ -63,7 +64,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps super(props); const nw = Doc.NativeWidth(this.Document, this.dataDoc) || 927; const nh = Doc.NativeHeight(this.Document, this.dataDoc) || 1200; - !this.Document._fitWidth && (this.Document._height = this.Document[WidthSym]() * (nh / nw)); + !this.Document._layout_fitWidth && (this.Document._height = this.Document[Width]() * (nh / nw)); if (this.pdfUrl) { if (PDFBox.pdfcache.get(this.pdfUrl.url.href)) runInAction(() => (this._pdf = PDFBox.pdfcache.get(this.pdfUrl!.url.href))); else if (PDFBox.pdfpromise.get(this.pdfUrl.url.href)) PDFBox.pdfpromise.get(this.pdfUrl.url.href)?.then(action((pdf: any) => (this._pdf = pdf))); @@ -104,15 +105,15 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const docViewContent = this.props.docViewPath().lastElement().ContentDiv!; const newDiv = docViewContent.cloneNode(true) as HTMLDivElement; - newDiv.style.width = this.layoutDoc[WidthSym]().toString(); - newDiv.style.height = this.layoutDoc[HeightSym]().toString(); + newDiv.style.width = this.layoutDoc[Width]().toString(); + newDiv.style.height = this.layoutDoc[Height]().toString(); this.replaceCanvases(docViewContent, newDiv); const htmlString = this._pdfViewer?._mainCont.current && new XMLSerializer().serializeToString(newDiv); const anchx = NumCast(cropping.x); const anchy = NumCast(cropping.y); - const anchw = cropping[WidthSym]() * (this.props.NativeDimScaling?.() || 1); - const anchh = cropping[HeightSym]() * (this.props.NativeDimScaling?.() || 1); + const anchw = cropping[Width]() * (this.props.NativeDimScaling?.() || 1); + const anchh = cropping[Height]() * (this.props.NativeDimScaling?.() || 1); const viewScale = 1; cropping.title = 'crop: ' + this.rootDoc.title; cropping.x = NumCast(this.rootDoc.x) + NumCast(this.rootDoc._width); @@ -122,15 +123,15 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps cropping.onClick = undefined; const croppingProto = Doc.GetProto(cropping); croppingProto.annotationOn = undefined; - croppingProto.isPrototype = true; + croppingProto.isDataDoc = true; croppingProto.proto = Cast(this.rootDoc.proto, Doc, null)?.proto; // set proto of cropping's data doc to be IMAGE_PROTO croppingProto.type = DocumentType.IMG; croppingProto.layout = ImageBox.LayoutString('data'); croppingProto.data = new ImageField(Utils.CorsProxy('http://www.cs.brown.edu/~bcz/noImage.png')); - croppingProto['data-nativeWidth'] = anchw; - croppingProto['data-nativeHeight'] = anchh; + croppingProto['data_nativeWidth'] = anchw; + croppingProto['data_nativeHeight'] = anchh; if (addCrop) { - DocUtils.MakeLink(region, cropping, { linkRelationship: 'cropped image' }); + DocUtils.MakeLink(region, cropping, { link_relationship: 'cropped image' }); } this.props.bringToFront(cropping); @@ -140,8 +141,8 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps htmlString, anchw, anchh, - (NumCast(region.y) * this.props.PanelWidth()) / NumCast(this.rootDoc[this.fieldKey + '-nativeWidth']), - (NumCast(region.x) * this.props.PanelWidth()) / NumCast(this.rootDoc[this.fieldKey + '-nativeWidth']), + (NumCast(region.y) * this.props.PanelWidth()) / NumCast(this.rootDoc[this.fieldKey + '_nativeWidth']), + (NumCast(region.x) * this.props.PanelWidth()) / NumCast(this.rootDoc[this.fieldKey + '_nativeWidth']), 4 ) .then((data_url: any) => { @@ -169,19 +170,19 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps CollectionFreeFormView.UpdateIcon( filename, docViewContent, - this.layoutDoc[WidthSym](), - this.layoutDoc[HeightSym](), + this.layoutDoc[Width](), + this.layoutDoc[Height](), this.props.PanelWidth(), this.props.PanelHeight(), - NumCast(this.layoutDoc._scrollTop), - NumCast(this.rootDoc[this.fieldKey + '-nativeHeight'], 1), + NumCast(this.layoutDoc._layout_scrollTop), + NumCast(this.rootDoc[this.fieldKey + '_nativeHeight'], 1), true, this.layoutDoc[Id] + '-icon', (iconFile: string, nativeWidth: number, nativeHeight: number) => { setTimeout(() => { this.dataDoc.icon = new ImageField(iconFile); - this.dataDoc['icon-nativeWidth'] = nativeWidth; - this.dataDoc['icon-nativeHeight'] = nativeHeight; + this.dataDoc['icon_nativeWidth'] = nativeWidth; + this.dataDoc['icon_nativeHeight'] = nativeHeight; }, 500); } ); @@ -201,13 +202,13 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps { fireImmediately: true } ); this._disposers.scroll = reaction( - () => this.rootDoc.scrollTop, + () => this.rootDoc.layout_scrollTop, () => { - if (!(ComputedField.WithoutComputed(() => FieldValue(this.props.Document[this.SidebarKey + '-panY'])) instanceof ComputedField)) { - this.props.Document[this.SidebarKey + '-panY'] = ComputedField.MakeFunction('this.scrollTop'); + if (!(ComputedField.WithoutComputed(() => FieldValue(this.props.Document[this.SidebarKey + '_panY'])) instanceof ComputedField)) { + this.props.Document[this.SidebarKey + '_panY'] = ComputedField.MakeFunction('this.layout_scrollTop'); } - this.props.Document[this.SidebarKey + '-viewScale'] = 1; - this.props.Document[this.SidebarKey + '-panX'] = 0; + this.props.Document[this.SidebarKey + '_freeform_scale'] = 1; + this.props.Document[this.SidebarKey + '_freeform_panX'] = 0; } ); } @@ -215,7 +216,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps brushView = (view: { width: number; height: number; panX: number; panY: number }) => this._pdfViewer?.brushView(view); sidebarAddDocTab = (doc: Doc, where: OpenWhere) => { - if (DocListCast(this.props.Document[this.props.fieldKey + '-sidebar']).includes(doc) && !this.SidebarShown) { + if (DocListCast(this.props.Document[this.props.fieldKey + '_sidebar']).includes(doc) && !this.SidebarShown) { this.toggleSidebar(false); return true; } @@ -238,9 +239,8 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps ele.append(this._pdfViewer.selectionContent()!); } const docAnchor = () => { - const anchor = Docs.Create.TextanchorDocument({ - title: StrCast(this.rootDoc.title + '@' + NumCast(this.layoutDoc._scrollTop)?.toFixed(0)), - unrendered: true, + const anchor = Docs.Create.PdfConfigDocument({ + title: StrCast(this.rootDoc.title + '@' + NumCast(this.layoutDoc._layout_scrollTop)?.toFixed(0)), annotationOn: this.rootDoc, }); return anchor; @@ -258,11 +258,11 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @action loaded = (nw: number, nh: number, np: number) => { - this.dataDoc[this.props.fieldKey + '-numPages'] = np; + this.dataDoc[this.props.fieldKey + '_numPages'] = np; Doc.SetNativeWidth(this.dataDoc, Math.max(Doc.NativeWidth(this.dataDoc), (nw * 96) / 72)); Doc.SetNativeHeight(this.dataDoc, (nh * 96) / 72); - this.layoutDoc._height = this.layoutDoc[WidthSym]() / (Doc.NativeAspect(this.dataDoc) || 1); - !this.Document._fitWidth && (this.Document._height = this.Document[WidthSym]() * (nh / nw)); + this.layoutDoc._height = this.layoutDoc[Width]() / (Doc.NativeAspect(this.dataDoc) || 1); + !this.Document._layout_fitWidth && (this.Document._height = this.Document[Width]() * (nh / nw)); }; public search = action((searchString: string, bwd?: boolean, clear: boolean = false) => { @@ -279,14 +279,14 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps public prevAnnotation = () => this._pdfViewer?.prevAnnotation(); public nextAnnotation = () => this._pdfViewer?.nextAnnotation(); public backPage = () => { - this.Document._curPage = Math.max(1, (NumCast(this.Document._curPage) || 1) - 1); + this.Document._layout_curPage = Math.max(1, (NumCast(this.Document._layout_curPage) || 1) - 1); return true; }; public forwardPage = () => { - this.Document._curPage = Math.min(NumCast(this.dataDoc[this.props.fieldKey + '-numPages']), (NumCast(this.Document._curPage) || 1) + 1); + this.Document._layout_curPage = Math.min(NumCast(this.dataDoc[this.props.fieldKey + '_numPages']), (NumCast(this.Document._layout_curPage) || 1) + 1); return true; }; - public gotoPage = (p: number) => (this.Document._curPage = p); + public gotoPage = (p: number) => (this.Document._layout_curPage = p); @undoBatch onKeyDown = action((e: KeyboardEvent) => { @@ -316,9 +316,9 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps searchStringChanged = (e: React.ChangeEvent<HTMLInputElement>) => (this._searchString = e.currentTarget.value); // adding external documents; to sidebar key - // if (doc.Geolocation) this.addDocument(doc, this.fieldkey+"-annotation") + // if (doc.Geolocation) this.addDocument(doc, this.fieldkey+"_annotation") sidebarAddDocument = (doc: Doc | Doc[], sidebarKey: string = this.SidebarKey) => { - if (!this.layoutDoc._showSidebar) this.toggleSidebar(); + if (!this.layoutDoc._show_sidebar) this.toggleSidebar(); return this.addDocument(doc, sidebarKey); }; sidebarBtnDown = (e: React.PointerEvent, onButton: boolean) => { @@ -332,13 +332,13 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps .ScreenToLocalTransform() .scale(this.props.NativeDimScaling?.() || 1) .transformDirection(delta[0], delta[1]); - const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']); + const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']); const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth); const ratio = (curNativeWidth + ((onButton ? 1 : -1) * localDelta[0]) / (this.props.NativeDimScaling?.() || 1)) / nativeWidth; if (ratio >= 1) { this.layoutDoc.nativeWidth = nativeWidth * ratio; - onButton && (this.layoutDoc._width = this.layoutDoc[WidthSym]() + localDelta[0]); - this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth; + onButton && (this.layoutDoc._width = this.layoutDoc[Width]() + localDelta[0]); + this.layoutDoc._show_sidebar = nativeWidth !== this.layoutDoc._nativeWidth; } return false; }, @@ -352,18 +352,18 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @observable _previewNativeWidth: Opt<number> = undefined; @observable _previewWidth: Opt<number> = undefined; toggleSidebar = action((preview: boolean = false) => { - const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']); + const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']); const sideratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? PDFBox.openSidebarWidth : 0) + nativeWidth) / nativeWidth; const pdfratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? PDFBox.openSidebarWidth + PDFBox.sidebarResizerWidth : 0) + nativeWidth) / nativeWidth; const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth); if (preview) { this._previewNativeWidth = nativeWidth * sideratio; - this._previewWidth = (this.layoutDoc[WidthSym]() * nativeWidth * sideratio) / curNativeWidth; + this._previewWidth = (this.layoutDoc[Width]() * nativeWidth * sideratio) / curNativeWidth; this._showSidebar = true; } else { this.layoutDoc.nativeWidth = nativeWidth * pdfratio; - this.layoutDoc._width = (this.layoutDoc[WidthSym]() * nativeWidth * pdfratio) / curNativeWidth; - this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth; + this.layoutDoc._width = (this.layoutDoc[Width]() * nativeWidth * pdfratio) / curNativeWidth; + this.layoutDoc._show_sidebar = nativeWidth !== this.layoutDoc._nativeWidth; } }); settingsPanel() { @@ -378,7 +378,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps </> ); const searchTitle = `${!this._searching ? 'Open' : 'Close'} Search Bar`; - const curPage = NumCast(this.Document._curPage) || 1; + const curPage = NumCast(this.Document._layout_curPage) || 1; return !this.props.isContentActive() || this._pdfViewer?.isAnnotating ? null : ( <div className="pdfBox-ui" @@ -424,7 +424,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps <input value={curPage} style={{ width: `${curPage > 99 ? 4 : 3}ch`, pointerEvents: 'all' }} - onChange={e => (this.Document._curPage = Number(e.currentTarget.value))} + onChange={e => (this.Document._layout_curPage = Number(e.currentTarget.value))} onKeyDown={e => e.stopPropagation()} onClick={action(() => (this._pageControls = !this._pageControls))} /> @@ -441,7 +441,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return PDFBox.sidebarResizerWidth + nativeDiff * (this.props.NativeDimScaling?.() || 1); }; @undoBatch - toggleSidebarType = () => (this.rootDoc.sidebarViewType = this.rootDoc.sidebarViewType === CollectionViewType.Freeform ? CollectionViewType.Stacking : CollectionViewType.Freeform); + toggleSidebarType = () => (this.rootDoc.sidebar_collectionType = this.rootDoc.sidebar_collectionType === CollectionViewType.Freeform ? CollectionViewType.Stacking : CollectionViewType.Freeform); specificContextMenu = (e: React.MouseEvent): void => { const cm = ContextMenu.Instance; const options = cm.findByDescription('Options...'); @@ -470,7 +470,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick; @observable _showSidebar = false; @computed get SidebarShown() { - return this._showSidebar || this.layoutDoc._showSidebar ? true : false; + return this._showSidebar || this.layoutDoc._show_sidebar ? true : false; } @computed get sidebarHandle() { return ( @@ -480,7 +480,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps title="Toggle Sidebar" style={{ display: !this.props.isContentActive() ? 'none' : undefined, - top: StrCast(this.rootDoc._showTitle) === 'title' ? 20 : 5, + top: StrCast(this.rootDoc._layout_showTitle) === 'title' ? 20 : 5, backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, }} onPointerDown={e => this.sidebarBtnDown(e, true)}> @@ -490,10 +490,10 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } public get SidebarKey() { - return this.fieldKey + '-sidebar'; + return this.fieldKey + '_sidebar'; } @computed get pdfScale() { - const pdfNativeWidth = NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']); + const pdfNativeWidth = NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']); const nativeWidth = NumCast(this.layoutDoc.nativeWidth, pdfNativeWidth); const pdfRatio = pdfNativeWidth / nativeWidth; return (pdfRatio * this.props.PanelWidth()) / pdfNativeWidth; @@ -557,7 +557,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }; return ( <div className={'formattedTextBox-sidebar' + (Doc.ActiveTool !== InkTool.None ? '-inking' : '')} style={{ width: '100%', right: 0, backgroundColor: `white` }}> - {renderComponent(StrCast(this.layoutDoc.sidebarViewType))} + {renderComponent(StrCast(this.layoutDoc.sidebar_collectionType))} </div> ); } @@ -572,7 +572,7 @@ export class PDFBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps onContextMenu={this.specificContextMenu} style={{ display: this.props.thumbShown?.() ? 'none' : undefined, - height: this.props.Document._scrollTop && !this.Document._fitWidth && window.screen.width > 600 ? (NumCast(this.Document._height) * this.props.PanelWidth()) / NumCast(this.Document._width) : undefined, + height: this.props.Document._layout_scrollTop && !this.Document._layout_fitWidth && window.screen.width > 600 ? (NumCast(this.Document._height) * this.props.PanelWidth()) / NumCast(this.Document._width) : undefined, }}> <div className="pdfBox-background" onPointerDown={e => this.sidebarBtnDown(e, false)} /> <div diff --git a/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.scss b/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.scss new file mode 100644 index 000000000..ac2c611c7 --- /dev/null +++ b/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.scss @@ -0,0 +1,90 @@ +.physicsSimApp { + * { + box-sizing: border-box; + font-size: 14px; + } + + .mechanicsSimulationContainer { + background-color: white; + height: 100%; + width: 100%; + display: flex; + + .mechanicsSimulationEquationContainer { + position: fixed; + left: 60%; + padding: 1em; + height: 100%; + transform-origin: top left; + + .mechanicsSimulationControls { + display: flex; + justify-content: space-between; + } + } + .rod, + .spring, + .wheel, + .showvecs, + .wedge { + pointer-events: none; + position: absolute; + left: 0; + top: 0; + } + } + + .coordinateSystem { + z-index: -100; + } + + th, + td { + border-collapse: collapse; + padding: 1em; + } + + table { + min-width: 300px; + } + + tr:nth-child(even) { + background-color: #d6eeee; + } + + button { + z-index: 50; + } + + .angleLabel { + font-weight: bold; + font-size: 20px; + user-select: none; + pointer-events: none; + } + + .mechanicsSimulationSettingsMenu { + width: 100%; + height: 100%; + font-size: 12px; + background-color: rgb(224, 224, 224); + border-radius: 2px; + border-color: black; + border-style: solid; + padding: 10px; + position: fixed; + z-index: 1000; + } + + .mechanicsSimulationSettingsMenuRow { + display: flex; + } + + .mechanicsSimulationSettingsMenuRowDescription { + width: 50%; + } + + .dropdownMenu { + z-index: 50; + } +} diff --git a/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx b/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx new file mode 100644 index 000000000..cd1ff17dd --- /dev/null +++ b/src/client/views/nodes/PhysicsBox/PhysicsSimulationBox.tsx @@ -0,0 +1,1987 @@ +import ArrowLeftIcon from '@mui/icons-material/ArrowLeft'; +import ArrowRightIcon from '@mui/icons-material/ArrowRight'; +import PauseIcon from '@mui/icons-material/Pause'; +import PlayArrowIcon from '@mui/icons-material/PlayArrow'; +import QuestionMarkIcon from '@mui/icons-material/QuestionMark'; +import ReplayIcon from '@mui/icons-material/Replay'; +import { Box, Button, Checkbox, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, FormControl, FormControlLabel, FormGroup, IconButton, LinearProgress, Stack } from '@mui/material'; +import Typography from '@mui/material/Typography'; +import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; +import { observer } from 'mobx-react'; +import { NumListCast } from '../../../../fields/Doc'; +import { List } from '../../../../fields/List'; +import { BoolCast, NumCast, StrCast } from '../../../../fields/Types'; +import { ViewBoxAnnotatableComponent } from '../../DocComponent'; +import { FieldView, FieldViewProps } from './../FieldView'; +import './PhysicsSimulationBox.scss'; +import InputField from './PhysicsSimulationInputField'; +import * as questions from './PhysicsSimulationQuestions.json'; +import * as tutorials from './PhysicsSimulationTutorial.json'; +import Wall from './PhysicsSimulationWall'; +import Weight from './PhysicsSimulationWeight'; +import React = require('react'); + +interface IWallProps { + length: number; + xPos: number; + yPos: number; + angleInDegrees: number; +} +interface IForce { + description: string; + magnitude: number; + directionInDegrees: number; +} +interface VectorTemplate { + top: number; + left: number; + width: number; + height: number; + x1: number; + y1: number; + x2: number; + y2: number; + weightX: number; + weightY: number; +} +interface QuestionTemplate { + questionSetup: string[]; + variablesForQuestionSetup: string[]; + question: string; + answerParts: string[]; + answerSolutionDescriptions: string[]; + goal: string; + hints: { description: string; content: string }[]; +} + +interface TutorialTemplate { + question: string; + steps: { + description: string; + content: string; + forces: { + description: string; + magnitude: number; + directionInDegrees: number; + component: boolean; + }[]; + showMagnitude: boolean; + }[]; +} + +@observer +export class PhysicsSimulationBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(PhysicsSimulationBox, fieldKey); + } + + _widthDisposer: IReactionDisposer | undefined; + @observable _simReset = 0; + + // semi-Constants + xMin = 0; + yMin = 0; + xMax = this.props.PanelWidth() * 0.6; + yMax = this.props.PanelHeight(); + color = `rgba(0,0,0,0.5)`; + radius = 50; + wallPositions: IWallProps[] = []; + + @computed get circularMotionRadius() { + return (NumCast(this.dataDoc.circularMotionRadius, 150) * this.props.PanelWidth()) / 1000; + } + @computed get gravity() { + return NumCast(this.dataDoc.simulation_gravity, -9.81); + } + @computed get simulationType() { + return StrCast(this.dataDoc.simulation_type, 'Inclined Plane'); + } + @computed get simulationMode() { + return StrCast(this.dataDoc.simulation_mode, 'Freeform'); + } + // Used for spring simulation + @computed get springConstant() { + return NumCast(this.dataDoc.spring_constant, 0.5); + } + @computed get springLengthRest() { + return NumCast(this.dataDoc.spring_lengthRest, 200); + } + @computed get springLengthStart() { + return NumCast(this.dataDoc.spring_lengthStart, 200); + } + + @computed get pendulumAngle() { + return NumCast(this.dataDoc.pendulum_angle); + } + @computed get pendulumAngleStart() { + return NumCast(this.dataDoc.pendulum_angleStart); + } + @computed get pendulumLength() { + return NumCast(this.dataDoc.pendulum_length); + } + @computed get pendulumLengthStart() { + return NumCast(this.dataDoc.pendulum_lengthStart); + } + + // Used for wedge simulation + @computed get wedgeAngle() { + return NumCast(this.dataDoc.wedge_angle, 26); + } + @computed get wedgeHeight() { + return NumCast(this.dataDoc.wedge_height, Math.tan((26 * Math.PI) / 180) * this.xMax * 0.5); + } + @computed get wedgeWidth() { + return NumCast(this.dataDoc.wedge_width, this.xMax * 0.5); + } + @computed get mass1() { + return NumCast(this.dataDoc.mass1, 1); + } + @computed get mass2() { + return NumCast(this.dataDoc.mass2, 1); + } + + @computed get mass1Radius() { + return NumCast(this.dataDoc.mass1_radius, 30); + } + @computed get mass1PosXStart() { + return NumCast(this.dataDoc.mass1_positionXstart); + } + @computed get mass1PosYStart() { + return NumCast(this.dataDoc.mass1_positionYstart); + } + @computed get mass1VelXStart() { + return NumCast(this.dataDoc.mass1_velocityXstart); + } + @computed get mass1VelYStart() { + return NumCast(this.dataDoc.mass1_velocityYstart); + } + + @computed get mass2PosXStart() { + return NumCast(this.dataDoc.mass2_positionXstart); + } + @computed get mass2PosYStart() { + return NumCast(this.dataDoc.mass2_positionYstart); + } + @computed get mass2VelXStart() { + return NumCast(this.dataDoc.mass2_velocityXstart); + } + @computed get mass2VelYStart() { + return NumCast(this.dataDoc.mass2_velocityYstart); + } + + @computed get selectedQuestion() { + return this.dataDoc.selectedQuestion ? (JSON.parse(StrCast(this.dataDoc.selectedQuestion)) as QuestionTemplate) : questions.inclinePlane[0]; + } + @computed get tutorial() { + return this.dataDoc.tutorial ? (JSON.parse(StrCast(this.dataDoc.tutorial)) as TutorialTemplate) : tutorials.inclinePlane; + } + @computed get selectedSolutions() { + return NumListCast(this.dataDoc.selectedSolutions); + } + @computed get questionPartOne() { + return StrCast(this.dataDoc.questionPartOne); + } + @computed get questionPartTwo() { + return StrCast(this.dataDoc.questionPartTwo); + } + + componentWillUnmount() { + this._widthDisposer?.(); + } + + componentDidMount() { + // Setup and update simulation + this._widthDisposer = reaction(() => [this.props.PanelWidth(), this.props.PanelHeight()], this.setupSimulation, { fireImmediately: true }); + + // Create walls + this.wallPositions = [ + { length: 100, xPos: 0, yPos: 0, angleInDegrees: 0 }, + { length: 100, xPos: 0, yPos: 100, angleInDegrees: 0 }, + { length: 100, xPos: 0, yPos: 0, angleInDegrees: 90 }, + { length: 100, xPos: (this.xMax / this.props.PanelWidth()) * 100, yPos: 0, angleInDegrees: 90 }, + ]; + } + + componentDidUpdate() { + if (this.xMax !== this.props.PanelWidth() * 0.6 || this.yMax != this.props.PanelHeight()) { + this.xMax = this.props.PanelWidth() * 0.6; + this.yMax = this.props.PanelHeight(); + this.setupSimulation(); + } + } + + gravityForce = (mass: number): IForce => ({ + description: 'Gravity', + magnitude: mass * Math.abs(this.gravity), + directionInDegrees: 270, + }); + + @action + setupSimulation = () => { + const simulationType = this.simulationType; + const mode = this.simulationMode; + this.dataDoc.simulation_paused = true; + if (simulationType != 'Circular Motion') { + this.dataDoc.mass1_velocityXstart = 0; + this.dataDoc.mass1_velocityYstart = 0; + this.dataDoc.mass1_velocityX = 0; + this.dataDoc.mass1_velocityY = 0; + } + if (mode == 'Freeform') { + this.dataDoc.simulation_showForceMagnitudes = true; + // prettier-ignore + switch (simulationType) { + case 'One Weight': + this.dataDoc.simulation_showComponentForces = false; + this.dataDoc.mass1_positionYstart = this.yMin + this.mass1Radius; + this.dataDoc.mass1_positionXstart = (this.xMax + this.xMin) / 2 - this.mass1Radius; + this.dataDoc.mass1_positionY = this.getDisplayYPos(this.yMin + this.mass1Radius); + this.dataDoc.mass1_positionX = (this.xMax + this.xMin) / 2 - this.mass1Radius; + this.dataDoc.mass1_forcesUpdated = JSON.stringify([this.gravityForce(this.mass1)]); + this.dataDoc.mass1_forcesStart = JSON.stringify([this.gravityForce(this.mass1)]); + break; + case 'Inclined Plane': this.setupInclinedPlane(); break; + case 'Pendulum': this.setupPendulum(); break; + case 'Spring': this.setupSpring(); break; + case 'Circular Motion': this.setupCircular(20); break; + case 'Pulley': this.setupPulley(); break; + case 'Suspension': this.setupSuspension();break; + } + this._simReset++; + } else if (mode == 'Review') { + this.dataDoc.simulation_showComponentForces = false; + this.dataDoc.simulation_showForceMagnitudes = true; + this.dataDoc.simulation_showAcceleration = false; + this.dataDoc.simulation_showVelocity = false; + this.dataDoc.simulation_showForces = true; + this.generateNewQuestion(); + // prettier-ignore + switch (simulationType) { + case 'One Weight' : break;// TODO - one weight review problems + case 'Spring': this.setupSpring(); break; // TODO - spring review problems + case 'Inclined Plane': this.dataDoc.mass1_forcesUpdated = this.dataDoc.mass1_forcesStart = ''; break; + case 'Pendulum': this.setupPendulum(); break; // TODO - pendulum review problems + case 'Circular Motion': this.setupCircular(0); break; // TODO - circular motion review problems + case 'Pulley': this.setupPulley(); break; // TODO - pulley tutorial review problems + case 'Suspension': this.setupSuspension(); break; // TODO - suspension tutorial review problems + } + } else if (mode == 'Tutorial') { + this.dataDoc.simulation_showComponentForces = false; + this.dataDoc.tutorial_stepNumber = 0; + this.dataDoc.simulation_showAcceleration = false; + if (this.simulationType != 'Circular Motion') { + this.dataDoc.mass1_velocityX = 0; + this.dataDoc.mass1_velocityY = 0; + this.dataDoc.simulation_showVelocity = false; + } else { + this.dataDoc.mass1_velocityX = 20; + this.dataDoc.mass1_velocityY = 0; + this.dataDoc.simulation_showVelocity = true; + } + + switch (this.simulationType) { + case 'One Weight': + this.dataDoc.simulation_showForces = true; + this.dataDoc.mass1_positionYstart = this.yMax - 100; + this.dataDoc.mass1_positionXstart = (this.xMax + this.xMin) / 2 - this.mass1Radius; + this.dataDoc.tutorial = JSON.stringify(tutorials.freeWeight); + this.dataDoc.mass1_forcesStart = JSON.stringify(tutorials.freeWeight.steps[0].forces); + this.dataDoc.simulation_showForceMagnitudes = tutorials.freeWeight.steps[0].showMagnitude; + break; + case 'Spring': + this.dataDoc.simulation_showForces = true; + this.setupSpring(); + this.dataDoc.mass1_positionYstart = this.yMin + 200 + 19.62; + this.dataDoc.mass1_positionXstart = (this.xMax + this.xMin) / 2 - this.mass1Radius; + this.dataDoc.tutorial = JSON.stringify(tutorials.spring); + this.dataDoc.mass1_forcesStart = JSON.stringify(tutorials.spring.steps[0].forces); + this.dataDoc.simulation_showForceMagnitudes = tutorials.spring.steps[0].showMagnitude; + break; + case 'Pendulum': + this.setupPendulum(); + this.dataDoc.tutorial = JSON.stringify(tutorials.pendulum); + this.dataDoc.mass1_forcesStart = JSON.stringify(tutorials.pendulum.steps[0].forces); + this.dataDoc.simulation_showForceMagnitudes = tutorials.pendulum.steps[0].showMagnitude; + break; + case 'Inclined Plane': + this.dataDoc.wedge_angle = 26; + this.setupInclinedPlane(); + this.dataDoc.simulation_showForces = true; + this.dataDoc.tutorial = JSON.stringify(tutorials.inclinePlane); + this.dataDoc.mass1_forcesStart = JSON.stringify(tutorials.inclinePlane.steps[0].forces); + this.dataDoc.simulation_showForceMagnitudes = tutorials.inclinePlane.steps[0].showMagnitude; + break; + case 'Circular Motion': + this.dataDoc.simulation_showForces = true; + this.setupCircular(40); + this.dataDoc.tutorial = JSON.stringify(tutorials.circular); + this.dataDoc.mass1_forcesStart = JSON.stringify(tutorials.circular.steps[0].forces); + this.dataDoc.simulation_showForceMagnitudes = tutorials.circular.steps[0].showMagnitude; + break; + case 'Pulley': + this.dataDoc.simulation_showForces = true; + this.setupPulley(); + this.dataDoc.tutorial = JSON.stringify(tutorials.pulley); + this.dataDoc.mass1_forcesStart = JSON.stringify(tutorials.pulley.steps[0].forces); + this.dataDoc.simulation_showForceMagnitudes = tutorials.pulley.steps[0].showMagnitude; + break; + case 'Suspension': + this.dataDoc.simulation_showForces = true; + this.setupSuspension(); + this.dataDoc.tutorial = JSON.stringify(tutorials.suspension); + this.dataDoc.mass1_forcesStart = JSON.stringify(tutorials.suspension.steps[0].forces); + this.dataDoc.simulation_showForceMagnitudes = tutorials.suspension.steps[0].showMagnitude; + break; + } + this._simReset++; + } + }; + + // Helper function to go between display and real values + getDisplayYPos = (yPos: number) => this.yMax - yPos - 2 * this.mass1Radius + 5; + getYPosFromDisplay = (yDisplay: number) => this.yMax - yDisplay - 2 * this.mass1Radius + 5; + + // Update forces when coefficient of static friction changes in freeform mode + updateForcesWithFriction = (coefficient: number, width = this.wedgeWidth, height = this.wedgeHeight) => { + const normalForce: IForce = { + description: 'Normal Force', + magnitude: Math.abs(this.gravity) * Math.cos(Math.atan(height / width)) * this.mass1, + directionInDegrees: 180 - 90 - (Math.atan(height / width) * 180) / Math.PI, + }; + let frictionForce: IForce = { + description: 'Static Friction Force', + magnitude: coefficient * Math.abs(this.gravity) * Math.cos(Math.atan(height / width)) * this.mass1, + directionInDegrees: 180 - (Math.atan(height / width) * 180) / Math.PI, + }; + // reduce magnitude or friction force if necessary such that block cannot slide up plane + let yForce = -Math.abs(this.gravity) * this.mass1; + yForce += normalForce.magnitude * Math.sin((normalForce.directionInDegrees * Math.PI) / 180); + yForce += frictionForce.magnitude * Math.sin((frictionForce.directionInDegrees * Math.PI) / 180); + if (yForce > 0) { + frictionForce.magnitude = (-normalForce.magnitude * Math.sin((normalForce.directionInDegrees * Math.PI) / 180) + Math.abs(this.gravity) * this.mass1) / Math.sin((frictionForce.directionInDegrees * Math.PI) / 180); + } + + const normalForceComponent: IForce = { + description: 'Normal Force', + magnitude: this.mass1 * Math.abs(this.gravity) * Math.cos(Math.atan(height / width)), + directionInDegrees: 180 - 90 - (Math.atan(height / width) * 180) / Math.PI, + }; + const gravityParallel: IForce = { + description: 'Gravity Parallel Component', + magnitude: this.mass1 * Math.abs(this.gravity) * Math.sin(Math.PI / 2 - Math.atan(height / width)), + directionInDegrees: 180 - 90 - (Math.atan(height / width) * 180) / Math.PI + 180, + }; + const gravityPerpendicular: IForce = { + description: 'Gravity Perpendicular Component', + magnitude: this.mass1 * Math.abs(this.gravity) * Math.cos(Math.PI / 2 - Math.atan(height / width)), + directionInDegrees: 360 - (Math.atan(height / width) * 180) / Math.PI, + }; + const gravityForce = this.gravityForce(this.mass1); + if (coefficient != 0) { + this.dataDoc.mass1_forcesStart = JSON.stringify([gravityForce, normalForce, frictionForce]); + this.dataDoc.mass1_forcesUpdated = JSON.stringify([gravityForce, normalForce, frictionForce]); + this.dataDoc.mass1_componentForces = JSON.stringify([frictionForce, normalForceComponent, gravityParallel, gravityPerpendicular]); + } else { + this.dataDoc.mass1_forcesStart = JSON.stringify([gravityForce, normalForce]); + this.dataDoc.mass1_forcesUpdated = JSON.stringify([gravityForce, normalForce]); + this.dataDoc.mass1_componentForces = JSON.stringify([normalForceComponent, gravityParallel, gravityPerpendicular]); + } + }; + + // Change wedge height and width and weight position to match new wedge angle + changeWedgeBasedOnNewAngle = (angle: number) => { + const radAng = (angle * Math.PI) / 180; + this.dataDoc.wedge_width = this.xMax * 0.5; + this.dataDoc.wedge_height = Math.tan(radAng) * this.dataDoc.wedge_width; + + // update weight position based on updated wedge width/height + let yPos = this.yMax - this.dataDoc.wedge_height - this.mass1Radius * Math.cos(radAng) - this.mass1Radius; + let xPos = this.xMax * 0.25 + this.mass1Radius * Math.sin(radAng) - this.mass1Radius; + + this.dataDoc.mass1_positionXstart = xPos; + this.dataDoc.mass1_positionYstart = yPos; + if (this.simulationMode == 'Freeform') { + this.updateForcesWithFriction(NumCast(this.dataDoc.coefficientOfStaticFriction), this.dataDoc.wedge_width, Math.tan(radAng) * this.dataDoc.wedge_width); + } + }; + + // In review mode, update forces when coefficient of static friction changed + updateReviewForcesBasedOnCoefficient = (coefficient: number) => { + let theta = this.wedgeAngle; + let index = this.selectedQuestion.variablesForQuestionSetup.indexOf('theta - max 45'); + if (index >= 0) { + theta = NumListCast(this.dataDoc.questionVariables)[index]; + } + if (isNaN(theta)) { + return; + } + this.dataDoc.review_GravityMagnitude = Math.abs(this.gravity); + this.dataDoc.review_GravityAngle = 270; + this.dataDoc.review_NormalMagnitude = Math.abs(this.gravity) * Math.cos((theta * Math.PI) / 180); + this.dataDoc.review_NormalAngle = 90 - theta; + let yForce = -Math.abs(this.gravity); + yForce += Math.abs(this.gravity) * Math.cos((theta * Math.PI) / 180) * Math.sin(((90 - theta) * Math.PI) / 180); + yForce += coefficient * Math.abs(this.gravity) * Math.cos((theta * Math.PI) / 180) * Math.sin(((180 - theta) * Math.PI) / 180); + let friction = coefficient * Math.abs(this.gravity) * Math.cos((theta * Math.PI) / 180); + if (yForce > 0) { + friction = (-(Math.abs(this.gravity) * Math.cos((theta * Math.PI) / 180)) * Math.sin(((90 - theta) * Math.PI) / 180) + Math.abs(this.gravity)) / Math.sin(((180 - theta) * Math.PI) / 180); + } + this.dataDoc.review_StaticMagnitude = friction; + this.dataDoc.review_StaticAngle = 180 - theta; + }; + + // In review mode, update forces when wedge angle changed + updateReviewForcesBasedOnAngle = (angle: number) => { + this.dataDoc.review_GravityMagnitude = Math.abs(this.gravity); + this.dataDoc.review_GravityAngle = 270; + this.dataDoc.review_NormalMagnitude = Math.abs(this.gravity) * Math.cos((angle * Math.PI) / 180); + this.dataDoc.review_NormalAngle = 90 - angle; + let yForce = -Math.abs(this.gravity); + yForce += Math.abs(this.gravity) * Math.cos((angle * Math.PI) / 180) * Math.sin(((90 - angle) * Math.PI) / 180); + yForce += NumCast(this.dataDoc.review_Coefficient) * Math.abs(this.gravity) * Math.cos((angle * Math.PI) / 180) * Math.sin(((180 - angle) * Math.PI) / 180); + let friction = NumCast(this.dataDoc.review_Coefficient) * Math.abs(this.gravity) * Math.cos((angle * Math.PI) / 180); + if (yForce > 0) { + friction = (-(Math.abs(this.gravity) * Math.cos((angle * Math.PI) / 180)) * Math.sin(((90 - angle) * Math.PI) / 180) + Math.abs(this.gravity)) / Math.sin(((180 - angle) * Math.PI) / 180); + } + this.dataDoc.review_StaticMagnitude = friction; + this.dataDoc.review_StaticAngle = 180 - angle; + }; + + // Solve for the correct answers to the generated problem + getAnswersToQuestion = (question: QuestionTemplate, questionVars: number[]) => { + const solutions: number[] = []; + + let theta = this.wedgeAngle; + let index = question.variablesForQuestionSetup.indexOf('theta - max 45'); + if (index >= 0) { + theta = questionVars[index]; + } + let muS: number = NumCast(this.dataDoc.coefficientOfStaticFriction); + index = question.variablesForQuestionSetup.indexOf('coefficient of static friction'); + if (index >= 0) { + muS = questionVars[index]; + } + + for (let i = 0; i < question.answerSolutionDescriptions.length; i++) { + const description = question.answerSolutionDescriptions[i]; + if (!isNaN(NumCast(description))) { + solutions.push(NumCast(description)); + } else if (description == 'solve normal force angle from wedge angle') { + solutions.push(90 - theta); + } else if (description == 'solve normal force magnitude from wedge angle') { + solutions.push(Math.abs(this.gravity) * Math.cos((theta / 180) * Math.PI)); + } else if (description == 'solve static force magnitude from wedge angle given equilibrium') { + let normalForceMagnitude = Math.abs(this.gravity) * Math.cos((theta / 180) * Math.PI); + let normalForceAngle = 90 - theta; + let frictionForceAngle = 180 - theta; + let frictionForceMagnitude = (-normalForceMagnitude * Math.sin((normalForceAngle * Math.PI) / 180) + Math.abs(this.gravity)) / Math.sin((frictionForceAngle * Math.PI) / 180); + solutions.push(frictionForceMagnitude); + } else if (description == 'solve static force angle from wedge angle given equilibrium') { + solutions.push(180 - theta); + } else if (description == 'solve minimum static coefficient from wedge angle given equilibrium') { + let normalForceMagnitude = Math.abs(this.gravity) * Math.cos((theta / 180) * Math.PI); + let normalForceAngle = 90 - theta; + let frictionForceAngle = 180 - theta; + let frictionForceMagnitude = (-normalForceMagnitude * Math.sin((normalForceAngle * Math.PI) / 180) + Math.abs(this.gravity)) / Math.sin((frictionForceAngle * Math.PI) / 180); + let frictionCoefficient = frictionForceMagnitude / normalForceMagnitude; + solutions.push(frictionCoefficient); + } else if (description == 'solve maximum wedge angle from coefficient of static friction given equilibrium') { + solutions.push((Math.atan(muS) * 180) / Math.PI); + } + } + this.dataDoc.selectedSolutions = new List<number>(solutions); + return solutions; + }; + + // In review mode, check if input answers match correct answers and optionally generate alert + checkAnswers = (showAlert: boolean = true) => { + let error: boolean = false; + let epsilon: number = 0.01; + if (this.selectedQuestion) { + for (let i = 0; i < this.selectedQuestion.answerParts.length; i++) { + if (this.selectedQuestion.answerParts[i] == 'force of gravity') { + if (Math.abs(NumCast(this.dataDoc.review_GravityMagnitude) - this.selectedSolutions[i]) > epsilon) { + error = true; + } + } else if (this.selectedQuestion.answerParts[i] == 'angle of gravity') { + if (Math.abs(NumCast(this.dataDoc.review_GravityAngle) - this.selectedSolutions[i]) > epsilon) { + error = true; + } + } else if (this.selectedQuestion.answerParts[i] == 'normal force') { + if (Math.abs(NumCast(this.dataDoc.review_NormalMagnitude) - this.selectedSolutions[i]) > epsilon) { + error = true; + } + } else if (this.selectedQuestion.answerParts[i] == 'angle of normal force') { + if (Math.abs(NumCast(this.dataDoc.review_NormalAngle) - this.selectedSolutions[i]) > epsilon) { + error = true; + } + } else if (this.selectedQuestion.answerParts[i] == 'force of static friction') { + if (Math.abs(NumCast(this.dataDoc.review_StaticMagnitude) - this.selectedSolutions[i]) > epsilon) { + error = true; + } + } else if (this.selectedQuestion.answerParts[i] == 'angle of static friction') { + if (Math.abs(NumCast(this.dataDoc.review_StaticAngle) - this.selectedSolutions[i]) > epsilon) { + error = true; + } + } else if (this.selectedQuestion.answerParts[i] == 'coefficient of static friction') { + if (Math.abs(NumCast(this.dataDoc.coefficientOfStaticFriction) - this.selectedSolutions[i]) > epsilon) { + error = true; + } + } else if (this.selectedQuestion.answerParts[i] == 'wedge angle') { + if (Math.abs(this.wedgeAngle - this.selectedSolutions[i]) > epsilon) { + error = true; + } + } + } + } + if (showAlert) { + this.dataDoc.simulation_paused = false; + setTimeout(() => (this.dataDoc.simulation_paused = true), 3000); + } + if (this.selectedQuestion.goal == 'noMovement') { + this.dataDoc.noMovement = !error; + } + }; + + // Reset all review values to default + resetReviewValuesToDefault = () => { + this.dataDoc.review_GravityMagnitude = 0; + this.dataDoc.review_GravityAngle = 0; + this.dataDoc.review_NormalMagnitude = 0; + this.dataDoc.review_NormalAngle = 0; + this.dataDoc.review_StaticMagnitude = 0; + this.dataDoc.review_StaticAngle = 0; + this.dataDoc.coefficientOfKineticFriction = 0; + this.dataDoc.simulation_paused = true; + }; + + // In review mode, reset problem variables and generate a new question + generateNewQuestion = () => { + this.resetReviewValuesToDefault(); + + const vars: number[] = []; + let question: QuestionTemplate = questions.inclinePlane[0]; + + if (this.simulationType === 'Inclined Plane') { + this.dataDoc.questionNumber = (NumCast(this.dataDoc.questionNumber) + 1) % questions.inclinePlane.length; + question = questions.inclinePlane[NumCast(this.dataDoc.questionNumber)]; + + let coefficient = 0; + let wedge_angle = 0; + + for (let i = 0; i < question.variablesForQuestionSetup.length; i++) { + if (question.variablesForQuestionSetup[i] == 'theta - max 45') { + let randValue = Math.floor(Math.random() * 44 + 1); + vars.push(randValue); + wedge_angle = randValue; + } else if (question.variablesForQuestionSetup[i] == 'coefficient of static friction') { + let randValue = Math.round(Math.random() * 1000) / 1000; + vars.push(randValue); + coefficient = randValue; + } + } + this.dataDoc.wedge_angle = wedge_angle; + this.changeWedgeBasedOnNewAngle(wedge_angle); + this.dataDoc.coefficientOfStaticFriction = coefficient; + this.dataDoc.review_Coefficient = coefficient; + } + let q = ''; + for (let i = 0; i < question.questionSetup.length; i++) { + q += question.questionSetup[i]; + if (i != question.questionSetup.length - 1) { + q += vars[i]; + if (question.variablesForQuestionSetup[i].includes('theta')) { + q += ' degree (≈' + Math.round((1000 * (vars[i] * Math.PI)) / 180) / 1000 + ' rad)'; + } + } + } + this.dataDoc.questionVariables = new List<number>(vars); + this.dataDoc.selectedQuestion = JSON.stringify(question); + this.dataDoc.questionPartOne = q; + this.dataDoc.questionPartTwo = question.question; + this.dataDoc.answers = new List<number>(this.getAnswersToQuestion(question, vars)); + //this.dataDoc.simulation_reset = (!this.dataDoc.simulation_reset); + }; + + // Default setup for uniform circular motion simulation + @action + setupCircular = (value: number) => { + this.dataDoc.simulation_showComponentForces = false; + this.dataDoc.mass1_velocityYstart = 0; + this.dataDoc.mass1_velocityXstart = value; + let xPos = (this.xMax + this.xMin) / 2 - this.mass1Radius; + let yPos = (this.yMax + this.yMin) / 2 + this.circularMotionRadius - this.mass1Radius; + this.dataDoc.mass1_positionYstart = yPos; + this.dataDoc.mass1_positionXstart = xPos; + const tensionForce: IForce = { + description: 'Centripetal Force', + magnitude: (this.dataDoc.mass1_velocityXstart ** 2 * this.mass1) / this.circularMotionRadius, + directionInDegrees: 90, + }; + this.dataDoc.mass1_forcesUpdated = JSON.stringify([tensionForce]); + this.dataDoc.mass1_forcesStart = JSON.stringify([tensionForce]); + this._simReset++; + }; + + setupInclinedPlane = () => { + this.changeWedgeBasedOnNewAngle(this.wedgeAngle); + this.dataDoc.mass1_forcesStart = JSON.stringify([this.gravityForce(this.mass1)]); + this.updateForcesWithFriction(NumCast(this.dataDoc.coefficientOfStaticFriction)); + }; + + // Default setup for pendulum simulation + setupPendulum = () => { + const length = (300 * this.props.PanelWidth()) / 1000; + const angle = 30; + const x = length * Math.cos(((90 - angle) * Math.PI) / 180); + const y = length * Math.sin(((90 - angle) * Math.PI) / 180); + const xPos = this.xMax / 2 - x - this.mass1Radius; + const yPos = y - this.mass1Radius - 5; + this.dataDoc.mass1_positionXstart = xPos; + this.dataDoc.mass1_positionYstart = yPos; + const forceOfTension: IForce = { + description: 'Tension', + magnitude: this.mass1 * Math.abs(this.gravity) * Math.sin((60 * Math.PI) / 180), + directionInDegrees: 90 - angle, + }; + const gravityParallel: IForce = { + description: 'Gravity Parallel Component', + magnitude: this.mass1 * Math.abs(this.gravity) * Math.sin(((90 - angle) * Math.PI) / 180), + directionInDegrees: -angle - 90, + }; + const gravityPerpendicular: IForce = { + description: 'Gravity Perpendicular Component', + magnitude: this.mass1 * Math.abs(this.gravity) * Math.cos(((90 - angle) * Math.PI) / 180), + directionInDegrees: -angle, + }; + + this.dataDoc.mass1_componentForces = JSON.stringify([forceOfTension, gravityParallel, gravityPerpendicular]); + this.dataDoc.mass1_forcesUpdated = JSON.stringify([this.gravityForce(this.mass1)]); + this.dataDoc.mass1_forcesStart = JSON.stringify([this.gravityForce(this.mass1), forceOfTension]); + this.dataDoc.pendulum_angle = this.dataDoc.pendulum_angleStart = 30; + this.dataDoc.pendulum_length = this.dataDoc.pendulum_lengthStart = 300; + }; + + // Default setup for spring simulation + @action + setupSpring = () => { + this.dataDoc.simulation_showComponentForces = false; + this.dataDoc.mass1_forcesUpdated = JSON.stringify([this.gravityForce(this.mass1)]); + this.dataDoc.mass1_forcesStart = JSON.stringify([this.gravityForce(this.mass1)]); + this.dataDoc.mass1_positionXstart = this.xMax / 2 - this.mass1Radius; + this.dataDoc.mass1_positionYstart = 200; + this.dataDoc.spring_constant = 0.5; + this.dataDoc.spring_lengthRest = 200; + this.dataDoc.spring_lengthStart = 200; + this._simReset++; + }; + + // Default setup for suspension simulation + @action + setupSuspension = () => { + let xPos = (this.xMax + this.xMin) / 2 - this.mass1Radius; + let yPos = this.yMin + 200; + this.dataDoc.mass1_positionYstart = yPos; + this.dataDoc.mass1_positionXstart = xPos; + this.dataDoc.mass1_positionY = this.getDisplayYPos(yPos); + this.dataDoc.mass1_positionX = xPos; + let tensionMag = (this.mass1 * Math.abs(this.gravity)) / (2 * Math.sin(Math.PI / 4)); + const tensionForce1: IForce = { + description: 'Tension', + magnitude: tensionMag, + directionInDegrees: 45, + }; + const tensionForce2: IForce = { + description: 'Tension', + magnitude: tensionMag, + directionInDegrees: 135, + }; + const gravity = this.gravityForce(this.mass1); + this.dataDoc.mass1_forcesUpdated = JSON.stringify([tensionForce1, tensionForce2, gravity]); + this.dataDoc.mass1_forcesStart = JSON.stringify([tensionForce1, tensionForce2, gravity]); + this._simReset++; + }; + + // Default setup for pulley simulation + @action + setupPulley = () => { + this.dataDoc.simulation_showComponentForces = false; + this.dataDoc.mass1_positionYstart = (this.yMax + this.yMin) / 2; + this.dataDoc.mass1_positionXstart = (this.xMin + this.xMax) / 2 - 2 * this.mass1Radius - 5; + this.dataDoc.mass1_positionY = this.getDisplayYPos((this.yMax + this.yMin) / 2); + this.dataDoc.mass1_positionX = (this.xMin + this.xMax) / 2 - 2 * this.mass1Radius - 5; + const a = (-1 * ((this.mass1 - this.mass2) * Math.abs(this.gravity))) / (this.mass1 + this.mass2); + const gravityForce1 = this.gravityForce(this.mass1); + const tensionForce1: IForce = { + description: 'Tension', + magnitude: this.mass1 * a + this.mass1 * Math.abs(this.gravity), + directionInDegrees: 90, + }; + this.dataDoc.mass1_forcesUpdated = JSON.stringify([gravityForce1, tensionForce1]); + this.dataDoc.mass1_forcesStart = JSON.stringify([gravityForce1, tensionForce1]); + + const gravityForce2 = this.gravityForce(this.mass2); + const tensionForce2: IForce = { + description: 'Tension', + magnitude: -this.mass2 * a + this.mass2 * Math.abs(this.gravity), + directionInDegrees: 90, + }; + this.dataDoc.mass2_positionYstart = (this.yMax + this.yMin) / 2; + this.dataDoc.mass2_positionXstart = (this.xMin + this.xMax) / 2 + 5; + this.dataDoc.mass2_positionY = this.getDisplayYPos((this.yMax + this.yMin) / 2); + this.dataDoc.mass2_positionX = (this.xMin + this.xMax) / 2 + 5; + this.dataDoc.mass2_forcesUpdated = JSON.stringify([gravityForce2, tensionForce2]); + this.dataDoc.mass2_forcesStart = JSON.stringify([gravityForce2, tensionForce2]); + this._simReset++; + }; + + public static parseJSON(json: string) { + return !json ? [] : (JSON.parse(json) as IForce[]); + } + + // Handle force change in review mode + updateReviewModeValues = () => { + const forceOfGravityReview: IForce = { + description: 'Gravity', + magnitude: NumCast(this.dataDoc.review_GravityMagnitude), + directionInDegrees: NumCast(this.dataDoc.review_GravityAngle), + }; + const normalForceReview: IForce = { + description: 'Normal Force', + magnitude: NumCast(this.dataDoc.review_NormalMagnitude), + directionInDegrees: NumCast(this.dataDoc.review_NormalAngle), + }; + const staticFrictionForceReview: IForce = { + description: 'Static Friction Force', + magnitude: NumCast(this.dataDoc.review_StaticMagnitude), + directionInDegrees: NumCast(this.dataDoc.review_StaticAngle), + }; + this.dataDoc.mass1_forcesStart = JSON.stringify([forceOfGravityReview, normalForceReview, staticFrictionForceReview]); + this.dataDoc.mass1_forcesUpdated = JSON.stringify([forceOfGravityReview, normalForceReview, staticFrictionForceReview]); + }; + + pause = () => (this.dataDoc.simulation_paused = true); + componentForces1 = () => PhysicsSimulationBox.parseJSON(StrCast(this.dataDoc.mass1_componentForces)); + setComponentForces1 = (forces: IForce[]) => (this.dataDoc.mass1_componentForces = JSON.stringify(forces)); + componentForces2 = () => PhysicsSimulationBox.parseJSON(StrCast(this.dataDoc.mass2_componentForces)); + setComponentForces2 = (forces: IForce[]) => (this.dataDoc.mass2_componentForces = JSON.stringify(forces)); + startForces1 = () => PhysicsSimulationBox.parseJSON(StrCast(this.dataDoc.mass1_forcesStart)); + startForces2 = () => PhysicsSimulationBox.parseJSON(StrCast(this.dataDoc.mass2_forcesStart)); + forcesUpdated1 = () => PhysicsSimulationBox.parseJSON(StrCast(this.dataDoc.mass1_forcesUpdated)); + setForcesUpdated1 = (forces: IForce[]) => (this.dataDoc.mass1_forcesUpdated = JSON.stringify(forces)); + forcesUpdated2 = () => PhysicsSimulationBox.parseJSON(StrCast(this.dataDoc.mass2_forcesUpdated)); + setForcesUpdated2 = (forces: IForce[]) => (this.dataDoc.mass2_forcesUpdated = JSON.stringify(forces)); + setPosition1 = (xPos: number | undefined, yPos: number | undefined) => { + yPos !== undefined && (this.dataDoc.mass1_positionY = Math.round(yPos * 100) / 100); + xPos !== undefined && (this.dataDoc.mass1_positionX = Math.round(xPos * 100) / 100); + }; + setPosition2 = (xPos: number | undefined, yPos: number | undefined) => { + yPos !== undefined && (this.dataDoc.mass2_positionY = Math.round(yPos * 100) / 100); + xPos !== undefined && (this.dataDoc.mass2_positionX = Math.round(xPos * 100) / 100); + }; + setVelocity1 = (xVel: number | undefined, yVel: number | undefined) => { + yVel !== undefined && (this.dataDoc.mass1_velocityY = (-1 * Math.round(yVel * 100)) / 100); + xVel !== undefined && (this.dataDoc.mass1_velocityX = Math.round(xVel * 100) / 100); + }; + setVelocity2 = (xVel: number | undefined, yVel: number | undefined) => { + yVel !== undefined && (this.dataDoc.mass2_velocityY = (-1 * Math.round(yVel * 100)) / 100); + xVel !== undefined && (this.dataDoc.mass2_velocityX = Math.round(xVel * 100) / 100); + }; + setAcceleration1 = (xAccel: number, yAccel: number) => { + this.dataDoc.mass1_accelerationY = yAccel; + this.dataDoc.mass1_accelerationX = xAccel; + }; + setAcceleration2 = (xAccel: number, yAccel: number) => { + this.dataDoc.mass2_accelerationY = yAccel; + this.dataDoc.mass2_accelerationX = xAccel; + }; + setPendulumAngle = (angle: number | undefined, length: number | undefined) => { + angle !== undefined && (this.dataDoc.pendulum_angle = angle); + length !== undefined && (this.dataDoc.pendulum_length = length); + }; + setSpringLength = (length: number) => { + this.dataDoc.spring_lengthStart = length; + }; + resetRequest = () => this._simReset; + render() { + const commonWeightProps = { + pause: this.pause, + paused: BoolCast(this.dataDoc.simulation_paused), + panelWidth: this.props.PanelWidth, + panelHeight: this.props.PanelHeight, + resetRequest: this.resetRequest, + xMax: this.xMax, + xMin: this.xMin, + yMax: this.yMax, + yMin: this.yMin, + wallPositions: this.wallPositions, + gravity: Math.abs(this.gravity), + timestepSize: 0.05, + showComponentForces: BoolCast(this.dataDoc.simulation_showComponentForces), + coefficientOfKineticFriction: NumCast(this.dataDoc.coefficientOfKineticFriction), + elasticCollisions: BoolCast(this.dataDoc.elasticCollisions), + simulationMode: this.simulationMode, + noMovement: BoolCast(this.dataDoc.noMovement), + circularMotionRadius: this.circularMotionRadius, + wedgeHeight: this.wedgeHeight, + wedgeWidth: this.wedgeWidth, + springConstant: this.springConstant, + springStartLength: this.springLengthStart, + springRestLength: this.springLengthRest, + setSpringLength: this.setSpringLength, + setPendulumAngle: this.setPendulumAngle, + pendulumAngle: this.pendulumAngle, + pendulumLength: this.pendulumLength, + startPendulumAngle: this.pendulumAngleStart, + startPendulumLength: this.pendulumLengthStart, + radius: this.mass1Radius, + simulationSpeed: NumCast(this.dataDoc.simulation_speed, 2), + showAcceleration: BoolCast(this.dataDoc.simulation_showAcceleration), + showForceMagnitudes: BoolCast(this.dataDoc.simulation_showForceMagnitudes), + showForces: BoolCast(this.dataDoc.simulation_showForces), + showVelocity: BoolCast(this.dataDoc.simulation_showVelocity), + simulationType: this.simulationType, + }; + return ( + <div className="physicsSimApp"> + <div className="mechanicsSimulationContainer"> + <div className="mechanicsSimulationContentContainer"> + <div className="mechanicsSimulationButtonsAndElements"> + <div className="mechanicsSimulationButtons"> + {!this.dataDoc.simulation_paused && ( + <div + style={{ + position: 'fixed', + left: 0.1 * this.props.PanelWidth() + 'px', + top: 0.95 * this.props.PanelHeight() + 'px', + width: 0.5 * this.props.PanelWidth() + 'px', + }}> + <LinearProgress /> + </div> + )} + </div> + <div + className="mechanicsSimulationElements" + style={{ + // + width: '60%', + height: '100%', + position: 'absolute', + background: 'yellow', + }}> + <Weight + {...commonWeightProps} + color="red" + componentForces={this.componentForces1} + setComponentForces={this.setComponentForces1} + displayXVelocity={NumCast(this.dataDoc.mass1_velocityX)} + displayYVelocity={NumCast(this.dataDoc.mass1_velocityY)} + mass={this.mass1} + startForces={this.startForces1} + startPosX={this.mass1PosXStart} + startPosY={this.mass1PosYStart} + startVelX={this.mass1VelXStart} + startVelY={this.mass1VelYStart} + updateMassPosX={NumCast(this.dataDoc.mass1_xChange)} + updateMassPosY={NumCast(this.dataDoc.mass1_yChange)} + forcesUpdated={this.forcesUpdated1} + setForcesUpdated={this.setForcesUpdated1} + setPosition={this.setPosition1} + setVelocity={this.setVelocity1} + setAcceleration={this.setAcceleration1} + /> + {this.simulationType == 'Pulley' && ( + <Weight + {...commonWeightProps} + color="green" + componentForces={this.componentForces2} + setComponentForces={this.setComponentForces2} + displayXVelocity={NumCast(this.dataDoc.mass2_velocityX)} + displayYVelocity={NumCast(this.dataDoc.mass2_velocityY)} + mass={this.mass2} + startForces={this.startForces2} + startPosX={this.mass2PosXStart} + startPosY={this.mass2PosYStart} + startVelX={this.mass2VelXStart} + startVelY={this.mass2VelYStart} + updateMassPosX={NumCast(this.dataDoc.mass2_xChange)} + updateMassPosY={NumCast(this.dataDoc.mass2_yChange)} + forcesUpdated={this.forcesUpdated2} + setForcesUpdated={this.setForcesUpdated2} + setPosition={this.setPosition2} + setVelocity={this.setVelocity2} + setAcceleration={this.setAcceleration2} + /> + )} + </div> + <div style={{ position: 'absolute', transformOrigin: 'top left', top: 0, left: 0, width: '100%', height: '100%' }}> + {(this.simulationType == 'One Weight' || this.simulationType == 'Inclined Plane') && + this.wallPositions?.map((element, index) => <Wall key={index} length={element.length} xPos={element.xPos} yPos={element.yPos} angleInDegrees={element.angleInDegrees} />)} + </div> + </div> + </div> + <div + className="mechanicsSimulationEquationContainer" + onWheel={e => this.props.isContentActive() && e.stopPropagation()} + style={{ overflow: 'auto', height: `${Math.max(1, 800 / this.props.PanelWidth()) * 100}%`, transform: `scale(${Math.min(1, this.props.PanelWidth() / 850)})` }}> + <div className="mechanicsSimulationControls"> + <Stack direction="row" spacing={1}> + {this.dataDoc.simulation_paused && this.simulationMode != 'Tutorial' && ( + <IconButton onClick={() => (this.dataDoc.simulation_paused = false)}> + <PlayArrowIcon /> + </IconButton> + )} + {!this.dataDoc.simulation_paused && this.simulationMode != 'Tutorial' && ( + <IconButton onClick={() => (this.dataDoc.simulation_paused = true)}> + <PauseIcon /> + </IconButton> + )} + {this.dataDoc.simulation_paused && this.simulationMode != 'Tutorial' && ( + <IconButton onClick={action(() => this._simReset++)}> + <ReplayIcon /> + </IconButton> + )} + </Stack> + <div className="dropdownMenu"> + <select + value={StrCast(this.simulationType)} + onChange={event => { + this.dataDoc.simulation_type = event.target.value; + this.setupSimulation(); + }} + style={{ height: '2em', width: '100%', fontSize: '16px' }}> + <option value="One Weight">Projectile</option> + <option value="Inclined Plane">Inclined Plane</option> + <option value="Pendulum">Pendulum</option> + <option value="Spring">Spring</option> + <option value="Circular Motion">Circular Motion</option> + <option value="Pulley">Pulley</option> + <option value="Suspension">Suspension</option> + </select> + </div> + <div className="dropdownMenu"> + <select + value={this.simulationMode} + onChange={event => { + this.dataDoc.simulation_mode = event.target.value; + this.setupSimulation(); + }} + style={{ height: '2em', width: '100%', fontSize: '16px' }}> + <option value="Tutorial">Tutorial Mode</option> + <option value="Freeform">Freeform Mode</option> + <option value="Review">Review Mode</option> + </select> + </div> + </div> + {this.simulationMode == 'Review' && this.simulationType != 'Inclined Plane' && ( + <div className="wordProblemBox"> + <p> + <>{this.simulationType} review problems in progress!</> + </p> + <hr /> + </div> + )} + {this.simulationMode == 'Review' && this.simulationType == 'Inclined Plane' && ( + <div> + {!this.dataDoc.hintDialogueOpen && ( + <IconButton + onClick={() => (this.dataDoc.hintDialogueOpen = true)} + sx={{ + position: 'fixed', + left: this.xMax - 50 + 'px', + top: this.yMin + 14 + 'px', + }}> + <QuestionMarkIcon /> + </IconButton> + )} + <Dialog maxWidth={'sm'} fullWidth={true} open={BoolCast(this.dataDoc.hintDialogueOpen)} onClose={() => (this.dataDoc.hintDialogueOpen = false)}> + <DialogTitle>Hints</DialogTitle> + <DialogContent> + {this.selectedQuestion.hints?.map((hint: any, index: number) => ( + <div key={index}> + <DialogContentText> + <details> + <summary> + <b> + Hint {index + 1}: {hint.description} + </b> + </summary> + {hint.content} + </details> + </DialogContentText> + </div> + ))} + </DialogContent> + <DialogActions> + <Button onClick={() => (this.dataDoc.hintDialogueOpen = false)}>Close</Button> + </DialogActions> + </Dialog> + <div className="wordProblemBox"> + <div className="question"> + <p>{this.questionPartOne}</p> + <p>{this.questionPartTwo}</p> + </div> + <div className="answers"> + {this.selectedQuestion.answerParts.includes('force of gravity') && ( + <InputField + label={<p>Gravity magnitude</p>} + lowerBound={0} + dataDoc={this.dataDoc} + prop="review_GravityMagnitude" + step={0.1} + unit={'N'} + upperBound={50} + value={NumCast(this.dataDoc.review_GravityMagnitude)} + showIcon={BoolCast(this.dataDoc.simulation_showIcon)} + correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('force of gravity')]} + labelWidth={'7em'} + /> + )} + {this.selectedQuestion.answerParts.includes('angle of gravity') && ( + <InputField + label={<p>Gravity angle</p>} + lowerBound={0} + dataDoc={this.dataDoc} + prop="review_GravityAngle" + step={1} + unit={'°'} + upperBound={360} + value={NumCast(this.dataDoc.review_GravityAngle)} + radianEquivalent={true} + showIcon={BoolCast(this.dataDoc.simulation_showIcon)} + correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('angle of gravity')]} + labelWidth={'7em'} + /> + )} + {this.selectedQuestion.answerParts.includes('normal force') && ( + <InputField + label={<p>Normal force magnitude</p>} + lowerBound={0} + dataDoc={this.dataDoc} + prop="review_NormalMagnitude" + step={0.1} + unit={'N'} + upperBound={50} + value={NumCast(this.dataDoc.review_NormalMagnitude)} + showIcon={BoolCast(this.dataDoc.simulation_showIcon)} + correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('normal force')]} + labelWidth={'7em'} + /> + )} + {this.selectedQuestion.answerParts.includes('angle of normal force') && ( + <InputField + label={<p>Normal force angle</p>} + lowerBound={0} + dataDoc={this.dataDoc} + prop="review_NormalAngle" + step={1} + unit={'°'} + upperBound={360} + value={NumCast(this.dataDoc.review_NormalAngle)} + radianEquivalent={true} + showIcon={BoolCast(this.dataDoc.simulation_showIcon)} + correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('angle of normal force')]} + labelWidth={'7em'} + /> + )} + {this.selectedQuestion.answerParts.includes('force of static friction') && ( + <InputField + label={<p>Static friction magnitude</p>} + lowerBound={0} + dataDoc={this.dataDoc} + prop="review_StaticMagnitude" + step={0.1} + unit={'N'} + upperBound={50} + value={NumCast(this.dataDoc.review_StaticMagnitude)} + showIcon={BoolCast(this.dataDoc.simulation_showIcon)} + correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('force of static friction')]} + labelWidth={'7em'} + /> + )} + {this.selectedQuestion.answerParts.includes('angle of static friction') && ( + <InputField + label={<p>Static friction angle</p>} + lowerBound={0} + dataDoc={this.dataDoc} + prop="review_StaticAngle" + step={1} + unit={'°'} + upperBound={360} + value={NumCast(this.dataDoc.review_StaticAngle)} + radianEquivalent={true} + showIcon={BoolCast(this.dataDoc.simulation_showIcon)} + correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('angle of static friction')]} + labelWidth={'7em'} + /> + )} + {this.selectedQuestion.answerParts.includes('coefficient of static friction') && ( + <InputField + label={ + <Box> + μ<sub>s</sub> + </Box> + } + lowerBound={0} + dataDoc={this.dataDoc} + prop="coefficientOfStaticFriction" + step={0.1} + unit={''} + upperBound={1} + value={NumCast(this.dataDoc.coefficientOfStaticFriction)} + effect={this.updateReviewForcesBasedOnCoefficient} + showIcon={BoolCast(this.dataDoc.simulation_showIcon)} + correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('coefficient of static friction')]} + /> + )} + {this.selectedQuestion.answerParts.includes('wedge angle') && ( + <InputField + label={<Box>θ</Box>} + lowerBound={0} + dataDoc={this.dataDoc} + prop="wedge_angle" + step={1} + unit={'°'} + upperBound={49} + value={this.wedgeAngle} + effect={(val: number) => { + this.changeWedgeBasedOnNewAngle(val); + this.updateReviewForcesBasedOnAngle(val); + }} + radianEquivalent={true} + showIcon={BoolCast(this.dataDoc.simulation_showIcon)} + correctValue={NumListCast(this.dataDoc.answers)[this.selectedQuestion.answerParts.indexOf('wedge angle')]} + /> + )} + </div> + </div> + </div> + )} + {this.simulationMode == 'Tutorial' && ( + <div className="wordProblemBox"> + <div className="question"> + <h2>Problem</h2> + <p>{this.tutorial.question}</p> + </div> + <div + style={{ + display: 'flex', + justifyContent: 'spaceBetween', + width: '100%', + }}> + <IconButton + onClick={() => { + let step = NumCast(this.dataDoc.tutorial_stepNumber) - 1; + step = Math.max(step, 0); + step = Math.min(step, this.tutorial.steps.length - 1); + this.dataDoc.tutorial_stepNumber = step; + this.dataDoc.mass1_forcesStart = JSON.stringify(this.tutorial.steps[step].forces); + this.dataDoc.mass1_forcesUpdated = JSON.stringify(this.tutorial.steps[step].forces); + this.dataDoc.simulation_showForceMagnitudes = this.tutorial.steps[step].showMagnitude; + }} + disabled={this.dataDoc.tutorial_stepNumber == 0}> + <ArrowLeftIcon /> + </IconButton> + <div> + <h3> + Step {NumCast(this.dataDoc.tutorial_stepNumber) + 1}: {this.tutorial.steps[NumCast(this.dataDoc.tutorial_stepNumber)].description} + </h3> + <p>{this.tutorial.steps[NumCast(this.dataDoc.tutorial_stepNumber)].content}</p> + </div> + <IconButton + onClick={() => { + let step = NumCast(this.dataDoc.tutorial_stepNumber) + 1; + step = Math.max(step, 0); + step = Math.min(step, this.tutorial.steps.length - 1); + this.dataDoc.tutorial_stepNumber = step; + this.dataDoc.mass1_forcesStart = JSON.stringify(this.tutorial.steps[step].forces); + this.dataDoc.mass1_forcesUpdated = JSON.stringify(this.tutorial.steps[step].forces); + this.dataDoc.simulation_showForceMagnitudes = this.tutorial.steps[step].showMagnitude; + }} + disabled={this.dataDoc.tutorial_stepNumber === this.tutorial.steps.length - 1}> + <ArrowRightIcon /> + </IconButton> + </div> + <div> + {(this.simulationType == 'One Weight' || this.simulationType == 'Inclined Plane' || this.simulationType == 'Pendulum') && <p>Resources</p>} + {this.simulationType == 'One Weight' && ( + <ul> + <li> + <a + href="https://www.khanacademy.org/science/physics/one-dimensional-motion" + target="_blank" + rel="noreferrer" + style={{ + color: 'blue', + textDecoration: 'underline', + }}> + Khan Academy - One Dimensional Motion + </a> + </li> + <li> + <a + href="https://www.khanacademy.org/science/physics/two-dimensional-motion" + target="_blank" + rel="noreferrer" + style={{ + color: 'blue', + textDecoration: 'underline', + }}> + Khan Academy - Two Dimensional Motion + </a> + </li> + </ul> + )} + {this.simulationType == 'Inclined Plane' && ( + <ul> + <li> + <a + href="https://www.khanacademy.org/science/physics/forces-newtons-laws#normal-contact-force" + target="_blank" + rel="noreferrer" + style={{ + color: 'blue', + textDecoration: 'underline', + }}> + Khan Academy - Normal Force + </a> + </li> + <li> + <a + href="https://www.khanacademy.org/science/physics/forces-newtons-laws#inclined-planes-friction" + target="_blank" + rel="noreferrer" + style={{ + color: 'blue', + textDecoration: 'underline', + }}> + Khan Academy - Inclined Planes + </a> + </li> + </ul> + )} + {this.simulationType == 'Pendulum' && ( + <ul> + <li> + <a + href="https://www.khanacademy.org/science/physics/forces-newtons-laws#tension-tutorial" + target="_blank" + rel="noreferrer" + style={{ + color: 'blue', + textDecoration: 'underline', + }}> + Khan Academy - Tension + </a> + </li> + </ul> + )} + </div> + </div> + )} + {this.simulationMode == 'Review' && this.simulationType == 'Inclined Plane' && ( + <div + style={{ + display: 'flex', + justifyContent: 'space-between', + marginTop: '10px', + }}> + <p + style={{ + color: 'blue', + textDecoration: 'underline', + cursor: 'pointer', + }} + onClick={() => (this.dataDoc.simulation_mode = 'Tutorial')}> + {' '} + Go to walkthrough{' '} + </p> + <div style={{ display: 'flex', flexDirection: 'column' }}> + <Button + onClick={action(() => { + this._simReset++; + this.checkAnswers(); + this.dataDoc.simulation_showIcon = true; + })} + variant="outlined"> + <p>Submit</p> + </Button> + <Button + onClick={() => { + this.generateNewQuestion(); + this.dataDoc.simulation_showIcon = false; + }} + variant="outlined"> + <p>New question</p> + </Button> + </div> + </div> + )} + {this.simulationMode == 'Freeform' && ( + <div className="vars"> + <FormControl component="fieldset"> + <FormGroup> + {this.simulationType == 'One Weight' && ( + <FormControlLabel + control={<Checkbox checked={BoolCast(this.dataDoc.elasticCollisions)} onChange={() => (this.dataDoc.elasticCollisions = !this.dataDoc.elasticCollisions)} />} + label="Make collisions elastic" + labelPlacement="start" + /> + )} + <FormControlLabel + control={<Checkbox checked={BoolCast(this.dataDoc.simulation_showForces)} onChange={() => (this.dataDoc.simulation_showForces = !this.dataDoc.simulation_showForces)} />} + label="Show force vectors" + labelPlacement="start" + /> + {(this.simulationType == 'Inclined Plane' || this.simulationType == 'Pendulum') && ( + <FormControlLabel + control={<Checkbox checked={BoolCast(this.dataDoc.simulation_showComponentForces)} onChange={() => (this.dataDoc.simulation_showComponentForces = !this.dataDoc.simulation_showComponentForces)} />} + label="Show component force vectors" + labelPlacement="start" + /> + )} + <FormControlLabel + control={<Checkbox checked={BoolCast(this.dataDoc.simulation_showAcceleration)} onChange={() => (this.dataDoc.simulation_showAcceleration = !this.dataDoc.simulation_showAcceleration)} />} + label="Show acceleration vector" + labelPlacement="start" + /> + <FormControlLabel + control={<Checkbox checked={BoolCast(this.dataDoc.simulation_showVelocity)} onChange={() => (this.dataDoc.simulation_showVelocity = !this.dataDoc.simulation_showVelocity)} />} + label="Show velocity vector" + labelPlacement="start" + /> + <InputField label={<Box>Speed</Box>} lowerBound={1} dataDoc={this.dataDoc} prop="simulation_speed" step={1} unit={'x'} upperBound={10} value={NumCast(this.dataDoc.simulation_speed, 2)} labelWidth={'5em'} /> + {this.dataDoc.simulation_paused && this.simulationType != 'Circular Motion' && ( + <InputField + label={<Box>Gravity</Box>} + lowerBound={-30} + dataDoc={this.dataDoc} + prop="gravity" + step={0.01} + unit={'m/s2'} + upperBound={0} + value={NumCast(this.dataDoc.simulation_gravity, -9.81)} + effect={(val: number) => this.setupSimulation()} + labelWidth={'5em'} + /> + )} + {this.dataDoc.simulation_paused && this.simulationType != 'Pulley' && ( + <InputField + label={<Box>Mass</Box>} + lowerBound={1} + dataDoc={this.dataDoc} + prop="mass1" + step={0.1} + unit={'kg'} + upperBound={5} + value={this.mass1 ?? 1} + effect={(val: number) => this.setupSimulation()} + labelWidth={'5em'} + /> + )} + {this.dataDoc.simulation_paused && this.simulationType == 'Pulley' && ( + <InputField + label={<Box>Red mass</Box>} + lowerBound={1} + dataDoc={this.dataDoc} + prop="mass1" + step={0.1} + unit={'kg'} + upperBound={5} + value={this.mass1 ?? 1} + effect={(val: number) => this.setupSimulation()} + labelWidth={'5em'} + /> + )} + {this.dataDoc.simulation_paused && this.simulationType == 'Pulley' && ( + <InputField + label={<Box>Blue mass</Box>} + lowerBound={1} + dataDoc={this.dataDoc} + prop="mass2" + step={0.1} + unit={'kg'} + upperBound={5} + value={this.mass2 ?? 1} + effect={(val: number) => this.setupSimulation()} + labelWidth={'5em'} + /> + )} + {this.dataDoc.simulation_paused && this.simulationType == 'Circular Motion' && ( + <InputField + label={<Box>Rod length</Box>} + lowerBound={100} + dataDoc={this.dataDoc} + prop="circularMotionRadius" + step={5} + unit={'m'} + upperBound={250} + value={this.circularMotionRadius} + effect={(val: number) => this.setupSimulation()} + labelWidth={'5em'} + /> + )} + </FormGroup> + </FormControl> + {this.simulationType == 'Spring' && this.dataDoc.simulation_paused && ( + <div> + <InputField + label={<Typography color="inherit">Spring stiffness</Typography>} + lowerBound={0.1} + dataDoc={this.dataDoc} + prop="spring_constant" + step={1} + unit={'N/m'} + upperBound={500} + value={this.springConstant} + effect={action(() => this._simReset++)} + radianEquivalent={false} + mode={'Freeform'} + labelWidth={'7em'} + /> + <InputField + label={<Typography color="inherit">Rest length</Typography>} + lowerBound={10} + dataDoc={this.dataDoc} + prop="spring_lengthRest" + step={100} + unit="" + upperBound={500} + value={this.springLengthRest} + effect={action(() => this._simReset++)} + radianEquivalent={false} + mode="Freeform" + labelWidth={'7em'} + /> + <InputField + label={<Typography color="inherit">Starting displacement</Typography>} + lowerBound={-(this.springLengthRest - 10)} + dataDoc={this.dataDoc} + prop="" + step={10} + unit="" + upperBound={this.springLengthRest} + value={this.springLengthStart - this.springLengthRest} + effect={action((val: number) => { + this.dataDoc.mass1_positionYstart = this.springLengthRest + val; + this.dataDoc.spring_lengthStart = this.springLengthRest + val; + this._simReset++; + })} + radianEquivalent={false} + mode="Freeform" + labelWidth={'7em'} + /> + </div> + )} + {this.simulationType == 'Inclined Plane' && this.dataDoc.simulation_paused && ( + <div> + <InputField + label={<Box>θ</Box>} + lowerBound={0} + dataDoc={this.dataDoc} + prop="wedge_angle" + step={1} + unit={'°'} + upperBound={49} + value={this.wedgeAngle} + effect={action((val: number) => { + this.changeWedgeBasedOnNewAngle(val); + this._simReset++; + })} + radianEquivalent={true} + mode={'Freeform'} + labelWidth={'2em'} + /> + <InputField + label={ + <Box> + μ<sub>s</sub> + </Box> + } + lowerBound={0} + dataDoc={this.dataDoc} + prop="coefficientOfStaticFriction" + step={0.1} + unit={''} + upperBound={1} + value={NumCast(this.dataDoc.coefficientOfStaticFriction) ?? 0} + effect={action((val: number) => { + this.updateForcesWithFriction(val); + if (val < NumCast(this.dataDoc.coefficientOfKineticFriction)) { + this.dataDoc.soefficientOfKineticFriction = val; + } + this._simReset++; + })} + mode={'Freeform'} + labelWidth={'2em'} + /> + <InputField + label={ + <Box> + μ<sub>k</sub> + </Box> + } + lowerBound={0} + dataDoc={this.dataDoc} + prop="coefficientOfKineticFriction" + step={0.1} + unit={''} + upperBound={NumCast(this.dataDoc.coefficientOfStaticFriction)} + value={NumCast(this.dataDoc.coefficientOfKineticFriction) ?? 0} + effect={action(() => this._simReset++)} + mode={'Freeform'} + labelWidth={'2em'} + /> + </div> + )} + {this.simulationType == 'Inclined Plane' && !this.dataDoc.simulation_paused && ( + <Typography> + <> + θ: {Math.round(this.wedgeAngle * 100) / 100}° ≈ {Math.round(((this.wedgeAngle * Math.PI) / 180) * 100) / 100} rad + <br /> + μ <sub>s</sub>: {this.dataDoc.coefficientOfStaticFriction} + <br /> + μ <sub>k</sub>: {this.dataDoc.coefficientOfKineticFriction} + </> + </Typography> + )} + {this.simulationType == 'Pendulum' && !this.dataDoc.simulation_paused && ( + <Typography> + θ: {Math.round(this.pendulumAngle * 100) / 100}° ≈ {Math.round(((this.pendulumAngle * Math.PI) / 180) * 100) / 100} rad + </Typography> + )} + {this.simulationType == 'Pendulum' && this.dataDoc.simulation_paused && ( + <div> + <InputField + label={<Box>Angle</Box>} + lowerBound={0} + dataDoc={this.dataDoc} + prop="pendulum_angle" + step={1} + unit={'°'} + upperBound={59} + value={NumCast(this.dataDoc.pendulum_angle, 30)} + effect={action(value => { + this.dataDoc.pendulum_angleStart = value; + this.dataDoc.pendulum_lengthStart = this.dataDoc.pendulum_length; + if (this.simulationType == 'Pendulum') { + const mag = this.mass1 * Math.abs(this.gravity) * Math.cos((value * Math.PI) / 180); + + const forceOfTension: IForce = { + description: 'Tension', + magnitude: mag, + directionInDegrees: 90 - value, + }; + const gravityParallel: IForce = { + description: 'Gravity Parallel Component', + magnitude: Math.abs(this.gravity) * Math.cos((value * Math.PI) / 180), + directionInDegrees: 270 - value, + }; + const gravityPerpendicular: IForce = { + description: 'Gravity Perpendicular Component', + magnitude: Math.abs(this.gravity) * Math.sin((value * Math.PI) / 180), + directionInDegrees: -value, + }; + + const length = this.pendulumLength; + const x = length * Math.cos(((90 - value) * Math.PI) / 180); + const y = length * Math.sin(((90 - value) * Math.PI) / 180); + const xPos = this.xMax / 2 - x - NumCast(this.dataDoc.radius); + const yPos = y - NumCast(this.dataDoc.radius) - 5; + this.dataDoc.mass1_positionXstart = xPos; + this.dataDoc.mass1_positionYstart = yPos; + + this.dataDoc.mass1_forcesStart = JSON.stringify([this.gravityForce(this.mass1), forceOfTension]); + this.dataDoc.mass1_forcesUpdated = JSON.stringify([this.gravityForce(this.mass1), forceOfTension]); + this.dataDoc.mass1_componentForces = JSON.stringify([forceOfTension, gravityParallel, gravityPerpendicular]); + this._simReset++; + } + })} + radianEquivalent={true} + mode="Freeform" + labelWidth="5em" + /> + <InputField + label={<Box>Rod length</Box>} + lowerBound={0} + dataDoc={this.dataDoc} + prop="pendulum_length" + step={1} + unit="m" + upperBound={400} + value={Math.round(this.pendulumLength)} + effect={action(value => { + if (this.simulationType == 'Pendulum') { + this.dataDoc.pendulum_angleStart = this.pendulumAngle; + this.dataDoc.pendulum_lengthStart = value; + this._simReset++; + } + })} + radianEquivalent={false} + mode="Freeform" + labelWidth="5em" + /> + </div> + )} + </div> + )} + <div className="mechanicsSimulationEquation"> + {this.simulationMode == 'Freeform' && ( + <table> + <tbody> + <tr> + <td>{this.simulationType == 'Pulley' ? 'Red Weight' : ''}</td> + <td>X</td> + <td>Y</td> + </tr> + <tr> + <td + style={{ cursor: 'help' }} + // onClick={() => { + // window.open( + // "https://www.khanacademy.org/science/physics/two-dimensional-motion" + // ); + // }} + > + <Box>Position</Box> + </td> + {(!this.dataDoc.simulation_paused || this.simulationType == 'Inclined Plane' || this.simulationType == 'Circular Motion' || this.simulationType == 'Pulley') && ( + <td style={{ cursor: 'default' }}> + <>{this.dataDoc.mass1_positionX} m</> + </td> + )}{' '} + {this.dataDoc.simulation_paused && this.simulationType != 'Inclined Plane' && this.simulationType != 'Circular Motion' && this.simulationType != 'Pulley' && ( + <td + style={{ + cursor: 'default', + }}> + <InputField + lowerBound={this.simulationType == 'Projectile' ? 1 : (this.xMax + this.xMin) / 4 - this.radius - 15} + dataDoc={this.dataDoc} + prop="mass1_positionX" + step={1} + unit={'m'} + upperBound={this.simulationType == 'Projectile' ? this.xMax - 110 : (3 * (this.xMax + this.xMin)) / 4 - this.radius / 2 - 15} + value={NumCast(this.dataDoc.mass1_positionX)} + effect={value => { + this.dataDoc.mass1_xChange = value; + if (this.simulationType == 'Suspension') { + let x1rod = (this.xMax + this.xMin) / 2 - this.radius - this.yMin - 200; + let x2rod = (this.xMax + this.xMin) / 2 + this.yMin + 200 + this.radius; + let deltaX1 = value + this.radius - x1rod; + let deltaX2 = x2rod - (value + this.radius); + let deltaY = this.getYPosFromDisplay(NumCast(this.dataDoc.mass1_positionY)) + this.radius; + let dir1T = Math.PI - Math.atan(deltaY / deltaX1); + let dir2T = Math.atan(deltaY / deltaX2); + let tensionMag2 = (this.mass1 * Math.abs(this.gravity)) / ((-Math.cos(dir2T) / Math.cos(dir1T)) * Math.sin(dir1T) + Math.sin(dir2T)); + let tensionMag1 = (-tensionMag2 * Math.cos(dir2T)) / Math.cos(dir1T); + dir1T = (dir1T * 180) / Math.PI; + dir2T = (dir2T * 180) / Math.PI; + const tensionForce1: IForce = { + description: 'Tension', + magnitude: tensionMag1, + directionInDegrees: dir1T, + }; + const tensionForce2: IForce = { + description: 'Tension', + magnitude: tensionMag2, + directionInDegrees: dir2T, + }; + const gravity = this.gravityForce(this.mass1); + this.dataDoc.mass1_forcesUpdated = JSON.stringify([tensionForce1, tensionForce2, gravity]); + } + }} + small={true} + mode="Freeform" + /> + </td> + )}{' '} + {(!this.dataDoc.simulation_paused || this.simulationType == 'Inclined Plane' || this.simulationType == 'Circular Motion' || this.simulationType == 'Pulley') && ( + <td style={{ cursor: 'default' }}>{`${NumCast(this.dataDoc.mass1_positionY)} m`}</td> + )}{' '} + {this.dataDoc.simulation_paused && this.simulationType != 'Inclined Plane' && this.simulationType != 'Circular Motion' && this.simulationType != 'Pulley' && ( + <td + style={{ + cursor: 'default', + }}> + <InputField + lowerBound={1} + dataDoc={this.dataDoc} + prop="mass1_positionY" + step={1} + unit="m" + upperBound={this.yMax - 110} + value={NumCast(this.dataDoc.mass1_positionY)} + effect={value => { + this.dataDoc.mass1_yChange = value; + if (this.simulationType == 'Suspension') { + let x1rod = (this.xMax + this.xMin) / 2 - this.radius - this.yMin - 200; + let x2rod = (this.xMax + this.xMin) / 2 + this.yMin + 200 + this.radius; + let deltaX1 = NumCast(this.dataDoc.mass1_positionX) + this.radius - x1rod; + let deltaX2 = x2rod - (NumCast(this.dataDoc.mass1_positionX) + this.radius); + let deltaY = this.getYPosFromDisplay(value) + this.radius; + let dir1T = Math.PI - Math.atan(deltaY / deltaX1); + let dir2T = Math.atan(deltaY / deltaX2); + let tensionMag2 = (this.mass1 * Math.abs(this.gravity)) / ((-Math.cos(dir2T) / Math.cos(dir1T)) * Math.sin(dir1T) + Math.sin(dir2T)); + let tensionMag1 = (-tensionMag2 * Math.cos(dir2T)) / Math.cos(dir1T); + dir1T = (dir1T * 180) / Math.PI; + dir2T = (dir2T * 180) / Math.PI; + const tensionForce1: IForce = { + description: 'Tension', + magnitude: tensionMag1, + directionInDegrees: dir1T, + }; + const tensionForce2: IForce = { + description: 'Tension', + magnitude: tensionMag2, + directionInDegrees: dir2T, + }; + const gravity = this.gravityForce(this.mass1); + this.dataDoc.mass1_forcesUpdated = JSON.stringify([tensionForce1, tensionForce2, gravity]); + } + }} + small={true} + mode="Freeform" + /> + </td> + )}{' '} + </tr> + <tr> + <td + style={{ cursor: 'help' }} + // onClick={() => { + // window.open( + // "https://www.khanacademy.org/science/physics/two-dimensional-motion" + // ); + // }} + > + <Box>Velocity</Box> + </td> + {(!this.dataDoc.simulation_paused || (this.simulationType != 'One Weight' && this.simulationType != 'Circular Motion')) && ( + <td style={{ cursor: 'default' }}>{`${NumCast(this.dataDoc.mass1_velocityX)} m/s`}</td> + )}{' '} + {this.dataDoc.simulation_paused && (this.simulationType == 'One Weight' || this.simulationType == 'Circular Motion') && ( + <td + style={{ + cursor: 'default', + }}> + <InputField + lowerBound={-50} + dataDoc={this.dataDoc} + prop="mass1_velocityX" + step={1} + unit={'m/s'} + upperBound={50} + value={NumCast(this.dataDoc.mass1_velocityX)} + effect={action(value => { + this.dataDoc.mass1_velocityXstart = value; + this._simReset++; + })} + small={true} + mode="Freeform" + /> + </td> + )}{' '} + {(!this.dataDoc.simulation_paused || this.simulationType != 'One Weight') && ( + <td style={{ cursor: 'default' }}> + <>{this.dataDoc.mass1_velocityY} m/s</> + </td> + )}{' '} + {this.dataDoc.simulation_paused && this.simulationType == 'One Weight' && ( + <td + style={{ + cursor: 'default', + }}> + <InputField + lowerBound={-50} + dataDoc={this.dataDoc} + prop="mass1_velocityY" + step={1} + unit="m/s" + upperBound={50} + value={NumCast(this.dataDoc.mass1_velocityY)} + effect={value => { + this.dataDoc.mass1_velocityYstart = -value; + }} + small={true} + mode="Freeform" + /> + </td> + )}{' '} + </tr> + <tr> + <td + style={{ cursor: 'help' }} + // onClick={() => { + // window.open( + // "https://www.khanacademy.org/science/physics/two-dimensional-motion" + // ); + // }} + > + <Box>Acceleration</Box> + </td> + <td style={{ cursor: 'default' }}> + <> + {this.dataDoc.mass1_accelerationX} m/s<sup>2</sup> + </> + </td> + <td style={{ cursor: 'default' }}> + <> + {this.dataDoc.mass1_accelerationY} m/s<sup>2</sup> + </> + </td> + </tr> + <tr> + <td> + <Box>Momentum</Box> + </td> + <td>{Math.round(NumCast(this.dataDoc.mass1_velocityX) * this.mass1 * 10) / 10} kg*m/s</td> + <td>{Math.round(NumCast(this.dataDoc.mass1_velocityY) * this.mass1 * 10) / 10} kg*m/s</td> + </tr> + </tbody> + </table> + )} + {this.simulationMode == 'Freeform' && this.simulationType == 'Pulley' && ( + <table> + <tbody> + <tr> + <td>Blue Weight</td> + <td>X</td> + <td>Y</td> + </tr> + <tr> + <td> + <Box>Position</Box> + </td> + <td style={{ cursor: 'default' }}>{`${this.dataDoc.mass2_positionX} m`}</td> + <td style={{ cursor: 'default' }}>{`${this.dataDoc.mass2_positionY} m`}</td> + </tr> + <tr> + <td> + <Box>Velocity</Box> + </td> + <td style={{ cursor: 'default' }}>{`${this.dataDoc.mass2_positionX} m/s`}</td> + <td style={{ cursor: 'default' }}>{`${this.dataDoc.mass2_positionY} m/s`}</td> + </tr> + <tr> + <td> + <Box>Acceleration</Box> + </td> + <td style={{ cursor: 'default' }}> + <> + {this.dataDoc.mass2_accelerationX} m/s<sup>2</sup> + </> + </td> + <td style={{ cursor: 'default' }}> + <> + {this.dataDoc.mass2_accelerationY} m/s<sup>2</sup> + </> + </td> + </tr> + <tr> + <td> + <Box>Momentum</Box> + </td> + <td>{Math.round(NumCast(this.dataDoc.mass2_velocityX) * this.mass1 * 10) / 10} kg*m/s</td> + <td>{Math.round(NumCast(this.dataDoc.mass2_velocityY) * this.mass1 * 10) / 10} kg*m/s</td> + </tr> + </tbody> + </table> + )} + </div> + {this.simulationType != 'Pendulum' && this.simulationType != 'Spring' && ( + <div> + <p>Kinematic Equations</p> + <ul> + <li> + Position: x<sub>1</sub>=x<sub>0</sub>+v<sub>0</sub>t+ + <sup>1</sup>⁄ + <sub>2</sub>at + <sup>2</sup> + </li> + <li> + Velocity: v<sub>1</sub>=v<sub>0</sub>+at + </li> + <li>Acceleration: a = F/m</li> + </ul> + </div> + )} + {this.simulationType == 'Spring' && ( + <div> + <p>Harmonic Motion Equations: Spring</p> + <ul> + <li> + Spring force: F<sub>s</sub>=kd + </li> + <li> + Spring period: T<sub>s</sub>=2π√<sup>m</sup>⁄ + <sub>k</sub> + </li> + <li>Equilibrium displacement for vertical spring: d = mg/k</li> + <li> + Elastic potential energy: U<sub>s</sub>=<sup>1</sup>⁄ + <sub>2</sub>kd<sup>2</sup> + </li> + <ul> + <li>Maximum when system is at maximum displacement, 0 when system is at 0 displacement</li> + </ul> + <li> + Translational kinetic energy: K=<sup>1</sup>⁄ + <sub>2</sub>mv<sup>2</sup> + </li> + <ul> + <li>Maximum when system is at maximum/minimum velocity (at 0 displacement), 0 when velocity is 0 (at maximum displacement)</li> + </ul> + </ul> + </div> + )} + {this.simulationType == 'Pendulum' && ( + <div> + <p>Harmonic Motion Equations: Pendulum</p> + <ul> + <li> + Pendulum period: T<sub>p</sub>=2π√<sup>l</sup>⁄ + <sub>g</sub> + </li> + </ul> + </div> + )} + </div> + </div> + <div + style={{ + position: 'fixed', + top: this.yMax - 120 + 20 + 'px', + left: this.xMin + 90 - 80 + 'px', + zIndex: -10000, + }}> + <svg width={100 + 'px'} height={100 + 'px'}> + <defs> + <marker id="miniArrow" markerWidth="20" markerHeight="20" refX="0" refY="3" orient="auto" markerUnits="strokeWidth"> + <path d="M0,0 L0,6 L9,3 z" fill={'#000000'} /> + </marker> + </defs> + <line x1={20} y1={70} x2={70} y2={70} stroke={'#000000'} strokeWidth="2" markerEnd="url(#miniArrow)" /> + <line x1={20} y1={70} x2={20} y2={20} stroke={'#000000'} strokeWidth="2" markerEnd="url(#miniArrow)" /> + </svg> + <p + style={{ + position: 'fixed', + top: this.yMax - 120 + 40 + 'px', + left: this.xMin + 90 - 80 + 'px', + }}> + {this.simulationType == 'Circular Motion' ? 'Z' : 'Y'} + </p> + <p + style={{ + position: 'fixed', + top: this.yMax - 120 + 80 + 'px', + left: this.xMin + 90 - 40 + 'px', + }}> + X + </p> + </div> + </div> + ); + } +} diff --git a/src/client/views/nodes/PhysicsBox/PhysicsSimulationInputField.tsx b/src/client/views/nodes/PhysicsBox/PhysicsSimulationInputField.tsx new file mode 100644 index 000000000..d595a499e --- /dev/null +++ b/src/client/views/nodes/PhysicsBox/PhysicsSimulationInputField.tsx @@ -0,0 +1,179 @@ +import { TextField, InputAdornment } from '@mui/material'; +import { Doc } from '../../../../fields/Doc'; +import React = require('react'); +import TaskAltIcon from '@mui/icons-material/TaskAlt'; +import ErrorOutlineIcon from '@mui/icons-material/ErrorOutline'; +import { isNumber } from 'lodash'; +export interface IInputProps { + label?: JSX.Element; + lowerBound: number; + dataDoc: Doc; + prop: string; + step: number; + unit: string; + upperBound: number; + value: number | string | Array<number | string>; + correctValue?: number; + showIcon?: boolean; + effect?: (val: number) => any; + radianEquivalent?: boolean; + small?: boolean; + mode?: string; + update?: boolean; + labelWidth?: string; +} + +interface IState { + tempValue: string | number | (string | number)[]; + tempRadianValue: number; + width: string; + margin: string; +} + +export default class InputField extends React.Component<IInputProps, IState> { + constructor(props: any) { + super(props); + this.state = { + tempValue: this.props.mode != 'Freeform' && !this.props.showIcon ? 0 : this.props.value, + tempRadianValue: this.props.mode != 'Freeform' && !this.props.showIcon ? 0 : (Number(this.props.value) * Math.PI) / 180, + width: this.props.small ? '6em' : '7em', + margin: this.props.small ? '0px' : '10px', + }; + } + + epsilon: number = 0.01; + + componentDidMount(): void { + this.setState({ tempValue: Number(this.props.value) }); + } + + componentDidUpdate(prevProps: Readonly<IInputProps>, prevState: Readonly<IState>, snapshot?: any): void { + if (prevProps.value != this.props.value && isNumber(this.props.value) && !isNaN(this.props.value)) { + if (this.props.mode == 'Freeform') { + if (isNumber(this.state.tempValue) && Math.abs(this.state.tempValue - Number(this.props.value)) > 1) { + this.setState({ tempValue: Number(this.props.value) }); + } + } + } + if (prevProps.update != this.props.update) { + this.externalUpdate(); + } + } + + externalUpdate = () => { + this.setState({ tempValue: Number(this.props.value) }); + this.setState({ tempRadianValue: (Number(this.props.value) * Math.PI) / 180 }); + }; + + onChange = (event: React.ChangeEvent<HTMLInputElement>) => { + let value = event.target.value == '' ? 0 : Number(event.target.value); + if (value > this.props.upperBound) { + value = this.props.upperBound; + } else if (value < this.props.lowerBound) { + value = this.props.lowerBound; + } + if (this.props.prop != '') { + this.props.dataDoc[this.props.prop] = value; + } + this.setState({ tempValue: event.target.value == '' ? event.target.value : value }); + this.setState({ tempRadianValue: (value * Math.PI) / 180 }); + if (this.props.effect) { + this.props.effect(value); + } + }; + + onChangeRadianValue = (event: React.ChangeEvent<HTMLInputElement>) => { + let value = event.target.value === '' ? 0 : Number(event.target.value); + if (value > 2 * Math.PI) { + value = 2 * Math.PI; + } else if (value < 0) { + value = 0; + } + if (this.props.prop != '') { + this.props.dataDoc[this.props.prop] = (value * 180) / Math.PI; + } + this.setState({ tempValue: (value * 180) / Math.PI }); + this.setState({ tempRadianValue: value }); + if (this.props.effect) { + this.props.effect((value * 180) / Math.PI); + } + }; + + render() { + return ( + <div + style={{ + display: 'flex', + lineHeight: '1.5', + textAlign: 'right', + alignItems: 'center', + }}> + {this.props.label && ( + <div + style={{ + marginTop: '0.3em', + marginBottom: '-0.5em', + width: this.props.labelWidth ?? '2em', + }}> + {this.props.label} + </div> + )} + <TextField + type="number" + variant="standard" + value={this.state.tempValue} + onChange={this.onChange} + sx={{ + height: '1em', + width: this.state.width, + marginLeft: this.state.margin, + zIndex: 'modal', + }} + inputProps={{ + step: this.props.step, + min: this.props.lowerBound, + max: this.props.upperBound, + type: 'number', + }} + InputProps={{ + startAdornment: ( + <InputAdornment position="start"> + {Math.abs(Number(this.props.value) - (this.props.correctValue ?? 0)) < this.epsilon && this.props.showIcon && <TaskAltIcon color={'success'} />} + {Math.abs(Number(this.props.value) - (this.props.correctValue ?? 0)) >= this.epsilon && this.props.showIcon && <ErrorOutlineIcon color={'error'} />} + </InputAdornment> + ), + endAdornment: <InputAdornment position="end">{this.props.unit}</InputAdornment>, + }} + /> + {this.props.radianEquivalent && ( + <div style={{ marginTop: '0.3em', marginBottom: '-0.5em', width: '1em' }}> + <p>≈</p> + </div> + )} + {this.props.radianEquivalent && ( + <TextField + type="number" + variant="standard" + value={this.state.tempRadianValue} + onChange={this.onChangeRadianValue} + sx={{ + height: '1em', + width: this.state.width, + marginLeft: this.state.margin, + zIndex: 'modal', + }} + inputProps={{ + step: Math.PI / 8, + min: 0, + max: 2 * Math.PI, + type: 'number', + }} + InputProps={{ + endAdornment: <InputAdornment position="end">rad</InputAdornment>, + }} + /> + )} + </div> + ); + } +} diff --git a/src/client/views/nodes/PhysicsBox/PhysicsSimulationQuestions.json b/src/client/views/nodes/PhysicsBox/PhysicsSimulationQuestions.json new file mode 100644 index 000000000..cc79f7aad --- /dev/null +++ b/src/client/views/nodes/PhysicsBox/PhysicsSimulationQuestions.json @@ -0,0 +1,161 @@ +{ + "inclinePlane": [ + { + "questionSetup": [ + "There is a 1kg weight on an inclined plane. The plane is at a ", + " angle from the ground. The system is in equilibrium (the net force on the weight is 0)." + ], + "variablesForQuestionSetup": ["theta - max 45"], + "question": "What are the magnitudes and directions of the forces acting on the weight?", + "answerParts": [ + "force of gravity", + "angle of gravity", + "normal force", + "angle of normal force", + "force of static friction", + "angle of static friction" + ], + "answerSolutionDescriptions": [ + "9.81", + "270", + "solve normal force magnitude from wedge angle", + "solve normal force angle from wedge angle", + "solve static force magnitude from wedge angle given equilibrium", + "solve static force angle from wedge angle given equilibrium" + ], + "goal": "noMovement", + "hints": [ + { + "description": "Direction of Force of Gravity", + "content": "The force of gravity acts in the negative y direction: 3Ï€/2 rad." + }, + { + "description": "Direction of Normal Force", + "content": "The normal force acts in the direction perpendicular to the incline plane: Ï€/2-θ rad, where θ is the angle of the incline plane." + }, + { + "description": "Direction of Force of Friction", + "content": "The force of friction acts in the direction along the incline plane: Ï€-θ rad, where θ is the angle of the incline plane." + }, + { + "description": "Magnitude of Force of Gravity", + "content": "The magnitude of the force of gravity is approximately 9.81." + }, + { + "description": "Magnitude of Normal Force", + "content": "The magnitude of the normal force is equal to m*g*cos(θ), where θ is the angle of the incline plane." + }, + { + "description": "Net Force in Equilibrium", + "content": "For the system to be in equilibrium, the sum of the x components of all forces must equal 0, and the sum of the y components of all forces must equal 0." + }, + { + "description": "X Component of Normal Force", + "content": "The x component of the normal force is equal to m*g*cos(θ)*cos(Ï€/2-θ), where θ is the angle of the incline plane." + }, + { + "description": "X Component of Force of Friction", + "content": "Since the net force in the x direction must be 0, we know the magnitude of the x component of the friction force is m*g*cos(θ)*cos(Ï€/2-θ)." + }, + { + "description": "Y Component of Normal Force", + "content": "The y component of the normal force is equal to m*g*cos(θ)*sin(Ï€/2-θ), where θ is the angle of the incline plane. The y component of gravity is equal to m*g" + }, + { + "description": "Y Component of Force of Friction", + "content": "Since the net force in the x direction must be 0, we know the magnitude of the y component of the friction force is m*g-m*g*cos(θ)*sin(Ï€/2-θ)." + }, + { + "description": "Magnitude of Force of Friction", + "content": "Combining the x and y components of the friction force, we get the magnitude of the friction force is equal to sqrt((m*g*cos(θ)*cos(Ï€/2-θ))^2 + (m*g-m*g*cos(θ)*sin(Ï€/2-θ))^2)." + } + ] + }, + { + "questionSetup": [ + "There is a 1kg weight on an inclined plane. The plane is at a ", + " angle from the ground. The system is in equilibrium (the net force on the weight is 0)." + ], + "variablesForQuestionSetup": ["theta - max 45"], + "question": "What is the minimum coefficient of static friction?", + "answerParts": ["coefficient of static friction"], + "answerSolutionDescriptions": [ + "solve minimum static coefficient from wedge angle given equilibrium" + ], + "goal": "noMovement", + "hints": [ + { + "description": "Net Force in Equilibrium", + "content": "If the system is in equilibrium, the sum of the x components of all forces must equal 0. In this system, the normal force and force of static friction have non-zero x components." + }, + { + "description": "X Component of Normal Force", + "content": "The x component of the normal force is equal to m*g*cos(θ)*cos(Ï€/2-θ), where θ is the angle of the incline plane." + }, + { + "description": "X Component of Force of Friction", + "content": "The x component of the force of static friction is equal to μ*m*g*cos(θ)*cos(Ï€-θ), where θ is the angle of the incline plane." + }, + { + "description": "Equation to Solve for Minimum Coefficient of Static Friction", + "content": "Since the net force in the x direction must be 0, we can solve the equation 0=m*g*cos(θ)*cos(Ï€/2-θ)+μ*m*g*cos(θ)*cos(Ï€-θ) for μ to find the minimum coefficient of static friction such that the system stays in equilibrium." + } + ] + }, + { + "questionSetup": [ + "There is a 1kg weight on an inclined plane. The coefficient of static friction is ", + ". The system is in equilibrium (the net force on the weight is 0)." + ], + "variablesForQuestionSetup": ["coefficient of static friction"], + "question": "What is the maximum angle of the plane from the ground?", + "answerParts": ["wedge angle"], + "answerSolutionDescriptions": [ + "solve maximum wedge angle from coefficient of static friction given equilibrium" + ], + "goal": "noMovement", + "hints": [ + { + "description": "Net Force in Equilibrium", + "content": "If the system is in equilibrium, the sum of the x components of all forces must equal 0. In this system, the normal force and force of static friction have non-zero x components." + }, + { + "description": "X Component of Normal Force", + "content": "The x component of the normal force is equal to m*g*cos(θ)*cos(Ï€/2-θ), where θ is the angle of the incline plane." + }, + { + "description": "X Component of Force of Friction", + "content": "The x component of the force of static friction is equal to μ*m*g*cos(θ)*cos(Ï€-θ), where θ is the angle of the incline plane." + }, + { + "description": "Equation to Solve for Maximum Wedge Angle", + "content": "Since the net force in the x direction must be 0, we can solve the equation 0=m*g*cos(θ)*cos(Ï€/2-θ)+μ*m*g*cos(θ)*cos(Ï€-θ) for θ to find the maximum wedge angle such that the system stays in equilibrium." + }, + { + "description": "Simplifying Equation to Solve for Maximum Wedge Angle", + "content": "Simplifying 0=m*g*cos(θ)*cos(Ï€/2-θ)+μ*m*g*cos(θ)*cos(Ï€-θ), we get cos(Ï€/2-θ)=-μ*cos(Ï€-θ)." + }, + { + "description": "Simplifying Equation to Solve for Maximum Wedge Angle", + "content": "The cosine subtraction formula states that cos(A-B)=cos(A)*cos(B)+sin(A)sin(B)." + }, + { + "description": "Simplifying Equation to Solve for Maximum Wedge Angle", + "content": "Applying the cosine subtraction formula to cos(Ï€/2-θ)=-μ*cos(Ï€-θ), we get cos(Ï€/2)*cos(θ)+sin(Ï€/2)*sin(θ)=-μ*(cos(Ï€)cos(θ)+sin(Ï€)sin(θ))." + }, + { + "description": "Simplifying Equation to Solve for Maximum Wedge Angle", + "content": "Simplifying cos(Ï€/2)*cos(θ)-sin(Ï€/2)*sin(θ)=-μ*(cos(Ï€)cos(θ)-sin(Ï€)sin(θ)), we get -sin(θ)=-μ*(-cos(θ))." + }, + { + "description": "Simplifying Equation to Solve for Maximum Wedge Angle", + "content": "Simplifying -sin(θ)=-μ*(-cos(θ)), we get tan(θ)=-μ." + }, + { + "description": "Simplifying Equation to Solve for Maximum Wedge Angle", + "content": "Solving for θ, we get θ = atan(μ)." + } + ] + } + ] +} diff --git a/src/client/views/nodes/PhysicsBox/PhysicsSimulationTutorial.json b/src/client/views/nodes/PhysicsBox/PhysicsSimulationTutorial.json new file mode 100644 index 000000000..3015deaa4 --- /dev/null +++ b/src/client/views/nodes/PhysicsBox/PhysicsSimulationTutorial.json @@ -0,0 +1,600 @@ +{ + "freeWeight": { + "question": "A 1kg weight is at rest on the ground. What are the magnitude and directions of the forces acting on the weight?", + "steps": [ + { + "description": "Forces", + "content": "There are two forces acting on the weight: the force of gravity and the normal force.", + "forces": [ + { + "description": "Gravity", + "magnitude": 9.81, + "directionInDegrees": 270, + "component": false + }, + { + "description": "Normal Force", + "magnitude": 9.81, + "directionInDegrees": 90, + "component": false + } + ], + "showMagnitude": false + }, + { + "description": "Force of Gravity", + "content": "The force of gravity acts in the negative y direction: 3Ï€/2 rad. It has magnitude equal to m*g. We can approximate g as 9.81.", + "forces": [ + { + "description": "Gravity", + "magnitude": 9.81, + "directionInDegrees": 270, + "component": false + } + ], + "showMagnitude": true + }, + { + "description": "Normal Force", + "content": "The normal force acts in the positive y direction: Ï€/2 rad. It has magnitude equal to m*g. We can approximate g as 9.81.", + "forces": [ + { + "description": "Normal Force", + "magnitude": 9.81, + "directionInDegrees": 90, + "component": false + } + ], + "showMagnitude": true + }, + { + "description": "All Forces", + "content": "Combining all of the forces, we get the following free body diagram.", + "forces": [ + { + "description": "Gravity", + "magnitude": 9.81, + "directionInDegrees": 270, + "component": false + }, + { + "description": "Normal Force", + "magnitude": 9.81, + "directionInDegrees": 90, + "component": false + } + ], + "showMagnitude": true + } + ] + }, + "pendulum": { + "question": "A 1kg weight on a 300m rod of negligible mass is released from an angle 30 degrees offset from equilibrium. What are the magnitude and directions of the forces acting on the weight immediately after release? (Ignore air resistance)", + "steps": [ + { + "description": "Forces", + "content": "There are two force acting on the weight: the force of gravity and the force of tension.", + "forces": [ + { + "description": "Gravity", + "magnitude": 9.81, + "directionInDegrees": 270, + "component": false + }, + { + "description": "Tension", + "magnitude": 8.5, + "directionInDegrees": 60, + "component": false + } + ], + "showMagnitude": false + }, + { + "description": "Force of Gravity", + "content": "The force of gravity acts in the negative y direction: 3Ï€/2 rad. It has magnitude equal to m*g. We can approximate g as 9.81.", + "forces": [ + { + "description": "Gravity", + "magnitude": 9.81, + "directionInDegrees": 270, + "component": false + } + ], + "showMagnitude": true + }, + { + "description": "Tension", + "content": "The force of tension acts along the direction of the rod. The rod is 30 degrees offset from equilibrium, so the direction along the rod is 90-30=60 degrees. The tension force has two components—the component creating the centripetal force and the component canceling out the parallel component of gravity. The weight has just been released, so it has velocity 0, meaning the centripetal force is 0. Thus, the tension force only acts to cancel out the parallel component of gravity. Thus, the magnitude of tension is m*g*sin(60°)", + "forces": [ + { + "description": "Tension", + "magnitude": 8.5, + "directionInDegrees": 60, + "component": false + }, + { + "description": "Gravity - Parallel Component", + "magnitude": 8.5, + "directionInDegrees": 240, + "component": true + } + ], + "showMagnitude": true + }, + { + "description": "All Forces", + "content": "Combining all of the forces, we get the following free body diagram.", + "forces": [ + { + "description": "Gravity", + "magnitude": 9.81, + "directionInDegrees": 270, + "component": false + }, + { + "description": "Tension", + "magnitude": 8.5, + "directionInDegrees": 60, + "component": false + } + ], + "showMagnitude": true + } + ] + }, + "inclinePlane": { + "question": "There is a 1kg weight on an inclined plane. The plane is at an angle θ from the ground, and has a coefficient of static friction μ. The system is in equilibrium (the net force on the weight is 0). What are the magnitudes and directions of the forces acting on the weight?", + "steps": [ + { + "description": "Forces", + "content": "There are three forces acting on the weight: the force of gravity, the normal force, and the force of static friction.", + "forces": [ + { + "description": "Gravity", + "magnitude": 9.81, + "directionInDegrees": 270, + "component": false + }, + { + "description": "Normal Force", + "magnitude": 8.817, + "directionInDegrees": 64, + "component": false + }, + { + "description": "Friction Force", + "magnitude": 4.3, + "directionInDegrees": 154, + "component": false + } + ], + "showMagnitude": false + }, + { + "description": "Force of Gravity", + "content": "The force of gravity acts in the negative y direction: 3Ï€/2 rad. It has magnitude equal to m*g. We can approximate g as 9.81.", + "forces": [ + { + "description": "Gravity", + "magnitude": 9.81, + "directionInDegrees": 270, + "component": false + } + ], + "showMagnitude": true + }, + { + "description": "Normal Force", + "content": "The normal force acts in the direction perpendicular to the incline plane: Ï€/2-θ rad, where θ is the angle of the incline plane. The magnitude of the normal force is equal to m*g*cos(θ).", + "forces": [ + { + "description": "Normal Force", + "magnitude": 8.817, + "directionInDegrees": 64, + "component": false + } + ], + "showMagnitude": true + }, + { + "description": "Force of Static Friction", + "content": "The force of static friction acts in the direction along the incline plane: Ï€-θ rad, where θ is the angle of the incline plane. We can use the knowledge that the system is in equilibrium to solve for the magnitude of the force of static friction.", + "forces": [ + { + "description": "Friction Force", + "magnitude": 4.3, + "directionInDegrees": 154, + "component": false + } + ], + "showMagnitude": false + }, + { + "description": "Net X Force in Equilibrium", + "content": "For the system to be in equilibrium, the sum of the x components of all forces must equal 0. The x component of the normal force is equal to m*g*cos(θ)*cos(Ï€/2-θ), where θ is the angle of the incline plane. The x component of gravity is equal to 0. Since the net force in the x direction must be 0, we know the magnitude of the x component of the friction force is m*g*cos(θ)*cos(Ï€/2-θ).", + "forces": [ + { + "description": "Normal Force - X Component", + "magnitude": 3.87, + "directionInDegrees": 0, + "component": true + }, + { + "description": "Friction Force - X Component", + "magnitude": 3.87, + "directionInDegrees": 180, + "component": true + } + ], + "showMagnitude": true + }, + { + "description": "Net Y Force Normal Force", + "content": "For the system to be in equilibrium, the sum of the y components of all forces must equal 0. The y component of the normal force is equal to m*g*cos(θ)*sin(Ï€/2-θ), where θ is the angle of the incline plane. The y component of gravity is equal to m*g. Since the net force in the x direction must be 0, we know the magnitude of the y component of the friction force is m*g-m*g*cos(θ)*sin(Ï€/2-θ).", + "forces": [ + { + "description": "Normal Force - Y Component ", + "magnitude": 7.92, + "directionInDegrees": 90, + "component": true + }, + { + "description": "Gravity - Y Component ", + "magnitude": 9.81, + "directionInDegrees": 270, + "component": true + }, + { + "description": "Friction Force - Y Component ", + "magnitude": 1.89, + "directionInDegrees": 90, + "component": true + } + ], + "showMagnitude": true + }, + { + "description": "Magnitude of Force of Friction", + "content": "Combining the x and y components of the friction force, we get the magnitude of the friction force is equal to sqrt((m*g*cos(θ)*cos(Ï€/2-θ))^2 + (m*g*cos(θ)*sin(Ï€/2-θ)-m*g)^2).", + "forces": [ + { + "description": "Friction Force", + "magnitude": 4.3, + "directionInDegrees": 154, + "component": false + } + ], + "showMagnitude": true + }, + { + "description": "All Forces", + "content": "Combining all of the forces, we get the following free body diagram.", + "forces": [ + { + "description": "Gravity", + "magnitude": 9.81, + "directionInDegrees": 270, + "component": false + }, + { + "description": "Normal Force", + "magnitude": 8.817, + "directionInDegrees": 64, + "component": false + }, + { + "description": "Friction Force", + "magnitude": 4.3, + "directionInDegrees": 154, + "component": false + } + ], + "showMagnitude": true + } + ] + }, + "spring": { + "question": "A 1kg weight is on a spring of negligible mass with rest length 200m and spring constant 0.5. What is the equilibrium spring length?", + "steps": [ + { + "description": "Forces", + "content": "We can start by solving for the forces acting on the weight at any given point in time. There are two forces potentially acting on the weight: the force of gravity and the spring force. In equilibrium, these forces will be perfectly balanced.", + "forces": [ + { + "description": "Gravity", + "magnitude": 9.81, + "directionInDegrees": 270, + "component": false + }, + { + "description": "Spring Force", + "magnitude": 9.81, + "directionInDegrees": 90, + "component": false + } + ], + "showMagnitude": false + }, + { + "description": "Force of Gravity", + "content": "The force of gravity acts in the negative y direction: 3Ï€/2 rad. It has magnitude equal to m*g. We can approximate g as 9.81.", + "forces": [ + { + "description": "Gravity", + "magnitude": 9.81, + "directionInDegrees": 270, + "component": false + } + ], + "showMagnitude": true + }, + { + "description": "Spring Force", + "content": "The spring force acts in the negative y direction (3Ï€/2 rad) if the spring is compressed. The spring force acts in the positive y direction (Ï€/2 rad) if the spring is extended. Because the forces are perfectly balanced and gravity acts in the negative y direction, the spring force must act in the positive y direction and have the same magnitude as the force og gravity, m*g. We can approximate g as 9.81.", + "forces": [ + { + "description": "Spring Force", + "magnitude": 9.81, + "directionInDegrees": 90, + "component": false + } + ], + "showMagnitude": true + }, + { + "description": "Spring Force", + "content": "We can use the spring force equation, Fs=kd to solve for the displacement such that Fs=mg. Setting them equal, we get mg=kd. Plugging in for the known values of m,g, and k, we get 1*9.81=0.5d. Solving for d, we get d=19.62 as the equilibrium starting displacement", + "forces": [ + { + "description": "Spring Force", + "magnitude": 9.81, + "directionInDegrees": 90, + "component": false + } + ], + "showMagnitude": true + } + ] + }, + "circular": { + "question": "A 1kg weight is attached to a 100m rod of negligible mass. The weight is undergoing uniform circular motion with tangential velocity 40 m/s. What are the magnitude and directions of the forces acting on the weight? (Ignore air resistance)", + "steps": [ + { + "description": "Forces", + "content": "There is one force acting on the weight: the centripetal force.", + "forces": [ + { + "description": "Centripetal Force", + "magnitude": 16, + "directionInDegrees": 90, + "component": false + } + ], + "showMagnitude": false + }, + { + "description": "Centripetal Force", + "content": "The centripetal force is always directed toward the center of the circle. The formula for solving for the magnitude of centripetal force for an object undergoing uniform circular motion is Fc=mv^2 / r. Plugging in for known values, we get Fc=1*(40^2)/100. Solving for this, we get Fc=16", + "forces": [ + { + "description": "Centripetal Force", + "magnitude": 16, + "directionInDegrees": 90, + "component": false + } + ], + "showMagnitude": true + } + ] + }, + "pulley": { + "question": "A 1kg red weight is attached to a simple pulley with a rope of negligible mass. A 1.5kg blue weight is attached to the other end of the simple pulley. What are the forces acting on the red weight?", + "steps": [ + { + "description": "Forces", + "content": "There are two force acting on the red weight: the force of gravity and the force of tension.", + "forces": [ + { + "description": "Gravity", + "magnitude": 9.81, + "directionInDegrees": 270, + "component": false + }, + { + "description": "Tension", + "magnitude": 11.77, + "directionInDegrees": 90, + "component": false + } + ], + "showMagnitude": false + }, + { + "description": "Gravity", + "content": "The force of gravity acts in the negative y direction: 3Ï€/2 rad. It has magnitude equal to m*g. We can approximate g as 9.81.", + "forces": [ + { + "description": "Gravity", + "magnitude": 9.81, + "directionInDegrees": 270, + "component": false + } + ], + "showMagnitude": true + }, + { + "description": "Tension", + "content": "The force of tension acts in the positive y direction: Ï€/2 rad. We know that the acceleration in a simple pulley system is (mass 2 - mass 1) * acceleration due to gravity / (mass 1 + mass 2) = (1.5-1) * 9.81 / (1.5+1) = 1.962 m/s^2. Because the acceleration is caused by the force of gravity and force of tension, we can solve for the force of tension acting on the weight as mass 1 * (a + acceleration due to gravity) = 1 * (1.962+9.81) = 11.77.", + "forces": [ + { + "description": "Tension", + "magnitude": 11.77, + "directionInDegrees": 90, + "component": false + } + ], + "showMagnitude": true + }, + { + "description": "All Forces", + "content": "Combining all of the forces, we get the following free body diagram.", + "forces": [ + { + "description": "Gravity", + "magnitude": 9.81, + "directionInDegrees": 270, + "component": false + }, + { + "description": "Tension", + "magnitude": 11.77, + "directionInDegrees": 90, + "component": false + } + ], + "showMagnitude": true + } + ] + }, + "suspension": { + "question": "A 1kg weight is attached to two rods hanging from 45° angles from the ceiling. The system is in equilibrium, i.e. the weight does not move. What are the magnitudes and directions of the forces acting on the weight?", + "steps": [ + { + "description": "Forces", + "content": "There are three force acting on the red weight: the force of gravity, the force of tension from the left rod, and the force of tension from the right rod.", + "forces": [ + { + "description": "Gravity", + "magnitude": 9.81, + "directionInDegrees": 270, + "component": false + }, + { + "description": "Left Tension", + "magnitude": 6.94, + "directionInDegrees": 135, + "component": false + }, + { + "description": "Right Tension", + "magnitude": 6.94, + "directionInDegrees": 45, + "component": false + } + ], + "showMagnitude": false + }, + { + "description": "Force X Components", + "content": "There are two forces with x components to consider: the tension from the left rod and the tension from the right rod. These must cancel each other out so that the net x force is 0.", + "forces": [ + { + "description": "Left Tension X Component", + "magnitude": 4.907, + "directionInDegrees": 180, + "component": true + }, + { + "description": "Right Tension X Component", + "magnitude": 4.907, + "directionInDegrees": 0, + "component": true + } + ], + "showMagnitude": false + }, { + "description": "Force Y Components", + "content": "There are three forces with y components to consider: the tension from the left rod, the tension from the right rod, and the force of gravity.", + "forces": [ + { + "description": "Left Tension Y Component", + "magnitude": 4.907, + "directionInDegrees": 90, + "component": true + }, + { + "description": "Right Tension Y Component", + "magnitude": 4.907, + "directionInDegrees": 90, + "component": true + }, + { + "description": "Gravity Y Component", + "magnitude": 9.81, + "directionInDegrees": 270, + "component": true + } + ], + "showMagnitude": false + }, { + "description": "Force Y Components", + "content": "The y components of forces must cancel each other out so that the net y force is 0. Thus, gravity = left tension y component + right tension y component. Because the x components of tension are the same and the angles of each rod are the same, the y components must be the same. Thus, the y component for each force of tension must be 9.81/2.", + "forces": [ + { + "description": "Left Tension Y Component", + "magnitude": 4.907, + "directionInDegrees": 90, + "component": true + }, + { + "description": "Right Tension Y Component", + "magnitude": 4.907, + "directionInDegrees": 90, + "component": true + }, + { + "description": "Gravity Y Component", + "magnitude": 9.81, + "directionInDegrees": 270, + "component": true + } + ], + "showMagnitude": true + }, { + "description": "Tension", + "content": "Now that we know the y component of tension for each rod is 4.907, we can solve for the full force of tension as 4.907 = T * sin(45°) -> T = 6.94.", + "forces": [ + { + "description": "Left Tension", + "magnitude": 6.94, + "directionInDegrees": 135, + "component": false + }, + { + "description": "Right Tension", + "magnitude": 6.94, + "directionInDegrees": 45, + "component": false + } + ], + "showMagnitude": true + }, + { + "description": "All Forces", + "content": "Combining all of the forces, we get the following free body diagram.", + "forces": [ + { + "description": "Gravity", + "magnitude": 9.81, + "directionInDegrees": 270, + "component": false + }, + { + "description": "Left Tension", + "magnitude": 6.94, + "directionInDegrees": 135, + "component": false + }, + { + "description": "Right Tension", + "magnitude": 6.94, + "directionInDegrees": 45, + "component": false + } + ], + "showMagnitude": true + } + ] + } +} diff --git a/src/client/views/nodes/PhysicsBox/PhysicsSimulationWall.tsx b/src/client/views/nodes/PhysicsBox/PhysicsSimulationWall.tsx new file mode 100644 index 000000000..8cc1d0fbf --- /dev/null +++ b/src/client/views/nodes/PhysicsBox/PhysicsSimulationWall.tsx @@ -0,0 +1,34 @@ +import React = require('react'); + +export interface Force { + magnitude: number; + directionInDegrees: number; +} +export interface IWallProps { + length: number; + xPos: number; + yPos: number; + angleInDegrees: number; +} + +export default class Wall extends React.Component<IWallProps> { + + constructor(props: any) { + super(props) + } + + wallStyle = { + width: this.props.angleInDegrees == 0 ? this.props.length + "%" : "5px", + height: this.props.angleInDegrees == 0 ? "5px" : this.props.length + "%", + position: "absolute" as "absolute", + left: this.props.xPos + "%", + top: this.props.yPos + "%", + backgroundColor: "#6c7b8b", + margin: 0, + padding: 0, + }; + + render () { + return (<div style={this.wallStyle}></div>); + } +}; diff --git a/src/client/views/nodes/PhysicsBox/PhysicsSimulationWeight.tsx b/src/client/views/nodes/PhysicsBox/PhysicsSimulationWeight.tsx new file mode 100644 index 000000000..2165c8ba9 --- /dev/null +++ b/src/client/views/nodes/PhysicsBox/PhysicsSimulationWeight.tsx @@ -0,0 +1,990 @@ +import { computed, IReactionDisposer, reaction } from 'mobx'; +import { observer } from 'mobx-react'; +import './PhysicsSimulationBox.scss'; +import React = require('react'); + +interface IWallProps { + length: number; + xPos: number; + yPos: number; + angleInDegrees: number; +} +interface IForce { + description: string; + magnitude: number; + directionInDegrees: number; +} +export interface IWeightProps { + pause: () => void; + panelWidth: () => number; + panelHeight: () => number; + resetRequest: () => number; + circularMotionRadius: number; + coefficientOfKineticFriction: number; + color: string; + componentForces: () => IForce[]; + setComponentForces: (x: IForce[]) => {}; + displayXVelocity: number; + displayYVelocity: number; + elasticCollisions: boolean; + gravity: number; + mass: number; + simulationMode: string; + noMovement: boolean; + paused: boolean; + pendulumAngle: number; + pendulumLength: number; + radius: number; + showAcceleration: boolean; + showComponentForces: boolean; + showForceMagnitudes: boolean; + showForces: boolean; + showVelocity: boolean; + simulationSpeed: number; + simulationType: string; + springConstant: number; + springRestLength: number; + springStartLength: number; + startForces: () => IForce[]; + startPendulumAngle: number; + startPendulumLength: number; + startPosX: number; + startPosY: number; + startVelX: number; + startVelY: number; + timestepSize: number; + updateMassPosX: number; + updateMassPosY: number; + forcesUpdated: () => IForce[]; + setForcesUpdated: (x: IForce[]) => {}; + setPosition: (x: number | undefined, y: number | undefined) => void; + setVelocity: (x: number | undefined, y: number | undefined) => void; + setAcceleration: (x: number, y: number) => void; + setPendulumAngle: (ang: number | undefined, length: number | undefined) => void; + setSpringLength: (length: number) => void; + wallPositions: IWallProps[]; + wedgeHeight: number; + wedgeWidth: number; + xMax: number; + xMin: number; + yMax: number; + yMin: number; +} + +interface IState { + angleLabel: number; + clickPositionX: number; + clickPositionY: number; + coordinates: string; + dragging: boolean; + kineticFriction: boolean; + maxPosYConservation: number; + timer: number; + updatedStartPosX: any; + updatedStartPosY: any; + xPosition: number; + xVelocity: number; + yPosition: number; + yVelocity: number; + xAccel: number; + yAccel: number; +} +@observer +export default class Weight extends React.Component<IWeightProps, IState> { + constructor(props: any) { + super(props); + this.state = { + angleLabel: 0, + clickPositionX: 0, + clickPositionY: 0, + coordinates: '', + dragging: false, + kineticFriction: false, + maxPosYConservation: 0, + timer: 0, + updatedStartPosX: this.props.startPosX, + updatedStartPosY: this.props.startPosY, + xPosition: this.props.startPosX, + xVelocity: this.props.startVelX, + yPosition: this.props.startPosY, + yVelocity: this.props.startVelY, + xAccel: 0, + yAccel: 0, + }; + } + + _timer: NodeJS.Timeout | undefined; + _resetDisposer: IReactionDisposer | undefined; + + componentWillUnmount() { + this._timer && clearTimeout(this._timer); + this._resetDisposer?.(); + } + componentWillUpdate(nextProps: Readonly<IWeightProps>, nextState: Readonly<IState>, nextContext: any): void { + if (nextProps.paused) { + this._timer && clearTimeout(this._timer); + this._timer = undefined; + } else if (this.props.paused) { + this._timer && clearTimeout(this._timer); + this._timer = setInterval(() => this.setState({ timer: this.state.timer + 1 }), 50); + } + } + + // Constants + @computed get draggable() { + return !['Inclined Plane', 'Pendulum'].includes(this.props.simulationType) && this.props.simulationMode === 'Freeform'; + } + @computed get panelHeight() { + return Math.max(800, this.props.panelHeight()) + 'px'; + } + @computed get panelWidth() { + return Math.max(1000, this.props.panelWidth()) + 'px'; + } + + @computed get walls() { + return ['One Weight', 'Inclined Plane'].includes(this.props.simulationType) ? this.props.wallPositions : []; + } + epsilon = 0.0001; + labelBackgroundColor = `rgba(255,255,255,0.5)`; + + // Variables + weightStyle = { + alignItems: 'center', + backgroundColor: this.props.color, + borderColor: 'black', + borderRadius: 50 + '%', + borderStyle: 'solid', + display: 'flex', + height: 2 * this.props.radius + 'px', + justifyContent: 'center', + left: this.props.startPosX + 'px', + position: 'absolute' as 'absolute', + top: this.props.startPosY + 'px', + touchAction: 'none', + width: 2 * this.props.radius + 'px', + zIndex: 5, + }; + + // Helper function to go between display and real values + getDisplayYPos = (yPos: number) => this.props.yMax - yPos - 2 * this.props.radius + 5; + gravityForce = (): IForce => ({ + description: 'Gravity', + magnitude: this.props.mass * this.props.gravity, + directionInDegrees: 270, + }); + // Update display values when simulation updates + setDisplayValues = (xPos: number = this.state.xPosition, yPos: number = this.state.yPosition, xVel: number = this.state.xVelocity, yVel: number = this.state.yVelocity) => { + this.props.setPosition(xPos, this.getDisplayYPos(yPos)); + this.props.setVelocity(xVel, yVel); + const xAccel = Math.round(this.getNewAccelerationX(this.props.forcesUpdated()) * 100) / 100; + const yAccel = (-1 * Math.round(this.getNewAccelerationY(this.props.forcesUpdated()) * 100)) / 100; + this.props.setAcceleration(xAccel, yAccel); + this.setState({ xAccel, yAccel }); + }; + componentDidMount() { + this._resetDisposer = reaction(() => this.props.resetRequest(), this.resetEverything); + } + componentDidUpdate(prevProps: Readonly<IWeightProps>, prevState: Readonly<IState>, snapshot?: any): void { + if (prevProps.simulationType != this.props.simulationType) { + this.setState({ xVelocity: this.props.startVelX, yVelocity: this.props.startVelY }); + this.setDisplayValues(); + } + + // Change pendulum angle from input field + if (prevProps.startPendulumAngle != this.props.startPendulumAngle || prevProps.startPendulumLength !== this.props.startPendulumLength) { + const length = this.props.startPendulumLength; + const x = length * Math.cos(((90 - this.props.startPendulumAngle) * Math.PI) / 180); + const y = length * Math.sin(((90 - this.props.startPendulumAngle) * Math.PI) / 180); + const xPosition = this.props.xMax / 2 - x - this.props.radius; + const yPosition = y - this.props.radius - 5; + this.setState({ xPosition, yPosition, updatedStartPosX: xPosition, updatedStartPosY: yPosition }); + this.props.setPendulumAngle(this.props.startPendulumAngle, this.props.startPendulumLength); + } + + // When display values updated by user, update real value + if (prevProps.updateMassPosX !== this.props.updateMassPosX) { + const x = Math.min(Math.max(0, this.props.updateMassPosX), this.props.xMax - 2 * this.props.radius); + this.setState({ updatedStartPosX: x, xPosition: x }); + this.props.setPosition(x, undefined); + } + if (prevProps.updateMassPosY != this.props.updateMassPosY) { + const y = Math.min(Math.max(0, this.props.updateMassPosY), this.props.yMax - 2 * this.props.radius); + const coordinatePosition = this.getDisplayYPos(y); + this.setState({ yPosition: coordinatePosition, updatedStartPosY: coordinatePosition }); + this.props.setPosition(undefined, this.getDisplayYPos(y)); + + if (this.props.displayXVelocity != this.state.xVelocity) { + this.setState({ xVelocity: this.props.displayXVelocity }); + this.props.setVelocity(this.props.displayXVelocity, undefined); + } + + if (this.props.displayYVelocity != -this.state.yVelocity) { + this.setState({ yVelocity: -this.props.displayYVelocity }); + this.props.setVelocity(undefined, this.props.displayYVelocity); + } + } + + // Make sure weight doesn't go above max height + if ((prevState.updatedStartPosY != this.state.updatedStartPosY || prevProps.startVelY != this.props.startVelY) && !isNaN(this.state.updatedStartPosY) && !isNaN(this.props.startVelY)) { + if (this.props.simulationType == 'One Weight') { + let maxYPos = this.state.updatedStartPosY; + if (this.props.startVelY != 0) { + maxYPos -= (this.props.startVelY * this.props.startVelY) / (2 * this.props.gravity); + } + if (maxYPos < 0) maxYPos = 0; + + this.setState({ maxPosYConservation: maxYPos }); + } + } + + // Check for collisions and update + if (!this.props.paused && !this.props.noMovement && prevState.timer != this.state.timer) { + let collisions = false; + if (this.props.simulationType == 'One Weight' || this.props.simulationType == 'Inclined Plane') { + const collisionsWithGround = this.checkForCollisionsWithGround(); + const collisionsWithWalls = this.checkForCollisionsWithWall(); + collisions = collisionsWithGround || collisionsWithWalls; + } + if (this.props.simulationType == 'Pulley') { + if (this.state.yPosition <= this.props.yMin + 100 || this.state.yPosition >= this.props.yMax - 100) { + collisions = true; + } + } + if (!collisions) this.update(); + + this.setDisplayValues(); + } + + // Convert from static to kinetic friction if/when weight slips on inclined plane + if (prevState.xVelocity != this.state.xVelocity) { + if (this.props.simulationType == 'Inclined Plane' && Math.abs(this.state.xVelocity) > 0.1 && this.props.simulationMode != 'Review' && !this.state.kineticFriction) { + this.setState({ kineticFriction: true }); + const normalForce: IForce = { + description: 'Normal Force', + magnitude: this.props.mass * this.props.gravity * Math.cos(Math.atan(this.props.wedgeHeight / this.props.wedgeWidth)), + directionInDegrees: 180 - 90 - (Math.atan(this.props.wedgeHeight / this.props.wedgeWidth) * 180) / Math.PI, + }; + const frictionForce: IForce = { + description: 'Kinetic Friction Force', + magnitude: this.props.mass * this.props.coefficientOfKineticFriction * this.props.gravity * Math.cos(Math.atan(this.props.wedgeHeight / this.props.wedgeWidth)), + directionInDegrees: 180 - (Math.atan(this.props.wedgeHeight / this.props.wedgeWidth) * 180) / Math.PI, + }; + // reduce magnitude of friction force if necessary such that block cannot slide up plane + // prettier-ignore + const yForce = - this.props.gravity + + normalForce.magnitude * Math.sin((normalForce.directionInDegrees * Math.PI) / 180) + + frictionForce.magnitude * Math.sin((frictionForce.directionInDegrees * Math.PI) / 180); + if (yForce > 0) { + frictionForce.magnitude = (-normalForce.magnitude * Math.sin((normalForce.directionInDegrees * Math.PI) / 180) + this.props.gravity) / Math.sin((frictionForce.directionInDegrees * Math.PI) / 180); + } + + const normalForceComponent: IForce = { + description: 'Normal Force', + magnitude: this.props.mass * this.props.gravity * Math.cos(Math.atan(this.props.wedgeHeight / this.props.wedgeWidth)), + directionInDegrees: 180 - 90 - (Math.atan(this.props.wedgeHeight / this.props.wedgeWidth) * 180) / Math.PI, + }; + const gravityParallel: IForce = { + description: 'Gravity Parallel Component', + magnitude: this.props.mass * this.props.gravity * Math.sin(Math.PI / 2 - Math.atan(this.props.wedgeHeight / this.props.wedgeWidth)), + directionInDegrees: 180 - 90 - (Math.atan(this.props.wedgeHeight / this.props.wedgeWidth) * 180) / Math.PI + 180, + }; + const gravityPerpendicular: IForce = { + description: 'Gravity Perpendicular Component', + magnitude: this.props.mass * this.props.gravity * Math.cos(Math.PI / 2 - Math.atan(this.props.wedgeHeight / this.props.wedgeWidth)), + directionInDegrees: 360 - (Math.atan(this.props.wedgeHeight / this.props.wedgeWidth) * 180) / Math.PI, + }; + const kineticFriction = this.props.coefficientOfKineticFriction != 0 ? [frictionForce] : []; + this.props.setForcesUpdated([this.gravityForce(), normalForce, ...kineticFriction]); + this.props.setComponentForces([normalForceComponent, gravityParallel, gravityPerpendicular, ...kineticFriction]); + } + } + + // Update x position when start pos x changes + if (prevProps.startPosX != this.props.startPosX) { + if (this.props.paused && !isNaN(this.props.startPosX)) { + this.setState({ xPosition: this.props.startPosX, updatedStartPosX: this.props.startPosX }); + this.props.setPosition(this.props.startPosX, undefined); + } + } + + // Update y position when start pos y changes TODO debug + if (prevProps.startPosY != this.props.startPosY) { + if (this.props.paused && !isNaN(this.props.startPosY)) { + this.setState({ yPosition: this.props.startPosY, updatedStartPosY: this.props.startPosY ?? 0 }); + this.props.setPosition(undefined, this.getDisplayYPos(this.props.startPosY)); + } + } + + // Update wedge coordinates + if (!this.state.coordinates || this.props.yMax !== prevProps.yMax || prevProps.wedgeWidth != this.props.wedgeWidth || prevProps.wedgeHeight != this.props.wedgeHeight) { + const left = this.props.xMax * 0.25; + const coordinatePair1 = Math.round(left) + ',' + this.props.yMax + ' '; + const coordinatePair2 = Math.round(left + this.props.wedgeWidth) + ',' + this.props.yMax + ' '; + const coordinatePair3 = Math.round(left) + ',' + (this.props.yMax - this.props.wedgeHeight); + this.setState({ coordinates: coordinatePair1 + coordinatePair2 + coordinatePair3 }); + } + + if (this.state.xPosition != prevState.xPosition || this.state.yPosition != prevState.yPosition) { + this.weightStyle = { + alignItems: 'center', + backgroundColor: this.props.color, + borderColor: 'black', + borderRadius: 50 + '%', + borderStyle: 'solid', + display: 'flex', + height: 2 * this.props.radius + 'px', + justifyContent: 'center', + left: this.state.xPosition + 'px', + position: 'absolute' as 'absolute', + top: this.state.yPosition + 'px', + touchAction: 'none', + width: 2 * this.props.radius + 'px', + zIndex: 5, + }; + } + } + + // Reset simulation on reset button click + resetEverything = () => { + this.setState({ + kineticFriction: false, + xPosition: this.state.updatedStartPosX, + yPosition: this.state.updatedStartPosY, + xVelocity: this.props.startVelX, + yVelocity: this.props.startVelY, + angleLabel: Math.round(this.props.pendulumAngle * 100) / 100, + }); + this.props.setPendulumAngle(this.props.startPendulumAngle, undefined); + this.props.setForcesUpdated(this.props.startForces()); + this.props.setPosition(this.state.updatedStartPosX, this.state.updatedStartPosY); + this.props.setVelocity(this.props.startVelX, this.props.startVelY); + this.props.setAcceleration(0, 0); + setTimeout(() => this.setState({ timer: this.state.timer + 1 })); + }; + + // Compute x acceleration from forces, F=ma + getNewAccelerationX = (forceList: IForce[]) => { + // prettier-ignore + return forceList.reduce((newXacc, force) => + newXacc + (force.magnitude * Math.cos((force.directionInDegrees * Math.PI) / 180)) / this.props.mass, 0); + }; + + // Compute y acceleration from forces, F=ma + getNewAccelerationY = (forceList: IForce[]) => { + // prettier-ignore + return forceList.reduce((newYacc, force) => + newYacc + (-1 * (force.magnitude * Math.sin((force.directionInDegrees * Math.PI) / 180))) / this.props.mass, 0); + }; + + // Compute uniform circular motion forces given x, y positions + getNewCircularMotionForces = (xPos: number, yPos: number): IForce[] => { + const deltaX = (this.props.xMin + this.props.xMax) / 2 - (xPos + this.props.radius); + const deltaY = yPos + this.props.radius - (this.props.yMin + this.props.yMax) / 2; + return [ + { + description: 'Centripetal Force', + magnitude: (this.props.startVelX ** 2 * this.props.mass) / this.props.circularMotionRadius, + directionInDegrees: (Math.atan2(deltaY, deltaX) * 180) / Math.PI, + }, + ]; + }; + + // Compute spring forces given y position + getNewSpringForces = (yPos: number): IForce[] => { + const yPosPlus = yPos - this.props.springRestLength > 0; + const yPosMinus = yPos - this.props.springRestLength < 0; + return [ + this.gravityForce(), + { + description: 'Spring Force', + magnitude: this.props.springConstant * (yPos - this.props.springRestLength) * (yPosPlus ? 1 : yPosMinus ? -1 : 0), + directionInDegrees: yPosPlus ? 90 : 270, + }, + ]; + }; + + // Compute pendulum forces given position, velocity + getNewPendulumForces = (xPos: number, yPos: number, xVel: number, yVel: number): IForce[] => { + const x = this.props.xMax / 2 - xPos - this.props.radius; + const y = yPos + this.props.radius + 5; + const angle = (ang => (ang < 0 ? ang + 180 : ang))((Math.atan(y / x) * 180) / Math.PI); + + let oppositeAngle = 90 - angle; + if (oppositeAngle < 0) { + oppositeAngle = 90 - (180 - angle); + } + + const pendulumLength = Math.sqrt(x * x + y * y); + this.props.setPendulumAngle(oppositeAngle, undefined); + + const mag = this.props.mass * this.props.gravity * Math.cos((oppositeAngle * Math.PI) / 180) + (this.props.mass * (xVel * xVel + yVel * yVel)) / pendulumLength; + + return [ + this.gravityForce(), + { + description: 'Tension', + magnitude: mag, + directionInDegrees: angle, + }, + ]; + }; + + // Check for collisions in x direction + checkForCollisionsWithWall = () => { + let collision = false; + if (this.state.xVelocity !== 0) { + this.walls + .filter(wall => wall.angleInDegrees === 90) + .forEach(wall => { + const wallX = (wall.xPos / 100) * this.props.panelWidth(); + const minX = this.state.xPosition < wallX && wall.xPos < 0.35; + const maxX = this.state.xPosition + 2 * this.props.radius >= wallX && wall.xPos > 0.35; + if (minX || maxX) { + this.setState({ + xPosition: minX ? wallX + 0.01 : wallX - 2 * this.props.radius - 0.01, + xVelocity: this.props.elasticCollisions ? -this.state.xVelocity : 0, + }); + collision = true; + } + }); + } + return collision; + }; + + // Check for collisions in y direction + checkForCollisionsWithGround = () => { + let collision = false; + const minY = this.state.yPosition; + const maxY = this.state.yPosition + 2 * this.props.radius; + if (this.state.yVelocity > 0) { + this.walls.forEach(wall => { + if (wall.angleInDegrees == 0 && wall.yPos > 0.4) { + const groundY = (wall.yPos / 100) * this.props.panelHeight(); + const gravity = this.gravityForce(); + if (maxY > groundY) { + this.setState({ yPosition: groundY - 2 * this.props.radius - 0.01 }); + if (this.props.elasticCollisions) { + this.setState({ yVelocity: -this.state.yVelocity }); + } else { + this.setState({ yVelocity: 0 }); + const normalForce: IForce = { + description: 'Normal force', + magnitude: gravity.magnitude, + directionInDegrees: -gravity.directionInDegrees, + }; + this.props.setForcesUpdated([gravity, normalForce]); + if (this.props.simulationType === 'Inclined Plane') { + this.props.setComponentForces([gravity, normalForce]); + } + } + collision = true; + } + } + }); + } + if (this.state.yVelocity < 0) { + this.walls.forEach(wall => { + if (wall.angleInDegrees == 0 && wall.yPos < 0.4) { + const groundY = (wall.yPos / 100) * this.props.panelHeight(); + if (minY < groundY) { + this.setState({ + yPosition: groundY + 0.01, + yVelocity: this.props.elasticCollisions ? -this.state.yVelocity : 0, + }); + collision = true; + } + } + }); + } + return collision; + }; + + // Called at each RK4 step + evaluate = (currentXPos: number, currentYPos: number, currentXVel: number, currentYVel: number, currdeltaXPos: number, currdeltaYPos: number, currdeltaXVel: number, currdeltaYVel: number, dt: number) => { + const xPos = currentXPos + currdeltaXPos * dt; + const yPos = currentYPos + currdeltaYPos * dt; + const xVel = currentXVel + currdeltaXVel * dt; + const yVel = currentYVel + currdeltaYVel * dt; + const forces = this.getForces(xPos, yPos, xVel, yVel); + return { + xPos, + yPos, + xVel, + yVel, + deltaXPos: xVel, + deltaYPos: yVel, + deltaXVel: this.getNewAccelerationX(forces), + deltaYVel: this.getNewAccelerationY(forces), + }; + }; + + getForces = (xPos: number, yPos: number, xVel: number, yVel: number) => { + // prettier-ignore + switch (this.props.simulationType) { + case 'Pendulum': return this.getNewPendulumForces(xPos, yPos, xVel, yVel); + case 'Spring' : return this.getNewSpringForces(yPos); + case 'Circular Motion': return this.getNewCircularMotionForces(xPos, yPos); + default: return this.props.forcesUpdated(); + } + }; + + // Update position, velocity using RK4 method + update = () => { + const startXVel = this.state.xVelocity; + const startYVel = this.state.yVelocity; + let xPos = this.state.xPosition; + let yPos = this.state.yPosition; + let xVel = this.state.xVelocity; + let yVel = this.state.yVelocity; + const forces = this.getForces(xPos, yPos, xVel, yVel); + const xAcc = this.getNewAccelerationX(forces); + const yAcc = this.getNewAccelerationY(forces); + const coeff = (this.props.timestepSize * 1.0) / 6.0; + for (let i = 0; i < this.props.simulationSpeed; i++) { + const k1 = this.evaluate(xPos, yPos, xVel, yVel, xVel, yVel, xAcc, yAcc, 0); + const k2 = this.evaluate(xPos, yPos, xVel, yVel, k1.deltaXPos, k1.deltaYPos, k1.deltaXVel, k1.deltaYVel, this.props.timestepSize * 0.5); + const k3 = this.evaluate(xPos, yPos, xVel, yVel, k2.deltaXPos, k2.deltaYPos, k2.deltaXVel, k2.deltaYVel, this.props.timestepSize * 0.5); + const k4 = this.evaluate(xPos, yPos, xVel, yVel, k3.deltaXPos, k3.deltaYPos, k3.deltaXVel, k3.deltaYVel, this.props.timestepSize); + + xVel += coeff * (k1.deltaXVel + 2 * (k2.deltaXVel + k3.deltaXVel) + k4.deltaXVel); + yVel += coeff * (k1.deltaYVel + 2 * (k2.deltaYVel + k3.deltaYVel) + k4.deltaYVel); + xPos += coeff * (k1.deltaXPos + 2 * (k2.deltaXPos + k3.deltaXPos) + k4.deltaXPos); + yPos += coeff * (k1.deltaYPos + 2 * (k2.deltaYPos + k3.deltaYPos) + k4.deltaYPos); + } + // make sure harmonic motion maintained and errors don't propagate + switch (this.props.simulationType) { + case 'Spring': + const equilibriumPos = this.props.springRestLength + (this.props.mass * this.props.gravity) / this.props.springConstant; + const amplitude = Math.abs(equilibriumPos - this.props.springStartLength); + if (startYVel < 0 && yVel > 0 && yPos < this.props.springRestLength) { + yPos = equilibriumPos - amplitude; + } else if (startYVel > 0 && yVel < 0 && yPos > this.props.springRestLength) { + yPos = equilibriumPos + amplitude; + } + break; + case 'Pendulum': + const startX = this.state.updatedStartPosX; + if (startXVel <= 0 && xVel > 0) { + xPos = this.state.updatedStartPosX; + if (this.state.updatedStartPosX > this.props.xMax / 2) { + xPos = this.props.xMax / 2 + (this.props.xMax / 2 - startX) - 2 * this.props.radius; + } + yPos = this.props.startPosY; + } else if (startXVel >= 0 && xVel < 0) { + xPos = this.state.updatedStartPosX; + if (this.state.updatedStartPosX < this.props.xMax / 2) { + xPos = this.props.xMax / 2 + (this.props.xMax / 2 - startX) - 2 * this.props.radius; + } + yPos = this.props.startPosY; + } + break; + case 'One Weight': + if (yPos < this.state.maxPosYConservation) { + yPos = this.state.maxPosYConservation; + } + } + this.setState({ xVelocity: xVel, yVelocity: yVel, xPosition: xPos, yPosition: yPos }); + + const forcesn = this.getForces(xPos, yPos, xVel, yVel); + this.props.setForcesUpdated(forcesn); + + // set component forces if they change + if (this.props.simulationType == 'Pendulum') { + const x = this.props.xMax / 2 - xPos - this.props.radius; + const y = yPos + this.props.radius + 5; + let angle = (Math.atan(y / x) * 180) / Math.PI; + if (angle < 0) { + angle += 180; + } + let oppositeAngle = 90 - angle; + if (oppositeAngle < 0) { + oppositeAngle = 90 - (180 - angle); + } + + const pendulumLength = Math.sqrt(x * x + y * y); + + const tensionComponent: IForce = { + description: 'Tension', + magnitude: this.props.mass * this.props.gravity * Math.cos((oppositeAngle * Math.PI) / 180) + (this.props.mass * (xVel * xVel + yVel * yVel)) / pendulumLength, + directionInDegrees: angle, + }; + const gravityParallel: IForce = { + description: 'Gravity Parallel Component', + magnitude: this.props.gravity * Math.cos(((90 - angle) * Math.PI) / 180), + directionInDegrees: 270 - (90 - angle), + }; + const gravityPerpendicular: IForce = { + description: 'Gravity Perpendicular Component', + magnitude: this.props.gravity * Math.sin(((90 - angle) * Math.PI) / 180), + directionInDegrees: -(90 - angle), + }; + if (this.props.gravity * Math.sin(((90 - angle) * Math.PI) / 180) < 0) { + gravityPerpendicular.magnitude = Math.abs(this.props.gravity * Math.sin(((90 - angle) * Math.PI) / 180)); + gravityPerpendicular.directionInDegrees = 180 - (90 - angle); + } + this.props.setComponentForces([tensionComponent, gravityParallel, gravityPerpendicular]); + } + }; + + renderForce = (force: IForce, index: number, asComponent: boolean, color = '#0d0d0d') => { + if (force.magnitude < this.epsilon) return; + + const angle = (force.directionInDegrees * Math.PI) / 180; + const arrowStartY = this.state.yPosition + this.props.radius - this.props.radius * Math.sin(angle); + const arrowStartX = this.state.xPosition + this.props.radius + this.props.radius * Math.cos(angle); + const arrowEndY = arrowStartY - Math.abs(force.magnitude) * Math.sin(angle) - this.props.radius * Math.sin(angle); + const arrowEndX = arrowStartX + Math.abs(force.magnitude) * Math.cos(angle) + this.props.radius * Math.cos(angle); + + let labelTop = arrowEndY + (force.directionInDegrees >= 0 && force.directionInDegrees < 180 ? 40 : -40); + let labelLeft = arrowEndX + (force.directionInDegrees > 90 && force.directionInDegrees < 270 ? -120 : 30); + + labelTop = Math.max(Math.min(labelTop, this.props.yMax + 50), this.props.yMin); + labelLeft = Math.max(Math.min(labelLeft, this.props.xMax - 60), this.props.xMin); + + return ( + <div key={index} style={{ zIndex: 6, position: 'absolute' }}> + <div + style={{ + pointerEvents: 'none', + position: 'absolute', + left: this.props.xMin, + top: this.props.yMin, + }}> + <svg width={this.props.xMax - this.props.xMin + 'px'} height={this.panelHeight}> + <defs> + <marker id="forceArrow" markerWidth="4" markerHeight="4" refX="0" refY="2" orient="auto" markerUnits="strokeWidth"> + <path d="M0,0 L0,4 L4,2 z" fill={color} /> + </marker> + </defs> + <line strokeDasharray={asComponent ? '10,10' : undefined} x1={arrowStartX} y1={arrowStartY} x2={arrowEndX} y2={arrowEndY} stroke={color} strokeWidth="5" markerEnd="url(#forceArrow)" /> + </svg> + </div> + <div + style={{ + pointerEvents: 'none', + position: 'absolute', + left: labelLeft + 'px', + top: labelTop + 'px', + lineHeight: 1, + backgroundColor: this.labelBackgroundColor, + }}> + <p>{force.description || 'Force'}</p> + {this.props.showForceMagnitudes && <p>{Math.round(100 * force.magnitude) / 100} N</p>} + </div> + </div> + ); + }; + + renderVector = (id: string, magX: number, magY: number, color: string, label: string) => { + const mag = Math.sqrt(magX * magX + magY * magY); + return ( + <div className="showvecs" style={{ zIndex: 6 }}> + <svg width={this.panelWidth} height={this.panelHeight}> + <defs> + <marker id={id} markerWidth="10" markerHeight="10" refX="0" refY="3" orient="auto" markerUnits="strokeWidth"> + <path d="M0,0 L0,6 L9,3 z" fill={color} /> + </marker> + </defs> + <line + x1={this.state.xPosition + this.props.radius + (magX / mag) * this.props.radius} + y1={this.state.yPosition + this.props.radius + (magY / mag) * this.props.radius} + x2={this.state.xPosition + this.props.radius + (magX / mag) * this.props.radius + magX} + y2={this.state.yPosition + this.props.radius + (magY / mag) * this.props.radius + magY} + stroke={color} + strokeWidth="5" + markerEnd={`url(#${id})`} + /> + </svg> + <div + style={{ + pointerEvents: 'none', + position: 'absolute', + left: this.state.xPosition + this.props.radius + 2 * (magX / mag) * this.props.radius + magX + 'px', + top: this.state.yPosition + this.props.radius + 2 * (magY / mag) * this.props.radius + magY + 'px', + lineHeight: 1, + }}> + <p style={{ background: 'white' }}>{label}</p> + </div> + </div> + ); + }; + + // Render weight, spring, rod(s), vectors + render() { + return ( + <div> + <div + className="weightContainer" + onPointerDown={e => { + if (this.draggable) { + this.props.pause(); + this.setState({ + dragging: true, + clickPositionX: e.clientX, + clickPositionY: e.clientY, + }); + } + }} + onPointerMove={e => { + if (this.state.dragging) { + let newY = this.state.yPosition + e.clientY - this.state.clickPositionY; + if (newY > this.props.yMax - 2 * this.props.radius - 10) { + newY = this.props.yMax - 2 * this.props.radius - 10; + } else if (newY < 10) { + newY = 10; + } + + let newX = this.state.xPosition + e.clientX - this.state.clickPositionX; + if (newX > this.props.xMax - 2 * this.props.radius - 10) { + newX = this.props.xMax - 2 * this.props.radius - 10; + } else if (newX < 10) { + newX = 10; + } + if (this.props.simulationType == 'Suspension') { + if (newX < (this.props.xMax + this.props.xMin) / 4 - this.props.radius - 15) { + newX = (this.props.xMax + this.props.xMin) / 4 - this.props.radius - 15; + } else if (newX > (3 * (this.props.xMax + this.props.xMin)) / 4 - this.props.radius / 2 - 15) { + newX = (3 * (this.props.xMax + this.props.xMin)) / 4 - this.props.radius / 2 - 15; + } + } + + this.setState({ yPosition: newY }); + this.props.setPosition(undefined, Math.round((this.props.yMax - 2 * this.props.radius - newY + 5) * 100) / 100); + if (this.props.simulationType != 'Pulley') { + this.setState({ xPosition: newX }); + this.props.setPosition(newX, undefined); + } + if (this.props.simulationType != 'Suspension') { + if (this.props.simulationType != 'Pulley') { + this.setState({ updatedStartPosX: newX }); + } + this.setState({ updatedStartPosY: newY }); + } + this.setState({ + clickPositionX: e.clientX, + clickPositionY: e.clientY, + }); + this.setDisplayValues(); + } + }} + onPointerUp={e => { + if (this.state.dragging) { + if (this.props.simulationType != 'Pendulum' && this.props.simulationType != 'Suspension') { + this.resetEverything(); + } + this.setState({ dragging: false }); + let newY = this.state.yPosition + e.clientY - this.state.clickPositionY; + if (newY > this.props.yMax - 2 * this.props.radius - 10) { + newY = this.props.yMax - 2 * this.props.radius - 10; + } else if (newY < 10) { + newY = 10; + } + + let newX = this.state.xPosition + e.clientX - this.state.clickPositionX; + if (newX > this.props.xMax - 2 * this.props.radius - 10) { + newX = this.props.xMax - 2 * this.props.radius - 10; + } else if (newX < 10) { + newX = 10; + } + if (this.props.simulationType == 'Spring') { + this.props.setSpringLength(newY); + } + if (this.props.simulationType == 'Suspension') { + const x1rod = (this.props.xMax + this.props.xMin) / 2 - this.props.radius - this.props.yMin - 200; + const x2rod = (this.props.xMax + this.props.xMin) / 2 + this.props.yMin + 200 + this.props.radius; + const deltaX1 = this.state.xPosition + this.props.radius - x1rod; + const deltaX2 = x2rod - (this.state.xPosition + this.props.radius); + const deltaY = this.state.yPosition + this.props.radius; + const dir1T = Math.PI - Math.atan(deltaY / deltaX1); + const dir2T = Math.atan(deltaY / deltaX2); + const tensionMag2 = (this.props.mass * this.props.gravity) / ((-Math.cos(dir2T) / Math.cos(dir1T)) * Math.sin(dir1T) + Math.sin(dir2T)); + const tensionMag1 = (-tensionMag2 * Math.cos(dir2T)) / Math.cos(dir1T); + const tensionForce1: IForce = { + description: 'Tension', + magnitude: tensionMag1, + directionInDegrees: (dir1T * 180) / Math.PI, + }; + const tensionForce2: IForce = { + description: 'Tension', + magnitude: tensionMag2, + directionInDegrees: (dir2T * 180) / Math.PI, + }; + this.props.setForcesUpdated([tensionForce1, tensionForce2, this.gravityForce()]); + } + } + }}> + <div className="weight" style={this.weightStyle}> + <p className="weightLabel">{this.props.mass} kg</p> + </div> + </div> + {this.props.simulationType == 'Spring' && ( + <div className="spring"> + <svg width={this.panelWidth} height={this.panelHeight}> + {[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map(val => { + const count = 10; + const xPos1 = this.state.xPosition + this.props.radius + (val % 2 === 0 ? -20 : 20); + const xPos2 = this.state.xPosition + this.props.radius + (val === 10 ? 0 : val % 2 === 0 ? 20 : -20); + const yPos1 = (val * this.state.yPosition) / count; + const yPos2 = val === 10 ? this.state.yPosition + this.props.radius : ((val + 1) * this.state.yPosition) / count; + return <line key={val} x1={xPos1} strokeLinecap="round" y1={yPos1} x2={xPos2} y2={yPos2} stroke={'#808080'} strokeWidth="10" />; + })} + </svg> + </div> + )} + + {this.props.simulationType == 'Pulley' && ( + <div className="rod"> + <svg width={this.panelWidth} height={this.panelHeight}> + <line // + x1={this.state.xPosition + this.props.radius} + y1={this.state.yPosition + this.props.radius} + x2={this.state.xPosition + this.props.radius} + y2={this.props.yMin} + stroke={'#deb887'} + strokeWidth="10" + /> + </svg> + </div> + )} + {this.props.simulationType == 'Pulley' && ( + <div className="wheel"> + <svg width={this.panelWidth} height={this.panelHeight}> + <circle cx={(this.props.xMax + this.props.xMin) / 2} cy={this.props.radius} r={this.props.radius * 1.5} fill={'#808080'} /> + </svg> + </div> + )} + {this.props.simulationType == 'Suspension' && ( + <div className="rod"> + <svg width={this.panelWidth} height={this.panelHeight}> + <line + x1={this.state.xPosition + this.props.radius} + y1={this.state.yPosition + this.props.radius} + x2={(this.props.xMax + this.props.xMin) / 2 - this.props.radius - this.props.yMin - 200} + y2={this.props.yMin} + stroke={'#deb887'} + strokeWidth="10" + /> + </svg> + <p + style={{ + position: 'absolute', + left: (this.props.xMax + this.props.xMin) / 2 - this.props.radius - this.props.yMin - 200 + 80 + 'px', + top: 10 + 'px', + backgroundColor: this.labelBackgroundColor, + }}> + {Math.round( + ((Math.atan((this.state.yPosition + this.props.radius) / (this.state.xPosition + this.props.radius - ((this.props.xMax + this.props.xMin) / 2 - this.props.radius - this.props.yMin - 200))) * 180) / Math.PI) * 100 + ) / 100} + ° + </p> + <div className="rod"> + <svg width={this.props.panelWidth() + 'px'} height={this.panelHeight}> + <line + x1={this.state.xPosition + this.props.radius} + y1={this.state.yPosition + this.props.radius} + x2={(this.props.xMax + this.props.xMin) / 2 + this.props.yMin + 200 + this.props.radius} + y2={this.props.yMin} + stroke={'#deb887'} + strokeWidth="10" + /> + </svg> + </div> + <p + style={{ + position: 'absolute', + left: (this.props.xMax + this.props.xMin) / 2 + this.props.yMin + 200 + this.props.radius - 80 + 'px', + top: 10 + 'px', + backgroundColor: this.labelBackgroundColor, + }}> + {Math.round( + ((Math.atan((this.state.yPosition + this.props.radius) / ((this.props.xMax + this.props.xMin) / 2 + this.props.yMin + 200 + this.props.radius - (this.state.xPosition + this.props.radius))) * 180) / Math.PI) * 100 + ) / 100} + ° + </p> + </div> + )} + {this.props.simulationType == 'Circular Motion' && ( + <div className="rod"> + <svg width={this.panelWidth} height={this.panelHeight}> + <line + x1={this.state.xPosition + this.props.radius} + y1={this.state.yPosition + this.props.radius} + x2={(this.props.xMin + this.props.xMax) / 2} + y2={(this.props.yMin + this.props.yMax) / 2} + stroke={'#deb887'} + strokeWidth="10" + /> + </svg> + </div> + )} + {this.props.simulationType == 'Pendulum' && ( + <div className="rod"> + <svg width={this.panelWidth} height={this.panelHeight}> + <line x1={this.state.xPosition + this.props.radius} y1={this.state.yPosition + this.props.radius} x2={this.props.xMax / 2} y2={-5} stroke={'#deb887'} strokeWidth="10" /> + </svg> + {!this.state.dragging && ( + <div> + <p + style={{ + position: 'absolute', + zIndex: 5, + left: this.state.xPosition + 'px', + top: this.state.yPosition - 70 + 'px', + backgroundColor: this.labelBackgroundColor, + }}> + {Math.round(this.props.pendulumLength)} m + </p> + <p + style={{ + position: 'absolute', + left: this.props.xMax / 2 + 'px', + top: 30 + 'px', + backgroundColor: this.labelBackgroundColor, + }}> + {Math.round(this.props.pendulumAngle * 100) / 100}° + </p> + </div> + )} + </div> + )} + {this.props.simulationType == 'Inclined Plane' && ( + <div> + <div className="wedge"> + <svg width={this.panelWidth} height={this.props.yMax + 'px'}> + <polygon points={this.state.coordinates} style={{ fill: 'burlywood' }} /> + </svg> + </div> + <p + style={{ + position: 'absolute', + left: Math.round(this.props.xMax * 0.25 + this.props.wedgeWidth / 3) + 'px', + top: Math.round(this.props.yMax - 40) + 'px', + }}> + {Math.round(((Math.atan(this.props.wedgeHeight / this.props.wedgeWidth) * 180) / Math.PI) * 100) / 100}° + </p> + </div> + )} + {!this.state.dragging && + this.props.showAcceleration && + this.renderVector( + 'accArrow', + this.getNewAccelerationX(this.props.forcesUpdated()), + this.getNewAccelerationY(this.props.forcesUpdated()), + 'green', + `${Math.round(100 * Math.sqrt(this.state.xAccel * this.state.xAccel + this.state.yAccel * this.state.yAccel)) / 100} m/s^2` + )} + {!this.state.dragging && + this.props.showVelocity && + this.renderVector( + 'velArrow', + this.state.xVelocity, + this.state.yVelocity, + 'blue', + `${Math.round(100 * Math.sqrt(this.props.displayXVelocity * this.props.displayXVelocity + this.props.displayYVelocity * this.props.displayYVelocity)) / 100} m/s` + )} + {!this.state.dragging && this.props.showComponentForces && this.props.componentForces().map((force, index) => this.renderForce(force, index, true))} + {!this.state.dragging && this.props.showForces && this.props.forcesUpdated().map((force, index) => this.renderForce(force, index, false))} + </div> + ); + } +} diff --git a/src/client/views/nodes/QueryBox.scss b/src/client/views/nodes/QueryBox.scss deleted file mode 100644 index b5f90aa1e..000000000 --- a/src/client/views/nodes/QueryBox.scss +++ /dev/null @@ -1,5 +0,0 @@ -.queryBox, .queryBox-dragging { - width: 100%; - height: 100%; - position: absolute; -}
\ No newline at end of file diff --git a/src/client/views/nodes/QueryBox.tsx b/src/client/views/nodes/QueryBox.tsx deleted file mode 100644 index 1b6056be6..000000000 --- a/src/client/views/nodes/QueryBox.tsx +++ /dev/null @@ -1,38 +0,0 @@ -// import React = require("react"); -// import { IReactionDisposer } from "mobx"; -// import { observer } from "mobx-react"; -// import { documentSchema } from "../../../new_fields/documentSchemas"; -// import { Id } from '../../../new_fields/FieldSymbols'; -// import { makeInterface, listSpec } from "../../../new_fields/Schema"; -// import { StrCast, Cast } from "../../../new_fields/Types"; -// import { ViewBoxAnnotatableComponent } from '../DocComponent'; -// import { SearchBox } from "../search/SearchBox"; -// import { FieldView, FieldViewProps } from './FieldView'; -// import "./QueryBox.scss"; -// import { List } from "../../../new_fields/List"; -// import { SnappingManager } from "../../util/SnappingManager"; - -// type QueryDocument = makeInterface<[typeof documentSchema]>; -// const QueryDocument = makeInterface(documentSchema); - -// @observer -// export class QueryBox extends ViewBoxAnnotatableComponent<FieldViewProps, QueryDocument>(QueryDocument) { -// public static LayoutString(fieldKey: string) { return FieldView.LayoutString(QueryBox, fieldKey); } -// _docListChangedReaction: IReactionDisposer | undefined; -// componentDidMount() { -// } - -// componentWillUnmount() { -// this._docListChangedReaction?.(); -// } - -// render() { -// const dragging = !SnappingManager.GetIsDragging() ? "" : "-dragging"; -// return <div className={`queryBox${dragging}`} onWheel={(e) => e.stopPropagation()} > - -// <SearchBox Document={this.props.Document} /> -// </div >; -// } -// } - -// //<SearchBox id={this.props.Document[Id]} sideBar={side} Document={this.props.Document} searchQuery={StrCast(this.dataDoc.searchQuery)} filterQuery={this.dataDoc.filterQuery} /> diff --git a/src/client/views/nodes/RecordingBox/RecordingBox.tsx b/src/client/views/nodes/RecordingBox/RecordingBox.tsx index f406ffbea..04f11a5df 100644 --- a/src/client/views/nodes/RecordingBox/RecordingBox.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingBox.tsx @@ -11,6 +11,7 @@ import { DocumentType } from '../../../documents/DocumentTypes'; import { Presentation } from '../../../util/TrackMovements'; import { Doc } from '../../../../fields/Doc'; import { Id } from '../../../../fields/FieldSymbols'; +import { DocCast } from '../../../../fields/Types'; @observer export class RecordingBox extends ViewBoxBaseComponent() { @@ -41,7 +42,7 @@ export class RecordingBox extends ViewBoxBaseComponent() { setResult = (info: Upload.AccessPathInfo, presentation?: Presentation) => { this.result = info; this.dataDoc.type = DocumentType.VID; - this.dataDoc[this.fieldKey + '-duration'] = this.videoDuration; + this.dataDoc[this.fieldKey + '_duration'] = this.videoDuration; this.dataDoc.layout = VideoBox.LayoutString(this.fieldKey); this.dataDoc[this.props.fieldKey] = new VideoField(this.result.accessPaths.client); @@ -57,7 +58,7 @@ export class RecordingBox extends ViewBoxBaseComponent() { render() { return ( <div className="recordingBox" ref={this._ref}> - {!this.result && <RecordingView setResult={this.setResult} setDuration={this.setVideoDuration} id={this.rootDoc.proto?.[Id] || ''} />} + {!this.result && <RecordingView setResult={this.setResult} setDuration={this.setVideoDuration} id={DocCast(this.rootDoc.proto)?.[Id] || ''} />} </div> ); } diff --git a/src/client/views/nodes/RecordingBox/RecordingView.tsx b/src/client/views/nodes/RecordingBox/RecordingView.tsx index 424ebc384..51eb774e2 100644 --- a/src/client/views/nodes/RecordingBox/RecordingView.tsx +++ b/src/client/views/nodes/RecordingBox/RecordingView.tsx @@ -67,7 +67,7 @@ export function RecordingView(props: IRecordingViewProps) { const videoFiles = videos.map((vid, i) => new File(vid.videoChunks, `segvideo${i}.mkv`, { type: vid.videoChunks[0].type, lastModified: Date.now() })); // upload the segments to the server and get their server access paths - const serverPaths: string[] = (await Networking.UploadFilesToServer(videoFiles)).map(res => (res.result instanceof Error ? '' : res.result.accessPaths.agnostic.server)); + const serverPaths: string[] = (await Networking.UploadFilesToServer(videoFiles.map(file => ({file})))).map(res => (res.result instanceof Error ? '' : res.result.accessPaths.agnostic.server)); // concat the segments together using post call const result: Upload.AccessPathInfo | Error = await Networking.PostToServer('/concatVideos', serverPaths); diff --git a/src/client/views/nodes/ScreenshotBox.tsx b/src/client/views/nodes/ScreenshotBox.tsx index aa2b22e28..312b3c619 100644 --- a/src/client/views/nodes/ScreenshotBox.tsx +++ b/src/client/views/nodes/ScreenshotBox.tsx @@ -5,10 +5,11 @@ import { computed, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; // import { BufferAttribute, Camera, Vector2, Vector3 } from 'three'; import { DateField } from '../../../fields/DateField'; -import { Doc, HeightSym, WidthSym } from '../../../fields/Doc'; +import { Doc } from '../../../fields/Doc'; +import { Height, Width } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { ComputedField } from '../../../fields/ScriptField'; -import { Cast, NumCast } from '../../../fields/Types'; +import { Cast, DocCast, NumCast } from '../../../fields/Types'; import { AudioField, VideoField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; import { emptyFunction, returnFalse, returnOne, returnZero } from '../../../Utils'; @@ -24,6 +25,7 @@ import { FieldView, FieldViewProps } from './FieldView'; import { FormattedTextBox } from './formattedText/FormattedTextBox'; import './ScreenshotBox.scss'; import { VideoBox } from './VideoBox'; + declare class MediaRecorder { constructor(e: any, options?: any); // whatever MediaRecorder has } @@ -129,7 +131,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl } } getAnchor = (addAsAnnotation: boolean) => { - const startTime = Cast(this.layoutDoc._currentTimecode, 'number', null) || (this._videoRec ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined); + const startTime = Cast(this.layoutDoc._layout_currentTimecode, 'number', null) || (this._videoRec ? (Date.now() - (this.recordingStart || 0)) / 1000 : undefined); return CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, startTime, startTime === undefined ? undefined : startTime + 3, undefined, addAsAnnotation) || this.rootDoc; }; @@ -140,7 +142,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl if (!nativeWidth || !nativeHeight) { if (!nativeWidth) Doc.SetNativeWidth(this.dataDoc, 1200); Doc.SetNativeHeight(this.dataDoc, (nativeWidth || 1200) / aspect); - this.layoutDoc._height = (this.layoutDoc[WidthSym]() || 0) / aspect; + this.layoutDoc._height = (this.layoutDoc[Width]() || 0) / aspect; } }; @@ -222,7 +224,7 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl const aud_chunks: any = []; this._audioRec.ondataavailable = (e: any) => aud_chunks.push(e.data); this._audioRec.onstop = async (e: any) => { - const [{ result }] = await Networking.UploadFilesToServer(aud_chunks); + const [{ result }] = await Networking.UploadFilesToServer(aud_chunks.map((file: any) => ({file}))); if (!(result instanceof Error)) { this.dataDoc[this.props.fieldKey + '-audio'] = new AudioField(result.accessPaths.agnostic.client); } @@ -235,14 +237,14 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl this._videoRec.onstop = async (e: any) => { console.log('screenshotbox: upload'); const file = new File(vid_chunks, `${this.rootDoc[Id]}.mkv`, { type: vid_chunks[0].type, lastModified: Date.now() }); - const [{ result }] = await Networking.UploadFilesToServer(file); - this.dataDoc[this.fieldKey + '-duration'] = (new Date().getTime() - this.recordingStart!) / 1000; + const [{ result }] = await Networking.UploadFilesToServer({file}); + this.dataDoc[this.fieldKey + '_duration'] = (new Date().getTime() - this.recordingStart!) / 1000; if (!(result instanceof Error)) { // convert this screenshotBox into normal videoBox this.dataDoc.type = DocumentType.VID; this.layoutDoc.layout = VideoBox.LayoutString(this.fieldKey); this.dataDoc.nativeWidth = this.dataDoc.nativeHeight = undefined; - this.layoutDoc._fitWidth = undefined; + this.layoutDoc._layout_fitWidth = undefined; this.dataDoc[this.props.fieldKey] = new VideoField(result.accessPaths.agnostic.client); } else alert('video conversion failed'); }; @@ -270,14 +272,14 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl setupDictation = () => { if (this.dataDoc[this.fieldKey + '-dictation']) return; const dictationText = DocUtils.GetNewTextDoc('dictation', NumCast(this.rootDoc.x), NumCast(this.rootDoc.y) + NumCast(this.layoutDoc._height) + 10, NumCast(this.layoutDoc._width), 2 * NumCast(this.layoutDoc._height)); - dictationText._autoHeight = false; + dictationText._layout_autoHeight = false; const dictationTextProto = Doc.GetProto(dictationText); dictationTextProto.recordingSource = this.dataDoc; dictationTextProto.recordingStart = ComputedField.MakeFunction(`self.recordingSource["${this.props.fieldKey}-recordingStart"]`); dictationTextProto.mediaState = ComputedField.MakeFunction('self.recordingSource.mediaState'); this.dataDoc[this.fieldKey + '-dictation'] = dictationText; }; - videoPanelHeight = () => (NumCast(this.dataDoc[this.fieldKey + '-nativeHeight'], this.layoutDoc[HeightSym]()) / NumCast(this.dataDoc[this.fieldKey + '-nativeWidth'], this.layoutDoc[WidthSym]())) * this.props.PanelWidth(); + videoPanelHeight = () => (NumCast(this.dataDoc[this.fieldKey + '_nativeHeight'], this.layoutDoc[Height]()) / NumCast(this.dataDoc[this.fieldKey + '_nativeWidth'], this.layoutDoc[Width]())) * this.props.PanelWidth(); formattedPanelHeight = () => Math.max(0, this.props.PanelHeight() - this.videoPanelHeight()); render() { TraceMobx(); @@ -311,11 +313,11 @@ export class ScreenshotBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatabl </> </CollectionFreeFormView> </div> - <div style={{ position: 'relative', height: this.formattedPanelHeight() }}> + <div style={{ background: 'white', position: 'relative', height: this.formattedPanelHeight() }}> {!(this.dataDoc[this.fieldKey + '-dictation'] instanceof Doc) ? null : ( <FormattedTextBox {...this.props} - Document={this.dataDoc[this.fieldKey + '-dictation']} + Document={DocCast(this.dataDoc[this.fieldKey + '-dictation'])} fieldKey={'text'} PanelHeight={this.formattedPanelHeight} select={emptyFunction} diff --git a/src/client/views/nodes/ScriptingBox.tsx b/src/client/views/nodes/ScriptingBox.tsx index f09962b22..37fda14fc 100644 --- a/src/client/views/nodes/ScriptingBox.tsx +++ b/src/client/views/nodes/ScriptingBox.tsx @@ -167,7 +167,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable // only included in buttons, transforms scripting UI to a button @action onFinish = () => { - this.rootDoc.layoutKey = 'layout'; + this.rootDoc.layout_fieldKey = 'layout'; }; // displays error message @@ -702,7 +702,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable // toolbar (with compile and apply buttons) for scripting UI renderScriptingTools() { - const buttonStyle = 'scriptingBox-button' + (StrCast(this.rootDoc.layoutKey).startsWith('layout_on') ? '-finish' : ''); + const buttonStyle = 'scriptingBox-button' + (StrCast(this.rootDoc.layout_fieldKey).startsWith('layout_on') ? '-finish' : ''); return ( <div className="scriptingBox-toolbar"> <button @@ -730,7 +730,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable Save </button> - {!StrCast(this.rootDoc.layoutKey).startsWith('layout_on') ? null : ( // onClick, onChecked, etc need a Finish button to return to their default layout + {!StrCast(this.rootDoc.layout_fieldKey).startsWith('layout_on') ? null : ( // onClick, onChecked, etc need a Finish button to return to their default layout <button className={buttonStyle} onPointerDown={e => { @@ -776,7 +776,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable // toolbar (with edit and run buttons and error message) for params UI renderTools(toolSet: string, func: () => void) { - const buttonStyle = 'scriptingBox-button' + (StrCast(this.rootDoc.layoutKey).startsWith('layout_on') ? '-finish' : ''); + const buttonStyle = 'scriptingBox-button' + (StrCast(this.rootDoc.layout_fieldKey).startsWith('layout_on') ? '-finish' : ''); return ( <div className="scriptingBox-toolbar"> <button @@ -795,7 +795,7 @@ export class ScriptingBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatable }}> {toolSet} </button> - {!StrCast(this.rootDoc.layoutKey).startsWith('layout_on') ? null : ( + {!StrCast(this.rootDoc.layout_fieldKey).startsWith('layout_on') ? null : ( <button className={buttonStyle} onPointerDown={e => { diff --git a/src/client/views/nodes/SliderBox.tsx b/src/client/views/nodes/SliderBox.tsx index b96977f32..430b20eb5 100644 --- a/src/client/views/nodes/SliderBox.tsx +++ b/src/client/views/nodes/SliderBox.tsx @@ -12,50 +12,56 @@ import { FieldView, FieldViewProps } from './FieldView'; import { Handle, Tick, TooltipRail, Track } from './SliderBox-components'; import './SliderBox.scss'; - @observer export class SliderBox extends ViewBoxBaseComponent<FieldViewProps>() { - public static LayoutString(fieldKey: string) { return FieldView.LayoutString(SliderBox, fieldKey); } + public static LayoutString(fieldKey: string) { + return FieldView.LayoutString(SliderBox, fieldKey); + } - get minThumbKey() { return this.fieldKey + "-minThumb"; } - get maxThumbKey() { return this.fieldKey + "-maxThumb"; } - get minKey() { return this.fieldKey + "-min"; } - get maxKey() { return this.fieldKey + "-max"; } + get minThumbKey() { + return this.fieldKey + '-minThumb'; + } + get maxThumbKey() { + return this.fieldKey + '-maxThumb'; + } + get minKey() { + return this.fieldKey + '-min'; + } + get maxKey() { + return this.fieldKey + '-max'; + } specificContextMenu = (e: React.MouseEvent): void => { const funcs: ContextMenuProps[] = []; - funcs.push({ description: "Edit Thumb Change Script", icon: "edit", event: (obj: any) => ScriptBox.EditButtonScript("On Thumb Change ...", this.props.Document, "onThumbChange", obj.x, obj.y) }); - ContextMenu.Instance.addItem({ description: "Options...", subitems: funcs, icon: "asterisk" }); - } - onChange = (values: readonly number[]) => runInAction(() => { - this.dataDoc[this.minThumbKey] = values[0]; - this.dataDoc[this.maxThumbKey] = values[1]; - ScriptCast(this.layoutDoc.onThumbChanged, null)?.script.run({ - self: this.rootDoc, - scriptContext: this.props.scriptContext, range: values, this: this.layoutDoc + funcs.push({ description: 'Edit Thumb Change Script', icon: 'edit', event: (obj: any) => ScriptBox.EditButtonScript('On Thumb Change ...', this.props.Document, 'onThumbChange', obj.x, obj.y) }); + ContextMenu.Instance.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' }); + }; + onChange = (values: readonly number[]) => + runInAction(() => { + this.dataDoc[this.minThumbKey] = values[0]; + this.dataDoc[this.maxThumbKey] = values[1]; + ScriptCast(this.layoutDoc.onThumbChanged, null)?.script.run({ + self: this.rootDoc, + scriptContext: this.props.scriptContext, + range: values, + this: this.layoutDoc, + }); }); - }) render() { const domain = [NumCast(this.layoutDoc[this.minKey]), NumCast(this.layoutDoc[this.maxKey])]; const defaultValues = [NumCast(this.dataDoc[this.minThumbKey]), NumCast(this.dataDoc[this.maxThumbKey])]; - return domain[1] <= domain[0] ? (null) : ( - <div className="sliderBox-outerDiv" onContextMenu={this.specificContextMenu} onPointerDown={e => e.stopPropagation()} - style={{ boxShadow: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BoxShadow) }}> - <div className="sliderBox-mainButton" - onContextMenu={this.specificContextMenu} style={{ + return domain[1] <= domain[0] ? null : ( + <div className="sliderBox-outerDiv" onContextMenu={this.specificContextMenu} onPointerDown={e => e.stopPropagation()} style={{ boxShadow: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BoxShadow) }}> + <div + className="sliderBox-mainButton" + onContextMenu={this.specificContextMenu} + style={{ background: StrCast(this.layoutDoc.backgroundColor), - color: StrCast(this.layoutDoc.color, "black"), - fontSize: StrCast(this.layoutDoc._fontSize), letterSpacing: StrCast(this.layoutDoc.letterSpacing) - }} > - <Slider - mode={2} - step={Math.min(1, .1 * (domain[1] - domain[0]))} - domain={domain} - rootStyle={{ position: "relative", width: "100%" }} - onChange={this.onChange} - values={defaultValues} - > - + color: StrCast(this.layoutDoc.color, 'black'), + fontSize: StrCast(this.layoutDoc._text_fontSize), + letterSpacing: StrCast(this.layoutDoc.letterSpacing), + }}> + <Slider mode={2} step={Math.min(1, 0.1 * (domain[1] - domain[0]))} domain={domain} rootStyle={{ position: 'relative', width: '100%' }} onChange={this.onChange} values={defaultValues}> <Rail>{railProps => <TooltipRail {...railProps} />}</Rail> <Handles> {({ handles, activeHandleID, getHandleProps }) => ( @@ -64,13 +70,7 @@ export class SliderBox extends ViewBoxBaseComponent<FieldViewProps>() { const value = i === 0 ? defaultValues[0] : defaultValues[1]; return ( <div title={String(value)}> - <Handle - key={handle.id} - handle={handle} - domain={domain} - isActive={handle.id === activeHandleID} - getHandleProps={getHandleProps} - /> + <Handle key={handle.id} handle={handle} domain={domain} isActive={handle.id === activeHandleID} getHandleProps={getHandleProps} /> </div> ); })} @@ -81,13 +81,7 @@ export class SliderBox extends ViewBoxBaseComponent<FieldViewProps>() { {({ tracks, getTrackProps }) => ( <div className="slider-tracks"> {tracks.map(({ id, source, target }) => ( - <Track - key={id} - source={source} - target={target} - disabled={false} - getTrackProps={getTrackProps} - /> + <Track key={id} source={source} target={target} disabled={false} getTrackProps={getTrackProps} /> ))} </div> )} @@ -95,13 +89,8 @@ export class SliderBox extends ViewBoxBaseComponent<FieldViewProps>() { <Ticks count={5}> {({ ticks }) => ( <div className="slider-ticks"> - {ticks.map((tick) => ( - <Tick - key={tick.id} - tick={tick} - count={ticks.length} - format={(val: number) => val.toString()} - /> + {ticks.map(tick => ( + <Tick key={tick.id} tick={tick} count={ticks.length} format={(val: number) => val.toString()} /> ))} </div> )} @@ -111,4 +100,4 @@ export class SliderBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> ); } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/VideoBox.tsx b/src/client/views/nodes/VideoBox.tsx index 7a7d4fe37..1f52c2d92 100644 --- a/src/client/views/nodes/VideoBox.tsx +++ b/src/client/views/nodes/VideoBox.tsx @@ -3,7 +3,8 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction, untracked } from 'mobx'; import { observer } from 'mobx-react'; import { basename } from 'path'; -import { Doc, HeightSym, WidthSym } from '../../../fields/Doc'; +import { Doc, StrListCast } from '../../../fields/Doc'; +import { Height, Width } from '../../../fields/DocSymbols'; import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; import { ObjectField } from '../../../fields/ObjectField'; @@ -14,6 +15,7 @@ import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentType } from '../../documents/DocumentTypes'; import { Networking } from '../../Network'; import { DocumentManager } from '../../util/DocumentManager'; +import { FollowLinkScript } from '../../util/LinkFollower'; import { LinkManager } from '../../util/LinkManager'; import { ReplayMovements } from '../../util/ReplayMovements'; import { SelectionManager } from '../../util/SelectionManager'; @@ -33,8 +35,6 @@ import { FieldView, FieldViewProps } from './FieldView'; import { RecordingBox } from './RecordingBox'; import { PinProps, PresBox } from './trails'; import './VideoBox.scss'; -import { ScriptField } from '../../../fields/ScriptField'; -import { FollowLinkScript } from '../../util/LinkFollower'; const path = require('path'); /** @@ -91,9 +91,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp return LinkManager.Links(this.dataDoc); } @computed get heightPercent() { - return NumCast(this.layoutDoc._timelineHeightPercent, 100); + return NumCast(this.layoutDoc._layout_timelineHeightPercent, 100); } // current percent of video relative to VideoBox height - // @computed get rawDuration() { return NumCast(this.dataDoc[this.fieldKey + "-duration"]); } + // @computed get rawDuration() { return NumCast(this.dataDoc[this.fieldKey + "_duration"]); } @observable rawDuration: number = 0; @computed get youtubeVideoId() { @@ -111,7 +111,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // returns the presentation data if it exists, null otherwise @computed get presentation() { const data = this.dataDoc[this.fieldKey + '-presentation']; - return data ? JSON.parse(data) : null; + return data ? JSON.parse(StrCast(data)) : null; } @computed private get timeline() { @@ -325,14 +325,14 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp y: NumCast(this.layoutDoc.y, 1), _width: 150, _height: 50, - title: (this.layoutDoc._currentTimecode || 0).toString(), + title: (this.layoutDoc._layout_currentTimecode || 0).toString(), onClick: FollowLinkScript(), }); this.props.addDocument?.(b); - DocUtils.MakeLink(b, this.rootDoc, { linkRelationship: 'video snapshot' }); + DocUtils.MakeLink(b, this.rootDoc, { link_relationship: 'video snapshot' }); Networking.PostToServer('/youtubeScreenshot', { id: this.youtubeVideoId, - timecode: this.layoutDoc._currentTimecode, + timecode: this.layoutDoc._layout_currentTimecode, }).then(response => { const resolved = response?.accessPaths?.agnostic?.client; if (resolved) { @@ -345,7 +345,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp const dataUrl = canvas.toDataURL('image/png'); // can also use 'image/png' // if you want to preview the captured image, const retitled = StrCast(this.rootDoc.title).replace(/[ -\.:]/g, ''); - const encodedFilename = encodeURIComponent('snapshot' + retitled + '_' + (this.layoutDoc._currentTimecode || 0).toString().replace(/\./, '_')); + const encodedFilename = encodeURIComponent('snapshot' + retitled + '_' + (this.layoutDoc._layout_currentTimecode || 0).toString().replace(/\./, '_')); const filename = basename(encodedFilename); Utils.convertDataUri(dataUrl, filename).then((returnedFilename: string) => returnedFilename && (cb ?? this.createSnapshotLink)(returnedFilename, downX, downY)); } @@ -354,8 +354,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp updateIcon = () => { const makeIcon = (returnedfilename: string) => { this.dataDoc.icon = new ImageField(returnedfilename); - this.dataDoc['icon-nativeWidth'] = this.layoutDoc[WidthSym](); - this.dataDoc['icon-nativeHeight'] = this.layoutDoc[HeightSym](); + this.dataDoc.icon_nativeWidth = this.layoutDoc[Width](); + this.dataDoc.icon_nativeHeight = this.layoutDoc[Height](); }; this.Snapshot(undefined, undefined, makeIcon); }; @@ -373,18 +373,18 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp onClick: FollowLinkScript(), _width: 150, _height: (height / width) * 150, - title: '--snapshot' + NumCast(this.layoutDoc._currentTimecode) + ' image-', + title: '--snapshot' + NumCast(this.layoutDoc._layout_currentTimecode) + ' image-', }); Doc.SetNativeWidth(Doc.GetProto(imageSnapshot), Doc.NativeWidth(this.layoutDoc)); Doc.SetNativeHeight(Doc.GetProto(imageSnapshot), Doc.NativeHeight(this.layoutDoc)); this.props.addDocument?.(imageSnapshot); - const link = DocUtils.MakeLink(imageSnapshot, this.getAnchor(true), { linkRelationship: 'video snapshot' }); - link && (Doc.GetProto(link.anchor2 as Doc).timecodeToHide = NumCast((link.anchor2 as Doc).timecodeToShow) + 3); + const link = DocUtils.MakeLink(imageSnapshot, this.getAnchor(true), { link_relationship: 'video snapshot' }); + link && (Doc.GetProto(link.link_anchor_2 as Doc).timecodeToHide = NumCast((link.link_anchor_2 as Doc).timecodeToShow) + 3); setTimeout(() => downX !== undefined && downY !== undefined && DocumentManager.Instance.getFirstDocumentView(imageSnapshot)?.startDragging(downX, downY, 'move', true)); }; getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { - const timecode = Cast(this.layoutDoc._currentTimecode, 'number', null); + const timecode = Cast(this.layoutDoc._layout_currentTimecode, 'number', null); const marquee = AnchorMenu.Instance.GetAnchor?.(undefined, addAsAnnotation); if (!addAsAnnotation && marquee) marquee.backgroundColor = 'transparent'; const anchor = CollectionStackedTimeline.createAnchor(this.rootDoc, this.dataDoc, this.annotationKey, timecode ? timecode : undefined, undefined, marquee, addAsAnnotation) || this.rootDoc; @@ -402,16 +402,16 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp } if (Number.isFinite(this.player!.duration)) { this.rawDuration = this.player!.duration; - this.dataDoc[this.fieldKey + '-duration'] = this.rawDuration; - } else this.rawDuration = NumCast(this.dataDoc[this.fieldKey + '-duration']); + this.dataDoc[this.fieldKey + '_duration'] = this.rawDuration; + } else this.rawDuration = NumCast(this.dataDoc[this.fieldKey + '_duration']); }); // updates video time @action updateTimecode = () => { - this.player && (this.layoutDoc._currentTimecode = this.player.currentTime); + this.player && (this.layoutDoc._layout_currentTimecode = this.player.currentTime); try { - this._youtubePlayer && (this.layoutDoc._currentTimecode = this._youtubePlayer.getCurrentTime?.()); + this._youtubePlayer && (this.layoutDoc._layout_currentTimecode = this._youtubePlayer.getCurrentTime?.()); } catch (e) { console.log('Video Timecode Exception:', e); } @@ -423,8 +423,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // extracts video thumbnails and saves them as field of doc getVideoThumbnails = () => { - if (this.dataDoc.thumbnails !== undefined) return; - this.dataDoc.thumbnails = new List<string>(); + if (this.dataDoc[this.fieldKey + '_thumbnails'] !== undefined) return; + this.dataDoc[this.fieldKey + '_thumbnails'] = new List<string>(); const thumbnailPromises: Promise<any>[] = []; const video = document.createElement('video'); @@ -442,7 +442,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp if (newTime < video.duration) { video.currentTime = newTime; } else { - Promise.all(thumbnailPromises).then(thumbnails => (this.dataDoc.thumbnails = new List<string>(thumbnails))); + Promise.all(thumbnailPromises).then(thumbnails => (this.dataDoc[this.fieldKey + '_thumbnails'] = new List<string>(thumbnails))); } }; @@ -460,12 +460,12 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // vref.onfullscreenchange = action((e) => this._fullScreen = vref.webkitDisplayingFullscreen); this._disposers.reactionDisposer?.(); this._disposers.reactionDisposer = reaction( - () => NumCast(this.layoutDoc._currentTimecode), + () => NumCast(this.layoutDoc._layout_currentTimecode), time => !this._playing && (vref.currentTime = time), { fireImmediately: true } ); - (!this.dataDoc.thumbnails || this.dataDoc.thumbnails.length != VideoBox.numThumbnails) && this.getVideoThumbnails(); + (!this.dataDoc[this.fieldKey + '_thumbnails'] || StrListCast(this.dataDoc[this.fieldKey + '_thumbnails']).length != VideoBox.numThumbnails) && this.getVideoThumbnails(); } }; @@ -531,7 +531,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this.dataDoc.layout = RecordingBox.LayoutString(this.fieldKey); // delete assoicated video data this.dataDoc[this.props.fieldKey] = ''; - this.dataDoc[this.fieldKey + '-duration'] = ''; + this.dataDoc[this.fieldKey + '_duration'] = ''; // delete assoicated presentation data this.dataDoc[this.fieldKey + '-presentation'] = ''; }, @@ -573,7 +573,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp key="video" autoPlay={this._screenCapture} ref={this.setVideoRef} - style={this._fullScreen ? this.fullScreenSize() : this.isCropped ? { width: 'max-content', height: 'max-content', transform: `scale(${1 / NumCast(this.rootDoc._viewScale)})`, transformOrigin: 'top left' } : {}} + style={this._fullScreen ? this.fullScreenSize() : this.isCropped ? { width: 'max-content', height: 'max-content', transform: `scale(${1 / NumCast(this.rootDoc._freeform_scale)})`, transformOrigin: 'top left' } : {}} onCanPlay={this.videoLoad} controls={VideoBox._nativeControls} onPlay={() => this.Play()} @@ -620,8 +620,8 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this._disposers.reactionDisposer?.(); this._disposers.youtubeReactionDisposer?.(); this._disposers.reactionDisposer = reaction( - () => this.layoutDoc._currentTimecode, - () => !this._playing && this.Seek(NumCast(this.layoutDoc._currentTimecode)) + () => this.layoutDoc._layout_currentTimecode, + () => !this._playing && this.Seek(NumCast(this.layoutDoc._layout_currentTimecode)) ); this._disposers.youtubeReactionDisposer = reaction( () => Doc.ActiveTool === InkTool.None && this.props.isSelected(true) && !SnappingManager.GetIsDragging() && !DocumentDecorations.Instance.Interacting, @@ -677,15 +677,15 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this._clicking = false; if (this.props.isContentActive()) { // const local = this.props.ScreenToLocalTransform().scale(this.props.scaling?.() || 1).transformPoint(e.clientX, e.clientY); - // this.layoutDoc._timelineHeightPercent = Math.max(0, Math.min(100, local[1] / this.props.PanelHeight() * 100)); + // this.layoutDoc._layout_timelineHeightPercent = Math.max(0, Math.min(100, local[1] / this.props.PanelHeight() * 100)); - this.layoutDoc._timelineHeightPercent = 80; + this.layoutDoc._layout_timelineHeightPercent = 80; } return false; }), emptyFunction, () => { - this.layoutDoc._timelineHeightPercent = this.heightPercent !== 100 ? 100 : VideoBox.heightPercent; + this.layoutDoc._layout_timelineHeightPercent = this.heightPercent !== 100 ? 100 : VideoBox.heightPercent; setTimeout( action(() => (this._clicking = false)), 500 @@ -721,7 +721,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp this._youtubeIframeId = VideoBox._youtubeIframeCounter++; this._youtubeContentCreated = this._forceCreateYouTubeIFrame ? true : true; const classname = 'videoBox-content-YouTube' + (this._fullScreen ? '-fullScreen' : ''); - const start = untracked(() => Math.round(NumCast(this.layoutDoc._currentTimecode))); + const start = untracked(() => Math.round(NumCast(this.layoutDoc._layout_currentTimecode))); return ( <iframe key={this._youtubeIframeId} @@ -740,7 +740,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @action.bound addDocWithTimecode(doc: Doc | Doc[]): boolean { const docs = doc instanceof Doc ? [doc] : doc; - const curTime = NumCast(this.layoutDoc._currentTimecode); + const curTime = NumCast(this.layoutDoc._layout_currentTimecode); docs.forEach(doc => (doc._timecodeToHide = (doc._timecodeToShow = curTime) + 1)); return this.addDocument(doc); } @@ -859,7 +859,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp // starts marquee selection marqueeDown = (e: React.PointerEvent) => { - if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._viewScale, 1) === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(Doc.ActiveTool)) { + if (!e.altKey && e.button === 0 && NumCast(this.layoutDoc._freeform_scale, 1) === 1 && this.props.isContentActive(true) && ![InkTool.Highlighter, InkTool.Pen].includes(Doc.ActiveTool)) { setupMoveUpEvents( this, e, @@ -870,6 +870,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp }), returnFalse, () => MarqueeAnnotator.clearAnnotations(this._savedAnnotations), + false, false ); } @@ -890,7 +891,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp .scale(this.scaling()) .translate(0, (-this.heightPercent / 100) * this.props.PanelHeight()); - setPlayheadTime = (time: number) => (this.player!.currentTime = this.layoutDoc._currentTimecode = time); + setPlayheadTime = (time: number) => (this.player!.currentTime = this.layoutDoc._layout_currentTimecode = time); timelineHeight = () => (this.props.PanelHeight() * (100 - this.heightPercent)) / 100; @@ -899,7 +900,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp scaling = () => this.props.NativeDimScaling?.() || 1; panelWidth = () => (this.props.PanelWidth() * this.heightPercent) / 100; - panelHeight = () => (this.layoutDoc._fitWidth ? this.panelWidth() / (Doc.NativeAspect(this.rootDoc) || 1) : (this.props.PanelHeight() * this.heightPercent) / 100); + panelHeight = () => (this.layoutDoc._layout_fitWidth ? this.panelWidth() / (Doc.NativeAspect(this.rootDoc) || 1) : (this.props.PanelHeight() * this.heightPercent) / 100); screenToLocalTransform = () => { const offset = (this.props.PanelWidth() - this.panelWidth()) / 2 / this.scaling(); @@ -912,7 +913,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp marqueeFitScaling = () => ((this.props.NativeDimScaling?.() || 1) * this.heightPercent) / 100; marqueeOffset = () => [((this.panelWidth() / 2) * (1 - this.heightPercent / 100)) / (this.heightPercent / 100), 0]; - timelineDocFilter = () => [`_timelineLabel:true,${Utils.noRecursionHack}:x`]; + timelineDocFilter = () => [`_isTimelineLabel:true,${Utils.noRecursionHack}:x`]; // renders video controls componentUI = (boundsLeft: number, boundsTop: number) => { @@ -956,9 +957,11 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp <CollectionStackedTimeline ref={action((r: any) => (this._stackedTimeline = r))} {...this.props} + dataFieldKey={this.fieldKey} fieldKey={this.annotationKey} dictationKey={this.fieldKey + '-dictation'} mediaPath={this.audiopath} + thumbnails={() => StrListCast(this.dataDoc[this.fieldKey + '_thumbnails'])} renderDepth={this.props.renderDepth + 1} startTag={'_timecodeToShow' /* videoStart */} endTag={'_timecodeToHide' /* videoEnd */} @@ -1003,7 +1006,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp const anchy = NumCast(cropping.y); const anchw = NumCast(cropping._width); const anchh = NumCast(cropping._height); - const viewScale = NumCast(this.rootDoc[this.fieldKey + '-nativeWidth']) / anchw; + const viewScale = NumCast(this.rootDoc[this.fieldKey + '_nativeWidth']) / anchw; cropping.title = 'crop: ' + this.rootDoc.title; cropping.x = NumCast(this.rootDoc.x) + NumCast(this.rootDoc._width); cropping.y = NumCast(this.rootDoc.y); @@ -1014,25 +1017,25 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp cropping.onClick = undefined; const croppingProto = Doc.GetProto(cropping); croppingProto.annotationOn = undefined; - croppingProto.isPrototype = true; + croppingProto.isDataDoc = true; croppingProto.proto = Cast(this.rootDoc.proto, Doc, null)?.proto; // set proto of cropping's data doc to be IMAGE_PROTO croppingProto.type = DocumentType.VID; croppingProto.layout = VideoBox.LayoutString('data'); croppingProto.data = ObjectField.MakeCopy(this.rootDoc[this.fieldKey] as ObjectField); - croppingProto['data-nativeWidth'] = anchw; - croppingProto['data-nativeHeight'] = anchh; + croppingProto['data_nativeWidth'] = anchw; + croppingProto['data_nativeHeight'] = anchh; croppingProto.videoCrop = true; - croppingProto.currentTimecode = this.layoutDoc._currentTimecode; - croppingProto.viewScale = viewScale; - croppingProto.viewScaleMin = viewScale; - croppingProto.panX = anchx / viewScale; - croppingProto.panY = anchy / viewScale; - croppingProto.panXMin = anchx / viewScale; - croppingProto.panXMax = anchw / viewScale; - croppingProto.panYMin = anchy / viewScale; - croppingProto.panYMax = anchh / viewScale; + croppingProto.layout_currentTimecode = this.layoutDoc._layout_currentTimecode; + croppingProto.freeform_scale = viewScale; + croppingProto.freeform_scale_min = viewScale; + croppingProto.freeform_ = anchx / viewScale; + croppingProto.freeform_panY = anchy / viewScale; + croppingProto.freeform_panX_min = anchx / viewScale; + croppingProto.freeform_panX_max = anchw / viewScale; + croppingProto.freeform_panY_min = anchy / viewScale; + croppingProto.freeform_panY_max = anchh / viewScale; if (addCrop) { - DocUtils.MakeLink(region, cropping, { linkRelationship: 'cropped image' }); + DocUtils.MakeLink(region, cropping, { link_relationship: 'cropped image' }); } this.props.bringToFront(cropping); return cropping; @@ -1050,7 +1053,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp style={{ pointerEvents: this.layoutDoc._lockedPosition ? 'none' : undefined, borderRadius, - overflow: this.props.docViewPath?.().slice(-1)[0].fitWidth ? 'auto' : undefined, + overflow: this.props.docViewPath?.().slice(-1)[0].layout_fitWidth ? 'auto' : undefined, }} onWheel={e => { e.stopPropagation(); @@ -1079,8 +1082,9 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp PanelHeight={this.panelHeight} isAnyChildContentActive={returnFalse} ScreenToLocalTransform={this.screenToLocalTransform} - docFilters={this.timelineDocFilter} + childFilters={this.timelineDocFilter} select={emptyFunction} + focus={emptyFunction} NativeDimScaling={returnOne} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} removeDocument={this.removeDocument} @@ -1116,7 +1120,7 @@ export class VideoBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProp @computed get UIButtons() { const bounds = this.props.docViewPath().lastElement().getBounds(); const width = (bounds?.right || 0) - (bounds?.left || 0); - const curTime = NumCast(this.layoutDoc._currentTimecode) - (this.timeline?.clipStart || 0); + const curTime = NumCast(this.layoutDoc._layout_currentTimecode) - (this.timeline?.clipStart || 0); return ( <> <div className="videobox-button" title={this._playing ? 'play' : 'pause'} onPointerDown={this.onPlayDown}> diff --git a/src/client/views/nodes/WebBox.tsx b/src/client/views/nodes/WebBox.tsx index e05b48c0b..2ff0245d2 100644 --- a/src/client/views/nodes/WebBox.tsx +++ b/src/client/views/nodes/WebBox.tsx @@ -2,16 +2,18 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { action, computed, IReactionDisposer, observable, ObservableMap, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as WebRequest from 'web-request'; -import { Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../fields/Doc'; +import { Doc, DocListCast, Field, Opt } from '../../../fields/Doc'; +import { Height, Width } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { HtmlField } from '../../../fields/HtmlField'; import { InkTool } from '../../../fields/InkField'; import { List } from '../../../fields/List'; +import { RefField } from '../../../fields/RefField'; import { listSpec } from '../../../fields/Schema'; -import { Cast, ImageCast, NumCast, StrCast } from '../../../fields/Types'; +import { Cast, ImageCast, NumCast, StrCast, WebCast } from '../../../fields/Types'; import { ImageField, WebField } from '../../../fields/URLField'; import { TraceMobx } from '../../../fields/util'; -import { emptyFunction, getWordAtPoint, returnFalse, returnOne, returnZero, setupMoveUpEvents, smoothScroll, StopEvent, Utils } from '../../../Utils'; +import { addStyleSheet, addStyleSheetRule, clearStyleSheetRules, emptyFunction, getWordAtPoint, returnFalse, returnOne, returnZero, setupMoveUpEvents, smoothScroll, Utils } from '../../../Utils'; import { Docs, DocUtils } from '../../documents/Documents'; import { DocumentManager } from '../../util/DocumentManager'; import { DragManager } from '../../util/DragManager'; @@ -31,7 +33,7 @@ import { Annotation } from '../pdf/Annotation'; import { GPTPopup } from '../pdf/GPTPopup/GPTPopup'; import { SidebarAnnos } from '../SidebarAnnos'; import { StyleProp } from '../StyleProvider'; -import { DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere } from './DocumentView'; +import { DocComponentView, DocFocusOptions, DocumentView, DocumentViewProps, OpenWhere } from './DocumentView'; import { FieldView, FieldViewProps } from './FieldView'; import { LinkDocPreview } from './LinkDocPreview'; import { PinProps, PresBox } from './trails'; @@ -47,6 +49,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } public static openSidebarWidth = 250; public static sidebarResizerWidth = 5; + static webStyleSheet = addStyleSheet(); private _setPreviewCursor: undefined | ((x: number, y: number, drag: boolean, hide: boolean) => void); private _setBrushViewer: undefined | ((view: { width: number; height: number; panX: number; panY: number }) => void); private _mainCont: React.RefObject<HTMLDivElement> = React.createRef(); @@ -54,19 +57,19 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps private _disposers: { [name: string]: IReactionDisposer } = {}; private _annotationLayer: React.RefObject<HTMLDivElement> = React.createRef(); private _keyInput = React.createRef<HTMLInputElement>(); - private _initialScroll: Opt<number> = NumCast(this.layoutDoc.thumbScrollTop, NumCast(this.layoutDoc.scrollTop)); + private _initialScroll: Opt<number> = NumCast(this.layoutDoc.thumbScrollTop, NumCast(this.layoutDoc.layout_scrollTop)); private _sidebarRef = React.createRef<SidebarAnnos>(); private _searchRef = React.createRef<HTMLInputElement>(); private _searchString = ''; + private _scrollTimer: any; private get _getAnchor() { return AnchorMenu.Instance?.GetAnchor; } - @observable private _webUrl = ''; // url of the src parameter of the embedded iframe but not necessarily the rendered page - eg, when following a link, the rendered page changes but we don't wan the src parameter to also change as that would cause an unnecessary re-render. + @observable private _webUrl = ''; // url of the src parameter of the embedded iframe but not necessarily the rendered page - eg, when following a link, the rendered page changes but we don't want the src parameter to also change as that would cause an unnecessary re-render. @observable private _hackHide = false; // apparently changing the value of the 'sandbox' prop doesn't necessarily apply it to the active iframe. so thisforces the ifrmae to be rebuilt when allowScripts is toggled @observable private _searching: boolean = false; @observable private _showSidebar = false; - @observable private _scrollTimer: any; @observable private _webPageHasBeenRendered = false; @observable private _overlayAnnoInfo: Opt<Doc>; @observable private _marqueeing: number[] | undefined; @@ -82,7 +85,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return this._url ? WebBox.urlHash(this._url) + '' : ''; } @computed get scrollHeight() { - return Math.max(this.layoutDoc[HeightSym](), this._scrollHeight); + return Math.max(this.layoutDoc[Height](), this._scrollHeight); } @computed get allAnnotations() { return DocListCast(this.dataDoc[this.annotationKey]); @@ -99,7 +102,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps ImageCast( this.layoutDoc['thumb-frozen'], ImageCast( - this.layoutDoc.thumbScrollTop === this.layoutDoc._scrollTop && this.layoutDoc.thumbNativeWidth === NumCast(this.layoutDoc.nativeWidth) && this.layoutDoc.thumbNativeHeight === NumCast(this.layoutDoc.nativeHeight) + this.layoutDoc.thumbScrollTop === this.layoutDoc._layout_scrollTop && this.layoutDoc.thumbNativeWidth === NumCast(this.layoutDoc.nativeWidth) && this.layoutDoc.thumbNativeHeight === NumCast(this.layoutDoc.nativeHeight) ? this.layoutDoc.thumb : undefined ) @@ -141,87 +144,65 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }; updateThumb = async () => { - const imageBitmap = ImageCast(this.layoutDoc['thumb-frozen'])?.url.href; - const scrollTop = NumCast(this.layoutDoc._scrollTop); + if (!this._iframe) return; + const scrollTop = NumCast(this.layoutDoc._layout_scrollTop); const nativeWidth = NumCast(this.layoutDoc.nativeWidth); const nativeHeight = (nativeWidth * this.props.PanelHeight()) / this.props.PanelWidth(); - if ( - !this.props.isSelected(true) && - !Doc.IsBrushedDegree(this.rootDoc) && - !this.isAnyChildContentActive() && - !this.rootDoc.thumbLockout && - !this.props.dontRegisterView && - this._iframe && - !imageBitmap && - (scrollTop !== this.layoutDoc.thumbScrollTop || nativeWidth !== this.layoutDoc.thumbNativeWidth || nativeHeight !== this.layoutDoc.thumbNativeHeight) - ) { - var htmlString = this._iframe.contentDocument && new XMLSerializer().serializeToString(this._iframe.contentDocument); - if (!htmlString) { - htmlString = await (await fetch(Utils.CorsProxy(this.webField!.href))).text(); - } - this.layoutDoc.thumb = undefined; - this.rootDoc.thumbLockout = true; // lock to prevent multiple thumb updates. - CreateImage(this._webUrl.endsWith('/') ? this._webUrl.substring(0, this._webUrl.length - 1) : this._webUrl, this._iframe.contentDocument?.styleSheets ?? [], htmlString, nativeWidth, nativeHeight, scrollTop) - .then((data_url: any) => { - if (data_url.includes('<!DOCTYPE')) { - console.log('BAD DATA IN THUMB CREATION'); - return; - } - Utils.convertDataUri(data_url, this.layoutDoc[Id] + '-icon' + new Date().getTime(), true, this.layoutDoc[Id] + '-icon').then(returnedfilename => - setTimeout( - action(() => { - this.rootDoc.thumbLockout = false; - this.layoutDoc.thumb = new ImageField(returnedfilename); - this.layoutDoc.thumbScrollTop = scrollTop; - this.layoutDoc.thumbNativeWidth = nativeWidth; - this.layoutDoc.thumbNativeHeight = nativeHeight; - }), - 500 - ) - ); - }) - .catch(function (error: any) { - console.error('oops, something went wrong!', error); - }); + var htmlString = this._iframe.contentDocument && new XMLSerializer().serializeToString(this._iframe.contentDocument); + if (!htmlString) { + htmlString = await (await fetch(Utils.CorsProxy(this.webField!.href))).text(); } + this.layoutDoc.thumb = undefined; + this.rootDoc.thumbLockout = true; // lock to prevent multiple thumb updates. + CreateImage(this._webUrl.endsWith('/') ? this._webUrl.substring(0, this._webUrl.length - 1) : this._webUrl, this._iframe.contentDocument?.styleSheets ?? [], htmlString, nativeWidth, nativeHeight, scrollTop) + .then((data_url: any) => { + if (data_url.includes('<!DOCTYPE')) { + console.log('BAD DATA IN THUMB CREATION'); + return; + } + Utils.convertDataUri(data_url, this.layoutDoc[Id] + '-icon' + new Date().getTime(), true, this.layoutDoc[Id] + '-icon').then(returnedfilename => + setTimeout( + action(() => { + this.rootDoc.thumbLockout = false; + this.layoutDoc.thumb = new ImageField(returnedfilename); + this.layoutDoc.thumbScrollTop = scrollTop; + this.layoutDoc.thumbNativeWidth = nativeWidth; + this.layoutDoc.thumbNativeHeight = nativeHeight; + }), + 500 + ) + ); + }) + .catch(function (error: any) { + console.error('oops, something went wrong!', error); + }); }; - _thumbTimer: any; + async componentDidMount() { this.props.setContentView?.(this); // this tells the DocumentView that this WebBox is the "content" of the document. this allows the DocumentView to call WebBox relevant methods to configure the UI (eg, show back/forward buttons) runInAction(() => { - this._annotationKeySuffix = () => this._urlHash + '-annotations'; + this._annotationKeySuffix = () => this._urlHash + '_annotations'; const reqdFuncs: { [key: string]: string } = {}; - // bcz: need to make sure that doc.data-annotations points to the currently active web page's annotations (this could/should be when the doc is created) - reqdFuncs[this.fieldKey + '-annotations'] = `copyField(this["${this.fieldKey}-"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"-annotations"])`; - reqdFuncs[this.fieldKey + '-annotations-setter'] = `this["${this.fieldKey}-"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"-annotations"] = value`; - reqdFuncs[this.fieldKey + '-sidebar'] = `copyField(this["${this.fieldKey}-"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"-sidebar"])`; - DocUtils.AssignScripts(this.rootDoc, {}, reqdFuncs); + // bcz: need to make sure that doc.data_annotations points to the currently active web page's annotations (this could/should be when the doc is created) + reqdFuncs[this.fieldKey + '_annotations'] = `copyField(this["${this.fieldKey}_"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"_annotations"])`; + reqdFuncs[this.fieldKey + '_annotations-setter'] = `this["${this.fieldKey}_"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"_annotations"] = value`; + reqdFuncs[this.fieldKey + '_sidebar'] = `copyField(this["${this.fieldKey}_"+urlHash(this["${this.fieldKey}"]?.url?.toString())+"_sidebar"])`; + DocUtils.AssignScripts(this.dataDoc, {}, reqdFuncs); }); - reaction( - () => this.props.isSelected(true) || this.isAnyChildContentActive() || Doc.isBrushedHighlightedDegree(this.props.Document), - async selected => { - if (selected) { - this._thumbTimer && clearTimeout(this._thumbTimer); - this._webPageHasBeenRendered = true; - } else if ( - (!this.props.isContentActive(true) || SnappingManager.GetIsDragging()) && // update thumnail when unselected AND (no child annotation is active OR we've started dragging the document in which case no additional deselect will occur so this is the only chance to update the thumbnail) - LightboxView.LightboxDoc !== this.rootDoc - ) { - // don't create a thumbnail if entering Lightbox from maximize either, since thumb will be empty. - this._thumbTimer && clearTimeout(this._thumbTimer); - this._thumbTimer = setTimeout(this.updateThumb, 2000); - } - }, - { fireImmediately: this.props.isSelected(true) || this.isAnyChildContentActive() || (Doc.isBrushedHighlightedDegreeUnmemoized(this.props.Document) ? true : false) } + this._disposers.urlchange = reaction( + () => WebCast(this.rootDoc.data), + url => { + this.submitURL(url.url.href, false, false); + } ); - this._disposers.autoHeight = reaction( - () => this.layoutDoc._autoHeight, - autoHeight => { - if (autoHeight) { - this.layoutDoc._nativeHeight = NumCast(this.props.Document[this.props.fieldKey + '-nativeHeight']); - this.props.setHeight?.(NumCast(this.props.Document[this.props.fieldKey + '-nativeHeight']) * (this.props.NativeDimScaling?.() || 1)); + this._disposers.layout_autoHeight = reaction( + () => this.layoutDoc._layout_autoHeight, + layout_autoHeight => { + if (layout_autoHeight) { + this.layoutDoc._nativeHeight = NumCast(this.props.Document[this.props.fieldKey + '_nativeHeight']); + this.props.setHeight?.(NumCast(this.props.Document[this.props.fieldKey + '_nativeHeight']) * (this.props.NativeDimScaling?.() || 1)); } } ); @@ -234,7 +215,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps if (!nativeWidth || !nativeHeight || Math.abs(nativeWidth / nativeHeight - youtubeaspect) > 0.05) { if (!nativeWidth) Doc.SetNativeWidth(this.layoutDoc, 600); Doc.SetNativeHeight(this.layoutDoc, (nativeWidth || 600) / youtubeaspect); - this.layoutDoc._height = this.layoutDoc[WidthSym]() / youtubeaspect; + this.layoutDoc._height = this.layoutDoc[Width]() / youtubeaspect; } } // else it's an HTMLfield } else if (this.webField && !this.dataDoc.text) { @@ -245,7 +226,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } this._disposers.scrollReaction = reaction( - () => NumCast(this.layoutDoc._scrollTop), + () => NumCast(this.layoutDoc._layout_scrollTop), scrollTop => { const viewTrans = StrCast(this.Document._viewTransition); const durationMiliStr = viewTrans.match(/([0-9]*)ms/); @@ -296,14 +277,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return this._savedAnnotations; }; - menuControls = () => this.urlEditor; // controls to be added to the top bar when a document of this type is selected - setBrushViewer = (func?: (view: { width: number; height: number; panX: number; panY: number }) => void) => (this._setBrushViewer = func); brushView = (view: { width: number; height: number; panX: number; panY: number }) => this._setBrushViewer?.(view); focus = (anchor: Doc, options: DocFocusOptions) => { if (anchor !== this.rootDoc && this._outerRef.current) { const windowHeight = this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); - const scrollTo = Utils.scrollIntoView(NumCast(anchor.y), anchor[HeightSym](), NumCast(this.layoutDoc._scrollTop), windowHeight, windowHeight * 0.1, Math.max(NumCast(anchor.y) + anchor[HeightSym](), this._scrollHeight)); + const scrollTo = Utils.scrollIntoView(NumCast(anchor.y), anchor[Height](), NumCast(this.layoutDoc._layout_scrollTop), windowHeight, windowHeight * 0.1, Math.max(NumCast(anchor.y) + anchor[Height](), this._scrollHeight)); if (scrollTo !== undefined) { if (this._initialScroll === undefined) { const focusTime = options.zoomTime ?? 500; @@ -316,15 +295,18 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } }; - getView = async (doc: Doc) => { - if (this.rootDoc.layoutKey === 'layout_icon') this.props.DocumentView?.().iconify(); - if (this._url && StrCast(doc.webUrl) !== this._url) this.submitURL(StrCast(doc.webUrl)); + @action + getView = (doc: Doc) => { + if (Doc.AreProtosEqual(doc, this.rootDoc)) return new Promise<Opt<DocumentView>>(res => res(this.props.DocumentView?.())); + if (this.rootDoc.layout_fieldKey === 'layout_icon') this.props.DocumentView?.().iconify(); + const webUrl = WebCast(doc.presData)?.url; + if (this._url && webUrl && webUrl.href !== this._url) this.setData(webUrl.href); if (this._sidebarRef?.current?.makeDocUnfiltered(doc) && !this.SidebarShown) this.toggleSidebar(false); return new Promise<Opt<DocumentView>>(res => DocumentManager.Instance.AddViewRenderedCb(doc, dv => res(dv))); }; sidebarAddDocTab = (doc: Doc, where: OpenWhere) => { - if (DocListCast(this.props.Document[this.props.fieldKey + '-sidebar']).includes(doc) && !this.SidebarShown) { + if (DocListCast(this.props.Document[this.props.fieldKey + '_sidebar']).includes(doc) && !this.SidebarShown) { this.toggleSidebar(false); return true; } @@ -341,13 +323,12 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } catch (e) {} const anchor = this._getAnchor(this._savedAnnotations, false) ?? - Docs.Create.WebanchorDocument(this._url, { - title: StrCast(this.rootDoc.title + ' ' + this.layoutDoc._scrollTop), - y: NumCast(this.layoutDoc._scrollTop), - unrendered: true, + Docs.Create.WebConfigDocument({ + title: StrCast(this.rootDoc.title + ' ' + this.layoutDoc._layout_scrollTop), + y: NumCast(this.layoutDoc._layout_scrollTop), annotationOn: this.rootDoc, }); - PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: true, pannable: true } }, this.rootDoc); + PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: pinProps?.pinData ? true : false, pannable: true } }, this.rootDoc); anchor.text = ele?.textContent ?? ''; anchor.textHtml = ele?.innerHTML; //addAsAnnotation && @@ -370,9 +351,9 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps this._selectionText = sel.toString(); AnchorMenu.Instance.setSelectedText(sel.toString()); this._textAnnotationCreator = () => this.createTextAnnotation(sel, !sel.isCollapsed ? sel.getRangeAt(0) : undefined); - AnchorMenu.Instance.jumpTo(e.clientX * scale + mainContBounds.translateX, e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._scrollTop) * scale); + AnchorMenu.Instance.jumpTo(e.clientX * scale + mainContBounds.translateX, e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._layout_scrollTop) * scale); // Changing which document to add the annotation to (the currently selected WebBox) - GPTPopup.Instance.setSidebarId(`${this.props.fieldKey}-${this._urlHash}-sidebar`); + GPTPopup.Instance.setSidebarId(`${this.props.fieldKey}_${this._urlHash}_sidebar`); GPTPopup.Instance.addDoc = this.sidebarAddDocument; } } @@ -385,7 +366,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const word = getWordAtPoint(e.target, e.clientX, e.clientY); this._setPreviewCursor?.(e.clientX, e.clientY, false, true); MarqueeAnnotator.clearAnnotations(this._savedAnnotations); - e.button !== 2 && (this._marqueeing = [e.clientX * scale + mainContBounds.translateX, e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._scrollTop) * scale]); + e.button !== 2 && (this._marqueeing = [e.clientX * scale + mainContBounds.translateX, e.clientY * scale + mainContBounds.translateY - NumCast(this.layoutDoc._layout_scrollTop) * scale]); if (word || ((e.target as any) || '').className.includes('rangeslider') || (e.target as any)?.onclick || (e.target as any)?.parentNode?.onclick) { setTimeout( action(() => (this._marqueeing = undefined)), @@ -413,7 +394,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps iframeClick = () => this._iframeClick; iframeScaling = () => 1 / this.props.ScreenToLocalTransform().Scale; - addStyleSheet(document: any, styleType: string = 'text/css') { + addWebStyleSheet(document: any, styleType: string = 'text/css') { if (document) { const style = document.createElement('style'); style.type = styleType; @@ -421,7 +402,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps return (sheets as any).sheet; } } - addStyleSheetRule(sheet: any, selector: any, css: any, selectorPrefix = '.') { + addWebStyleSheetRule(sheet: any, selector: any, css: any, selectorPrefix = '.') { const propText = typeof css === 'string' ? css @@ -438,8 +419,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps if (this._initialScroll !== undefined) { this.setScrollPos(this._initialScroll); } - - this.addStyleSheetRule(this.addStyleSheet(this._iframe?.contentDocument), '::selection', { color: 'white', background: 'orange' }, ''); + this._scrollHeight = this._iframe?.contentDocument?.body?.scrollHeight ?? 0; + this.addWebStyleSheetRule(this.addWebStyleSheet(this._iframe?.contentDocument), '::selection', { color: 'white', background: 'orange' }, ''); let href: Opt<string>; try { @@ -463,17 +444,24 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps .replace('search&', 'search?') .replace('?gbv=1', ''); } - this.submitURL(requrlraw, undefined, true); + this.setData(requrlraw); } const iframeContent = iframe?.contentDocument; if (iframeContent) { iframeContent.addEventListener('pointerup', this.iframeUp); iframeContent.addEventListener('pointerdown', this.iframeDown); + // iframeContent.addEventListener( + // 'wheel', + // e => { + // e.ctrlKey && e.preventDefault(); + // }, + // { passive: false } + // ); const initHeights = () => { this._scrollHeight = Math.max(this._scrollHeight, (iframeContent.body.children[0] as any)?.scrollHeight || 0); if (this._scrollHeight) { this.rootDoc.nativeHeight = Math.min(NumCast(this.rootDoc.nativeHeight), this._scrollHeight); - this.layoutDoc.height = Math.min(this.layoutDoc[HeightSym](), (this.layoutDoc[WidthSym]() * NumCast(this.rootDoc.nativeHeight)) / NumCast(this.rootDoc.nativeWidth)); + this.layoutDoc.height = Math.min(this.layoutDoc[Height](), (this.layoutDoc[Width]() * NumCast(this.rootDoc.nativeHeight)) / NumCast(this.rootDoc.nativeWidth)); } }; initHeights(); @@ -482,7 +470,6 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps action(() => initHeights), 5000 ); - iframe.setAttribute('enable-annotation', 'true'); iframeContent.addEventListener( 'click', undoBatch( @@ -493,49 +480,63 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps } const origin = this.webField?.origin; if (href && origin) { + const batch = UndoManager.StartBatch('webclick'); e.stopPropagation(); - setTimeout(() => this.submitURL(href.replace(Utils.prepend(''), origin))); + setTimeout(() => { + this.setData(href.replace(Utils.prepend(''), origin)); + batch.end(); + }); if (this._outerRef.current) { - this._outerRef.current.scrollTop = NumCast(this.layoutDoc._scrollTop); + this._outerRef.current.scrollTop = NumCast(this.layoutDoc._layout_scrollTop); this._outerRef.current.scrollLeft = 0; } } }) ) ); - iframe.contentDocument.addEventListener('wheel', this.iframeWheel, false); - //iframe.contentDocument.addEventListener('scroll', () => !this.active() && this._iframe && (this._iframe.scrollTop = NumCast(this.layoutDoc._scrollTop), false)); + iframe.contentDocument.addEventListener('wheel', this.iframeWheel, { passive: false }); } }; @action - iframeWheel = (e: any) => { + iframeWheel = (e: WheelEvent) => { if (!this._scrollTimer) { - this._scrollTimer = setTimeout( - action(() => (this._scrollTimer = undefined)), - 250 - ); // this turns events off on the iframe which allows scrolling to change direction smoothly + addStyleSheetRule(WebBox.webStyleSheet, 'webBox-iframe', { 'pointer-events': 'none' }); + this._scrollTimer = setTimeout(() => { + this._scrollTimer = undefined; + clearStyleSheetRules(WebBox.webStyleSheet); + }, 250); // this turns events off on the iframe which allows scrolling to change direction smoothly + } + if (e.ctrlKey) { + if (this._innerCollectionView) { + this._innerCollectionView.zoom(e.screenX, e.screenY, e.deltaY); + const offset = e.clientY - NumCast(this.layoutDoc._layout_scrollTop); + this.layoutDoc.freeform_panY = offset - offset / NumCast(this.layoutDoc._freeform_scale) + NumCast(this.layoutDoc._layout_scrollTop) - NumCast(this.layoutDoc._layout_scrollTop) / NumCast(this.layoutDoc._freeform_scale); + } + e.preventDefault(); } }; @action setDashScrollTop = (scrollTop: number, timeout: number = 250) => { const iframeHeight = Math.max(scrollTop, this._scrollHeight - this.panelHeight()); - this._scrollTimer && clearTimeout(this._scrollTimer); - this._scrollTimer = setTimeout( - action(() => { - this._scrollTimer = undefined; - const newScrollTop = scrollTop > iframeHeight ? iframeHeight : scrollTop; - if (!LinkDocPreview.LinkInfo && this._outerRef.current && newScrollTop !== this.layoutDoc.thumbScrollTop && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath()))) { - this.layoutDoc.thumb = undefined; - this.layoutDoc.thumbScrollTop = undefined; - this.layoutDoc.thumbNativeWidth = undefined; - this.layoutDoc.thumbNativeHeight = undefined; - this.layoutDoc.scrollTop = this._outerRef.current.scrollTop = newScrollTop; - } else if (this._outerRef.current) this._outerRef.current.scrollTop = newScrollTop; - }), - timeout - ); + if (this._scrollTimer) { + clearTimeout(this._scrollTimer); + clearStyleSheetRules(WebBox.webStyleSheet); + } + addStyleSheetRule(WebBox.webStyleSheet, 'webBox-iframe', { 'pointer-events': 'none' }); + this._scrollTimer = setTimeout(() => { + clearStyleSheetRules(WebBox.webStyleSheet); + this._scrollTimer = undefined; + const newScrollTop = scrollTop > iframeHeight ? iframeHeight : scrollTop; + if (!LinkDocPreview.LinkInfo && this._outerRef.current && newScrollTop !== this.layoutDoc.thumbScrollTop && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath()))) { + this.layoutDoc.thumb = undefined; + this.layoutDoc.thumbScrollTop = undefined; + this.layoutDoc.thumbNativeWidth = undefined; + this.layoutDoc.thumbNativeHeight = undefined; + this.layoutDoc.layout_scrollTop = this._outerRef.current.scrollTop = newScrollTop; + } else if (this._outerRef.current) this._outerRef.current.scrollTop = newScrollTop; + }, timeout); }; goTo = (scrollTop: number, duration: number, easeFunc: 'linear' | 'ease' | undefined) => { @@ -551,14 +552,15 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }; forward = (checkAvailable?: boolean) => { - const future = Cast(this.rootDoc[this.fieldKey + '-future'], listSpec('string'), []); - const history = Cast(this.rootDoc[this.fieldKey + '-history'], listSpec('string'), []); + const future = Cast(this.rootDoc[this.fieldKey + '_future'], listSpec('string'), []); + const history = Cast(this.rootDoc[this.fieldKey + '_history'], listSpec('string'), []); if (checkAvailable) return future.length; runInAction(() => { if (future.length) { const curUrl = this._url; - this.rootDoc[this.fieldKey + '-history'] = new List<string>([...history, this._url]); - this.rootDoc[this.fieldKey] = new WebField(new URL(future.pop()!)); + this.dataDoc[this.fieldKey + '_history'] = new List<string>([...history, this._url]); + this.dataDoc[this.fieldKey] = new WebField(new URL(future.pop()!)); + this._scrollHeight = 0; if (this._webUrl === this._url) { this._webUrl = curUrl; setTimeout(action(() => (this._webUrl = this._url))); @@ -572,15 +574,16 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }; back = (checkAvailable?: boolean) => { - const future = Cast(this.rootDoc[this.fieldKey + '-future'], listSpec('string')); - const history = Cast(this.rootDoc[this.fieldKey + '-history'], listSpec('string'), []); + const future = Cast(this.rootDoc[this.fieldKey + '_future'], listSpec('string')); + const history = Cast(this.rootDoc[this.fieldKey + '_history'], listSpec('string'), []); if (checkAvailable) return history.length; runInAction(() => { if (history.length) { const curUrl = this._url; - if (future === undefined) this.rootDoc[this.fieldKey + '-future'] = new List<string>([this._url]); - else this.rootDoc[this.fieldKey + '-future'] = new List<string>([...future, this._url]); - this.layoutDoc[this.fieldKey] = new WebField(new URL(history.pop()!)); + if (future === undefined) this.dataDoc[this.fieldKey + '_future'] = new List<string>([this._url]); + else this.dataDoc[this.fieldKey + '_future'] = new List<string>([...future, this._url]); + this.dataDoc[this.fieldKey] = new WebField(new URL(history.pop()!)); + this._scrollHeight = 0; if (this._webUrl === this._url) { this._webUrl = curUrl; setTimeout(action(() => (this._webUrl = this._url))); @@ -607,23 +610,18 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps if (!newUrl) return; if (!newUrl.startsWith('http')) newUrl = 'http://' + newUrl; try { - const future = Cast(this.rootDoc[this.fieldKey + '-future'], listSpec('string')); - const history = Cast(this.rootDoc[this.fieldKey + '-history'], listSpec('string')); - const url = this.webField?.toString(); - if (url && !preview) { - this.rootDoc[this.fieldKey + '-history'] = new List<string>([...(history || []), url]); - this.layoutDoc._scrollTop = 0; + if (!preview) { if (this._webPageHasBeenRendered) { this.layoutDoc.thumb = undefined; this.layoutDoc.thumbScrollTop = undefined; this.layoutDoc.thumbNativeWidth = undefined; this.layoutDoc.thumbNativeHeight = undefined; } - future && (future.length = 0); } if (!preview) { - this.layoutDoc[this.fieldKey] = new WebField(new URL(newUrl)); - !dontUpdateIframe && (this._webUrl = this._url); + if (!dontUpdateIframe) { + this._webUrl = this._url; + } } } catch (e) { console.log('WebBox URL error:' + this._url); @@ -637,53 +635,34 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps const uri = dataTransfer.getData('text/uri-list'); const url = uri || html || this._url || ''; const newurl = url.startsWith(window.location.origin) ? url.replace(window.location.origin, this._url?.match(/http[s]?:\/\/[^\/]*/)?.[0] || '') : url; - this.submitURL(newurl); + this.setData(newurl); e.stopPropagation(); }; + + @action + setData = (data: Field | Promise<RefField | undefined>) => { + if (!(typeof data === 'string') && !(data instanceof WebField)) return false; + if (Field.toString(data) === this._url) return false; + this._scrollHeight = 0; + const oldUrl = this._url; + const history = Cast(this.rootDoc[this.fieldKey + '_history'], listSpec('string'), []); + const weburl = new WebField(Field.toString(data)); + this.dataDoc[this.fieldKey + '_future'] = new List<string>([]); + this.dataDoc[this.fieldKey + '_history'] = new List<string>([...(history || []), oldUrl]); + this.dataDoc[this.fieldKey] = weburl; + return true; + }; onWebUrlValueKeyDown = (e: React.KeyboardEvent) => { - e.key === 'Enter' && this.submitURL(this._keyInput.current!.value); + if (e.key === 'Enter') this.setData(this._keyInput.current!.value); e.stopPropagation(); }; - @computed get urlEditor() { - return ( - <div className="collectionMenu-webUrlButtons" onDrop={this.onWebUrlDrop} onDragOver={e => e.preventDefault()}> - <input - className="collectionMenu-urlInput" - key={this._url} - placeholder="ENTER URL" - defaultValue={this._url} - onDrop={this.onWebUrlDrop} - onDragOver={e => e.preventDefault()} - onKeyDown={this.onWebUrlValueKeyDown} - onClick={e => { - this._keyInput.current!.select(); - e.stopPropagation(); - }} - ref={this._keyInput} - /> - <div style={{ display: 'flex', flexDirection: 'row', justifyContent: 'space-between', maxWidth: '250px' }}> - <button className="submitUrl" onClick={() => this.submitURL(this._keyInput.current!.value)} onDragOver={e => e.stopPropagation()} onDrop={this.onWebUrlDrop}> - GO - </button> - <button className="submitUrl" onClick={() => this.back}> - {' '} - <FontAwesomeIcon icon="caret-left" size="lg" />{' '} - </button> - <button className="submitUrl" onClick={() => this.forward}> - {' '} - <FontAwesomeIcon icon="caret-right" size="lg" />{' '} - </button> - </div> - </div> - ); - } - specificContextMenu = (e: React.MouseEvent | PointerEvent): void => { const cm = ContextMenu.Instance; const funcs: ContextMenuProps[] = []; if (!cm.findByDescription('Options...')) { - !Doc.noviceMode && funcs.push({ description: (this.layoutDoc.useCors ? "Don't Use" : 'Use') + ' Cors', event: () => (this.layoutDoc.useCors = !this.layoutDoc.useCors), icon: 'snowflake' }); + !Doc.noviceMode && + funcs.push({ description: (this.layoutDoc[this.fieldKey + '_useCors'] ? "Don't Use" : 'Use') + ' Cors', event: () => (this.layoutDoc[this.fieldKey + '_useCors'] = !this.layoutDoc[this.fieldKey + '_useCors']), icon: 'snowflake' }); funcs.push({ description: (this.layoutDoc.allowScripts ? 'Prevent' : 'Allow') + ' Scripts', event: () => { @@ -696,16 +675,17 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps icon: 'snowflake', }); funcs.push({ - description: (!this.layoutDoc.forceReflow ? 'Force' : 'Prevent') + ' Reflow', + description: (!this.layoutDoc.layout_forceReflow ? 'Force' : 'Prevent') + ' Reflow', event: () => { - const nw = !this.layoutDoc.forceReflow ? undefined : Doc.NativeWidth(this.layoutDoc) - this.sidebarWidth() / (this.props.NativeDimScaling?.() || 1); - this.layoutDoc.forceReflow = !nw; + const nw = !this.layoutDoc.layout_forceReflow ? undefined : Doc.NativeWidth(this.layoutDoc) - this.sidebarWidth() / (this.props.NativeDimScaling?.() || 1); + this.layoutDoc.layout_forceReflow = !nw; if (nw) { - Doc.SetInPlace(this.layoutDoc, this.fieldKey + '-nativeWidth', nw, true); + Doc.SetInPlace(this.layoutDoc, this.fieldKey + '_nativeWidth', nw, true); } }, icon: 'snowflake', }); + funcs.push({ description: 'Create Thumbnail', event: () => this.updateThumb(), icon: 'portrait' }); cm.addItem({ description: 'Options...', subitems: funcs, icon: 'asterisk' }); } }; @@ -746,58 +726,43 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps }; @computed get urlContent() { - if (this._hackHide || (this.webThumb && !this._webPageHasBeenRendered && LightboxView.LightboxDoc !== this.rootDoc)) return null; - this.props.thumbShown?.(); + setTimeout( + action(() => { + if (this._initialScroll === undefined && !this._webPageHasBeenRendered) { + this.setScrollPos(NumCast(this.layoutDoc.thumbScrollTop, NumCast(this.layoutDoc.layout_scrollTop))); + } + this._webPageHasBeenRendered = true; + }) + ); const field = this.rootDoc[this.props.fieldKey]; - let view; if (field instanceof HtmlField) { - view = <span className="webBox-htmlSpan" contentEditable onPointerDown={e => e.stopPropagation()} dangerouslySetInnerHTML={{ __html: field.html }} />; - } else if (field instanceof WebField) { - const url = this.layoutDoc.useCors ? Utils.CorsProxy(this._webUrl) : this._webUrl; - view = ( + return <span className="webBox-htmlSpan" contentEditable onPointerDown={e => e.stopPropagation()} dangerouslySetInnerHTML={{ __html: field.html }} />; + } + if (field instanceof WebField) { + const url = this.layoutDoc[this.fieldKey + '_useCors'] ? Utils.CorsProxy(this._webUrl) : this._webUrl; + return ( <iframe className="webBox-iframe" - enable-annotation={'true'} - style={{ pointerEvents: this._scrollTimer ? 'none' : undefined }} ref={action((r: HTMLIFrameElement | null) => (this._iframe = r))} src={url} onLoad={this.iframeLoaded} + scrolling="no" // ugh.. on windows, I get an inner scroll bar for the iframe's body even though the scrollHeight should be set to the full height of the document. // the 'allow-top-navigation' and 'allow-top-navigation-by-user-activation' attributes are left out to prevent iframes from redirecting the top-level Dash page // sandbox={"allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin allow-scripts"} />; sandbox={`${this.layoutDoc.allowScripts ? 'allow-scripts' : ''} allow-forms allow-modals allow-orientation-lock allow-pointer-lock allow-popups allow-popups-to-escape-sandbox allow-presentation allow-same-origin`} /> ); - } else { - view = ( - <iframe - className="webBox-iframe" - enable-annotation={'true'} - style={{ pointerEvents: this._scrollTimer ? 'none' : undefined }} // if we allow pointer events when scrolling is on, then reversing direction does not work smoothly - ref={action((r: HTMLIFrameElement | null) => (this._iframe = r))} - src={'https://crossorigin.me/https://cs.brown.edu'} - /> - ); } - setTimeout( - action(() => { - this._scrollHeight = Math.max(this._scrollHeight, this._iframe && this._iframe.contentDocument && this._iframe.contentDocument.body ? this._iframe.contentDocument.body.scrollHeight : 0); - if (this._initialScroll === undefined && !this._webPageHasBeenRendered) { - this.setScrollPos(NumCast(this.layoutDoc.thumbScrollTop, NumCast(this.layoutDoc.scrollTop))); - } - this._webPageHasBeenRendered = true; - }) - ); - return view; + return <iframe className="webBox-iframe" ref={action((r: HTMLIFrameElement | null) => (this._iframe = r))} src={'https://crossorigin.me/https://cs.brown.edu'} />; } addDocumentWrapper = (doc: Doc | Doc[], annotationKey?: string) => { - console.log(annotationKey); - (doc instanceof Doc ? [doc] : doc).forEach(doc => (doc.webUrl = this._url)); + (doc instanceof Doc ? [doc] : doc).forEach(doc => (doc.presData = new WebField(this._url))); return this.addDocument(doc, annotationKey); }; sidebarAddDocument = (doc: Doc | Doc[], sidebarKey?: string) => { - if (!this.layoutDoc._showSidebar) this.toggleSidebar(); + if (!this.layoutDoc._layout_showSidebar) this.toggleSidebar(); return this.addDocumentWrapper(doc, sidebarKey); }; @observable _draggingSidebar = false; @@ -813,15 +778,15 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps .ScreenToLocalTransform() .scale(this.props.NativeDimScaling?.() || 1) .transformDirection(delta[0], delta[1]); - const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']); - const nativeHeight = NumCast(this.layoutDoc[this.fieldKey + '-nativeHeight']); + const nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']); + const nativeHeight = NumCast(this.layoutDoc[this.fieldKey + '_nativeHeight']); const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth); const ratio = (curNativeWidth + ((onButton ? 1 : -1) * localDelta[0]) / (this.props.NativeDimScaling?.() || 1)) / nativeWidth; if (ratio >= 1) { this.layoutDoc.nativeWidth = nativeWidth * ratio; this.layoutDoc.nativeHeight = nativeHeight * (1 + ratio); - onButton && (this.layoutDoc._width = this.layoutDoc[WidthSym]() + localDelta[0]); - this.layoutDoc._showSidebar = nativeWidth !== this.layoutDoc._nativeWidth; + onButton && (this.layoutDoc._width = this.layoutDoc[Width]() + localDelta[0]); + this.layoutDoc._layout_showSidebar = nativeWidth !== this.layoutDoc._nativeWidth; } return false; }), @@ -843,7 +808,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps title="Toggle Sidebar" style={{ display: !this.props.isContentActive() ? 'none' : undefined, - top: StrCast(this.rootDoc._showTitle) === 'title' ? 20 : 5, + top: StrCast(this.layoutDoc._layout_showTitle) === 'title' ? 20 : 5, backgroundColor: this.SidebarShown ? Colors.MEDIUM_BLUE : Colors.BLACK, }} onPointerDown={e => this.sidebarBtnDown(e, true)}> @@ -854,41 +819,57 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @observable _previewNativeWidth: Opt<number> = undefined; @observable _previewWidth: Opt<number> = undefined; toggleSidebar = action((preview: boolean = false) => { - var nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']); + var nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']); if (!nativeWidth) { - const defaultNativeWidth = this.rootDoc[this.fieldKey] instanceof WebField ? 850 : this.Document[WidthSym](); + const defaultNativeWidth = this.rootDoc[this.fieldKey] instanceof WebField ? 850 : this.Document[Width](); Doc.SetNativeWidth(this.dataDoc, Doc.NativeWidth(this.dataDoc) || defaultNativeWidth); - Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(this.dataDoc) || (this.Document[HeightSym]() / this.Document[WidthSym]()) * defaultNativeWidth); - nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']); + Doc.SetNativeHeight(this.dataDoc, Doc.NativeHeight(this.dataDoc) || (this.Document[Height]() / this.Document[Width]()) * defaultNativeWidth); + nativeWidth = NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']); } const sideratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? WebBox.openSidebarWidth : 0) + nativeWidth) / nativeWidth; const pdfratio = ((!this.layoutDoc.nativeWidth || this.layoutDoc.nativeWidth === nativeWidth ? WebBox.openSidebarWidth + WebBox.sidebarResizerWidth : 0) + nativeWidth) / nativeWidth; const curNativeWidth = NumCast(this.layoutDoc.nativeWidth, nativeWidth); if (preview) { this._previewNativeWidth = nativeWidth * sideratio; - this._previewWidth = (this.layoutDoc[WidthSym]() * nativeWidth * sideratio) / curNativeWidth; + this._previewWidth = (this.layoutDoc[Width]() * nativeWidth * sideratio) / curNativeWidth; this._showSidebar = true; } else { - this.layoutDoc._showSidebar = !this.layoutDoc._showSidebar; - this.layoutDoc._width = (this.layoutDoc[WidthSym]() * nativeWidth * pdfratio) / curNativeWidth; - if (!this.layoutDoc._showSidebar && !(this.dataDoc[this.fieldKey] instanceof WebField)) { - this.layoutDoc.nativeWidth = this.dataDoc[this.fieldKey + '-nativeWidth'] = undefined; + this.layoutDoc._layout_showSidebar = !this.layoutDoc._layout_showSidebar; + this.layoutDoc._width = (this.layoutDoc[Width]() * nativeWidth * pdfratio) / curNativeWidth; + if (!this.layoutDoc._layout_showSidebar && !(this.dataDoc[this.fieldKey] instanceof WebField)) { + this.layoutDoc.nativeWidth = this.dataDoc[this.fieldKey + '_nativeWidth'] = undefined; } else { this.layoutDoc.nativeWidth = nativeWidth * pdfratio; } } }); + @action + onZoomWheel = (e: React.WheelEvent) => { + if (this.props.isContentActive(true)) { + e.stopPropagation(); + } + }; sidebarWidth = () => { if (!this.SidebarShown) return 0; if (this._previewWidth) return WebBox.sidebarResizerWidth + WebBox.openSidebarWidth; // return default sidebar if previewing (as in viewing a link target) const nativeDiff = NumCast(this.layoutDoc.nativeWidth) - Doc.NativeWidth(this.dataDoc); return WebBox.sidebarResizerWidth + nativeDiff * (this.props.NativeDimScaling?.() || 1); }; + _innerCollectionView: CollectionFreeFormView | undefined; + zoomScaling = () => this._innerCollectionView?.zoomScaling() ?? 1; + setInnerContent = (component: DocComponentView) => (this._innerCollectionView = component as CollectionFreeFormView); + @computed get content() { const interactive = this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && Doc.ActiveTool === InkTool.None; return ( - <div className={'webBox-cont' + (interactive ? '-interactive' : '')} onKeyDown={e => e.stopPropagation()} style={{ width: !this.layoutDoc.forceReflow ? NumCast(this.layoutDoc[this.fieldKey + '-nativeWidth']) || `100%` : '100%' }}> - {this.urlContent} + <div + className={'webBox-cont' + (interactive ? '-interactive' : '')} + onKeyDown={e => e.stopPropagation()} + style={{ + width: !this.layoutDoc.layout_forceReflow ? NumCast(this.layoutDoc[this.fieldKey + '_nativeWidth']) || `100%` : '100%', + transform: `scale(${this.zoomScaling()}) translate(${-NumCast(this.layoutDoc.freeform_panX)}px, ${-NumCast(this.layoutDoc.freeform_panY)}px)`, + }}> + {this._hackHide || (this.webThumb && !this._webPageHasBeenRendered && LightboxView.LightboxDoc !== this.rootDoc) ? null : this.urlContent} </div> ); } @@ -896,7 +877,13 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps @computed get annotationLayer() { TraceMobx(); return ( - <div className="webBox-annotationLayer" style={{ height: Doc.NativeHeight(this.Document) || undefined }} ref={this._annotationLayer}> + <div + className="webBox-annotationLayer" + style={{ + transform: `scale(${this.zoomScaling()}) translate(${-NumCast(this.layoutDoc.freeform_panX)}px, ${-NumCast(this.layoutDoc.freeform_panY)}px)`, + height: Doc.NativeHeight(this.Document) || undefined, + }} + ref={this._annotationLayer}> {this.inlineTextAnnotations .sort((a, b) => NumCast(a.y) - NumCast(b.y)) .map(anno => ( @@ -906,19 +893,21 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps ); } @computed get SidebarShown() { - return this._showSidebar || this.layoutDoc._showSidebar ? true : false; + return this._showSidebar || this.layoutDoc._layout_showSidebar ? true : false; } @computed get webpage() { const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1; const pointerEvents = this.layoutDoc._lockedPosition ? 'none' : (this.props.pointerEvents?.() as any); const scale = previewScale * (this.props.NativeDimScaling?.() || 1); - const renderAnnotations = (docFilters: () => string[]) => ( + const renderAnnotations = (childFilters: () => string[]) => ( <CollectionFreeFormView {...this.props} - setContentView={emptyFunction} + setContentView={this.setInnerContent} NativeWidth={returnZero} NativeHeight={returnZero} + originTopLeft={false} + isAnnotationOverlayScrollable={true} renderDepth={this.props.renderDepth + 1} isAnnotationOverlay={true} fieldKey={this.annotationKey} @@ -929,8 +918,8 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps ScreenToLocalTransform={this.scrollXf} NativeDimScaling={returnOne} focus={this.focus} - dropAction={'alias'} - docFilters={docFilters} + dropAction="embed" + childFilters={childFilters} select={emptyFunction} isAnyChildContentActive={returnFalse} bringToFront={emptyFunction} @@ -938,23 +927,24 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} removeDocument={this.removeDocument} moveDocument={this.moveDocument} - addDocument={this.addDocument} + addDocument={this.addDocumentWrapper} childPointerEvents={this.props.isContentActive() ? 'all' : undefined} pointerEvents={this.annotationPointerEvents} /> ); return ( <div - className={'webBox-outerContent'} + className="webBox-outerContent" ref={this._outerRef} style={{ height: `${100 / scale}%`, pointerEvents, }} - onWheel={StopEvent} // block wheel events from propagating since they're handled by the iframe + // when active, block wheel events from propagating since they're handled by the iframe + onWheel={this.onZoomWheel} onScroll={e => this.setDashScrollTop(this._outerRef.current?.scrollTop || 0)} onPointerDown={this.onMarqueeDown}> - <div className={'webBox-innerContent'} style={{ height: this._webPageHasBeenRendered && this._scrollHeight ? this.scrollHeight : '100%', pointerEvents }}> + <div className="webBox-innerContent" style={{ height: (this._webPageHasBeenRendered && this._scrollHeight) || '100%', pointerEvents }}> {this.content} {<div style={{ display: DragManager.docsBeingDragged.length ? 'none' : undefined, mixBlendMode: 'multiply' }}>{renderAnnotations(this.transparentFilter)}</div>} {renderAnnotations(this.opaqueFilter)} @@ -1003,20 +993,19 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps setPreviewCursor = (func?: (x: number, y: number, drag: boolean, hide: boolean) => void) => (this._setPreviewCursor = func); panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1) - this.sidebarWidth() + WebBox.sidebarResizerWidth; panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); - scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._scrollTop)); + scrollXf = () => this.props.ScreenToLocalTransform().translate(0, NumCast(this.layoutDoc._layout_scrollTop)); anchorMenuClick = () => this._sidebarRef.current?.anchorMenuClick; - transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()]; - opaqueFilter = () => [...this.props.docFilters(), Utils.noDragsDocFilter, ...(DragManager.docsBeingDragged.length ? [] : [Utils.IsOpaqueFilter()])]; + transparentFilter = () => [...this.props.childFilters(), Utils.IsTransparentFilter()]; + opaqueFilter = () => [...this.props.childFilters(), Utils.noDragsDocFilter, ...(DragManager.docsBeingDragged.length ? [] : [Utils.IsOpaqueFilter()])]; childStyleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => { if (doc instanceof Doc && property === StyleProp.PointerEvents) { - if (doc.textInlineAnnotations) return 'none'; + if (this.inlineTextAnnotations.includes(doc)) return 'none'; } return this.props.styleProvider?.(doc, props, property); }; pointerEvents = () => (!this._draggingSidebar && this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && !MarqueeOptionsMenu.Instance?.isShown() ? 'all' : SnappingManager.GetIsDragging() ? undefined : 'none'); annotationPointerEvents = () => (this._isAnnotating || SnappingManager.GetIsDragging() || Doc.ActiveTool !== InkTool.None ? 'all' : 'none'); render() { - setTimeout(() => DocListCast(this.rootDoc[this.annotationKey]).forEach(doc => (doc.webUrl = this._url))); const previewScale = this._previewNativeWidth ? 1 - this.sidebarWidth() / this._previewNativeWidth : 1; const pointerEvents = this.layoutDoc._lockedPosition ? 'none' : (this.props.pointerEvents?.() as any); const scale = previewScale * (this.props.NativeDimScaling?.() || 1); @@ -1070,7 +1059,7 @@ export class WebBox extends ViewBoxAnnotatableComponent<ViewBoxAnnotatableProps ref={this._sidebarRef} {...this.props} whenChildContentsActiveChanged={this.whenChildContentsActiveChanged} - fieldKey={this.fieldKey + '-' + this._urlHash} + fieldKey={this.fieldKey + '_' + this._urlHash} rootDoc={this.rootDoc} layoutDoc={this.layoutDoc} dataDoc={this.dataDoc} diff --git a/src/client/views/nodes/button/FontIconBadge.tsx b/src/client/views/nodes/button/FontIconBadge.tsx index 3b5aac221..b50588ce2 100644 --- a/src/client/views/nodes/button/FontIconBadge.tsx +++ b/src/client/views/nodes/button/FontIconBadge.tsx @@ -1,10 +1,6 @@ -import { observer } from "mobx-react"; -import * as React from "react"; -import { AclPrivate, Doc, DocListCast } from "../../../../fields/Doc"; -import { GetEffectiveAcl } from "../../../../fields/util"; -import { emptyFunction, returnFalse, setupMoveUpEvents } from "../../../../Utils"; -import { DragManager } from "../../../util/DragManager"; -import "./FontIconBadge.scss"; +import { observer } from 'mobx-react'; +import * as React from 'react'; +import './FontIconBadge.scss'; interface FontIconBadgeProps { value: string | undefined; @@ -25,13 +21,17 @@ export class FontIconBadge extends React.Component<FontIconBadgeProps> { // } render() { - if (this.props.value === undefined) return (null); - return <div className="fontIconBadge-container" ref={this._notifsRef}> - <div className="fontIconBadge" style={{ "display": "initial" }} - // onPointerDown={this.onPointerDown} - > - {this.props.value} + if (this.props.value === undefined) return null; + return ( + <div className="fontIconBadge-container" ref={this._notifsRef}> + <div + className="fontIconBadge" + style={{ display: 'initial' }} + // onPointerDown={this.onPointerDown} + > + {this.props.value} + </div> </div> - </div>; + ); } -}
\ No newline at end of file +} diff --git a/src/client/views/nodes/button/FontIconBox.tsx b/src/client/views/nodes/button/FontIconBox.tsx index 26515da30..5bba51ec8 100644 --- a/src/client/views/nodes/button/FontIconBox.tsx +++ b/src/client/views/nodes/button/FontIconBox.tsx @@ -5,7 +5,8 @@ import { action, computed, observable, runInAction } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { ColorState, SketchPicker } from 'react-color'; -import { Doc, HeightSym, StrListCast, WidthSym } from '../../../../fields/Doc'; +import { Doc, StrListCast } from '../../../../fields/Doc'; +import { Height, Width } from '../../../../fields/DocSymbols'; import { InkTool } from '../../../../fields/InkField'; import { ScriptField } from '../../../../fields/ScriptField'; import { BoolCast, Cast, NumCast, ScriptCast, StrCast } from '../../../../fields/Types'; @@ -16,7 +17,7 @@ import { CollectionViewType, DocumentType } from '../../../documents/DocumentTyp import { LinkManager } from '../../../util/LinkManager'; import { ScriptingGlobals } from '../../../util/ScriptingGlobals'; import { SelectionManager } from '../../../util/SelectionManager'; -import { undoBatch, UndoManager } from '../../../util/UndoManager'; +import { undoable, undoBatch, UndoManager } from '../../../util/UndoManager'; import { CollectionFreeFormView } from '../../collections/collectionFreeForm'; import { ContextMenu } from '../../ContextMenu'; import { DocComponent } from '../../DocComponent'; @@ -44,16 +45,12 @@ export enum ButtonType { ToggleButton = 'tglBtn', ColorButton = 'colorBtn', ToolButton = 'toolBtn', - NumberButton = 'numBtn', + NumberSliderButton = 'numSliderBtn', + NumberDropdownButton = 'numDropdownBtn', + NumberInlineButton = 'numInlineBtn', EditableText = 'editableText', } -export enum NumButtonType { - Slider = 'slider', - DropdownOptions = 'list', - Inline = 'inline', -} - export interface ButtonProps extends FieldViewProps { type?: ButtonType; } @@ -98,7 +95,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { // Determining UI Specs @computed get label() { - return StrCast(this.rootDoc.label, StrCast(this.rootDoc.title)); + return StrCast(this.rootDoc.icon_label, StrCast(this.rootDoc.title)); } Icon = (color: string) => { const icon = StrCast(this.dataDoc[this.fieldKey ?? 'icon'] ?? this.dataDoc.icon, 'user') as any; @@ -132,112 +129,116 @@ export class FontIconBox extends DocComponent<ButtonProps>() { /** * Number button */ - @computed get numberButton() { - const numBtnType: string = StrCast(this.rootDoc.numBtnType); - const numScript = ScriptCast(this.rootDoc.script); - const setValue = (value: number) => UndoManager.RunInBatch(() => numScript?.script.run({ self: this.rootDoc, value, _readOnly_: false }), 'set num value'); - + @computed get numberSliderButton() { + const numScript = (value?: number) => ScriptCast(this.rootDoc.script).script.run({ self: this.rootDoc, value, _readOnly_: value === undefined }); // Script for checking the outcome of the toggle - const checkResult = Number(numScript?.script.run({ self: this.rootDoc, value: 0, _readOnly_: true }).result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3)); - + const checkResult = Number(numScript().result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3)); const label = !FontIconBox.GetShowLabels() ? null : <div className="fontIconBox-label">{this.label}</div>; - if (numBtnType === NumButtonType.Slider) { - const dropdown = ( - <div className="menuButton-dropdownBox" onPointerDown={e => e.stopPropagation()}> - <input - type="range" - step="1" - min={NumCast(this.rootDoc.numBtnMin, 0)} - max={NumCast(this.rootDoc.numBtnMax, 100)} - value={checkResult} - className="menu-slider" - onPointerDown={() => (this._batch = UndoManager.StartBatch('presDuration'))} - onPointerUp={() => this._batch?.end()} - onChange={e => { - e.stopPropagation(); - setValue(Number(e.target.value)); - }} - /> + const dropdown = ( + <div className="menuButton-dropdownBox" onPointerDown={e => e.stopPropagation()}> + <input + className="menu-slider" + type="range" + step="1" + min={NumCast(this.rootDoc.numBtnMin, 0)} + max={NumCast(this.rootDoc.numBtnMax, 100)} + //readOnly={true} + value={checkResult} + onPointerDown={() => (this._batch = UndoManager.StartBatch('num slider changing'))} + onPointerUp={() => this._batch?.end()} + onChange={undoable(e => { + e.stopPropagation(); + numScript(Number(e.target.value)); + }, 'set num value')} + /> + </div> + ); + return ( + <div + className="menuButton numBtn slider" + onPointerDown={e => e.stopPropagation()} + onClick={action(() => { + this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen; + this.noTooltip = this.rootDoc.dropDownOpen; + Doc.UnBrushAllDocs(); + })}> + {checkResult} + {label} + {this.rootDoc.dropDownOpen ? dropdown : null} + </div> + ); + } + /** + * Number button + */ + @computed get numberDropdownButton() { + const numScript = (value?: number) => ScriptCast(this.rootDoc.script)?.script.run({ self: this.rootDoc, value, _readOnly_: value === undefined }); + + const checkResult = Number(numScript().result ?? 0).toPrecision(NumCast(this.dataDoc.numPrecision, 3)); + + const items: number[] = []; + for (let i = 0; i < 100; i += 2) items.push(i); + + const list = items.map(value => { + return ( + <div + className="list-item" + key={`${value}`} + style={{ + backgroundColor: value.toString() === checkResult ? Colors.LIGHT_BLUE : undefined, + }} + onClick={undoable(value => numScript(value), `${this.rootDoc.title} button set from list`)}> + {value} </div> ); - return ( + }); + return ( + <div className="menuButton numBtn list"> + <div className="button" onClick={undoable(e => numScript(Number(checkResult) - 1), `${this.rootDoc.title} decrement value`)}> + <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon="minus" /> + </div> <div - className={`menuButton ${this.type} ${numBtnType}`} - onPointerDown={e => e.stopPropagation()} + className={`button ${'number'}`} + onPointerDown={e => { + e.stopPropagation(); + e.preventDefault(); + }} onClick={action(() => { this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen; this.noTooltip = this.rootDoc.dropDownOpen; Doc.UnBrushAllDocs(); })}> - {checkResult} - {label} - {this.rootDoc.dropDownOpen ? dropdown : null} + <input style={{ width: 30 }} className="button-input" type="number" value={checkResult} readOnly={true} onChange={undoable(e => numScript(Number(e.target.value)), `${this.rootDoc.title} button set value`)} /> + </div> + <div className={`button`} onClick={undoable(e => numScript(Number(checkResult) + 1), `${this.rootDoc.title} increment value`)}> + <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={'plus'} /> </div> - ); - } else if (numBtnType === NumButtonType.DropdownOptions) { - const items: number[] = []; - for (let i = 0; i < 100; i++) { - if (i % 2 === 0) { - items.push(i); - } - } - const list = items.map(value => { - return ( - <div - className="list-item" - key={`${value}`} - style={{ - backgroundColor: value.toString() === checkResult ? Colors.LIGHT_BLUE : undefined, - }} - onClick={() => setValue(value)}> - {value} - </div> - ); - }); - return ( - <div className={`menuButton ${this.type} ${numBtnType}`}> - <div className={`button`} onClick={action(e => setValue(Number(checkResult) - 1))}> - <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={'minus'} /> - </div> - <div - className={`button ${'number'}`} - onPointerDown={e => { - e.stopPropagation(); - e.preventDefault(); - }} - onClick={action(() => { - this.rootDoc.dropDownOpen = !this.rootDoc.dropDownOpen; - this.noTooltip = this.rootDoc.dropDownOpen; - Doc.UnBrushAllDocs(); - })}> - <input style={{ width: 30 }} className="button-input" type="number" value={checkResult} onChange={undoBatch(action(e => setValue(Number(e.target.value))))} /> - </div> - <div className={`button`} onClick={action(e => setValue(Number(checkResult) + 1))}> - <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={'plus'} /> - </div> - {this.rootDoc.dropDownOpen ? ( - <div> - <div className="menuButton-dropdownList" style={{ left: '25%' }}> - {list} - </div> - <div - className="dropbox-background" - onClick={action(e => { - e.stopPropagation(); - this.rootDoc.dropDownOpen = false; - this.noTooltip = false; - Doc.UnBrushAllDocs(); - })} - /> + {this.rootDoc.dropDownOpen ? ( + <div> + <div className="menuButton-dropdownList" style={{ left: '25%' }}> + {list} </div> - ) : null} - </div> - ); - } else { - return <div />; - } + <div + className="dropbox-background" + onClick={action(e => { + e.stopPropagation(); + this.rootDoc.dropDownOpen = false; + this.noTooltip = false; + Doc.UnBrushAllDocs(); + })} + /> + </div> + ) : null} + </div> + ); + } + /** + * Number button + */ + @computed get numberInlineButton() { + return <div />; } /** @@ -290,7 +291,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { const selected = SelectionManager.Docs().lastElement(); if (selected) { if (StrCast(selected.type) === DocumentType.COL) { - text = StrCast(selected._viewType); + text = StrCast(selected._type_collection); } else { dropdown = false; text = selected.type === DocumentType.RTF ? 'Text' : StrCast(selected.type); @@ -313,12 +314,12 @@ export class FontIconBox extends DocComponent<ButtonProps>() { .map(value => ( <div className="list-item" - key={`${value}`} + key={value} style={{ fontFamily: script.script.originalScript.startsWith('{ return setFont') ? value : undefined, backgroundColor: value === text ? Colors.LIGHT_BLUE : undefined, }} - onClick={undoBatch(() => script.script.run({ self: this.rootDoc, value }))}> + onClick={undoable(() => script.script.run({ self: this.rootDoc, value }), value)}> {value[0].toUpperCase() + value.slice(1)} </div> )); @@ -505,7 +506,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { <div className="menuButton editableText"> <FontAwesomeIcon className={`fontIconBox-icon-${this.type}`} icon={'lock'} /> <div style={{ width: 'calc(100% - .875em)', paddingLeft: '4px' }}> - <EditableView GetValue={() => script?.script.run({ value: '', _readOnly_: true }).result} SetValue={setValue} contents={checkResult} /> + <EditableView GetValue={() => script?.script.run({ value: '', _readOnly_: true }).result} SetValue={setValue} oneLine={true} contents={checkResult} /> </div> </div> ); @@ -528,7 +529,9 @@ export class FontIconBox extends DocComponent<ButtonProps>() { case ButtonType.EditableText: return this.editableText; case ButtonType.DropdownList: button = this.dropdownListButton; break; case ButtonType.ColorButton: button = this.colorButton; break; - case ButtonType.NumberButton: button = this.numberButton; break; + case ButtonType.NumberDropdownButton: button = this.numberDropdownButton; break; + case ButtonType.NumberInlineButton: button = this.numberInlineButton; break; + case ButtonType.NumberSliderButton: button = this.numberSliderButton; break; case ButtonType.DropdownButton: button = this.dropdownButton; break; case ButtonType.ToggleButton: button = this.toggleButton; break; case ButtonType.TextButton: @@ -568,7 +571,7 @@ export class FontIconBox extends DocComponent<ButtonProps>() { // toggle: Set overlay status of selected document ScriptingGlobals.add(function setView(view: string) { const selected = SelectionManager.Docs().lastElement(); - selected ? (selected._viewType = view) : console.log('[FontIconBox.tsx] changeView failed'); + selected ? (selected._type_collection = view) : console.log('[FontIconBox.tsx] changeView failed'); }); // toggle: Set overlay status of selected document @@ -613,7 +616,7 @@ ScriptingGlobals.add(function setHeaderColor(color?: string, checkResult?: boole } Doc.SharingDoc().userColor = undefined; Doc.GetProto(Doc.SharingDoc()).userColor = color; - Doc.UserDoc().showTitle = color === 'transparent' ? undefined : StrCast(Doc.UserDoc().showTitle, 'creationDate'); + Doc.UserDoc().layout_showTitle = color === 'transparent' ? undefined : StrCast(Doc.UserDoc().layout_showTitle, 'author_date'); }); // toggle: Set overlay status of selected document @@ -626,41 +629,42 @@ ScriptingGlobals.add(function toggleOverlay(checkResult?: boolean) { selected ? selected.props.CollectionFreeFormDocumentView?.().float() : console.log('[FontIconBox.tsx] toggleOverlay failed'); }); -ScriptingGlobals.add(function showFreeform(attr: 'grid' | 'snapline' | 'clusters' | 'arrange' | 'viewAll', checkResult?: boolean) { +ScriptingGlobals.add(function showFreeform(attr: 'flashcards' | 'grid' | 'snaplines' | 'clusters' | 'arrange' | 'viewAll', checkResult?: boolean) { const selected = SelectionManager.Docs().lastElement(); // prettier-ignore - const map: Map<'grid' | 'snapline' | 'clusters' | 'arrange'| 'viewAll', { undo: boolean, checkResult: (doc:Doc) => any; setDoc: (doc:Doc) => void;}> = new Map([ + const map: Map<'flashcards' | 'grid' | 'snaplines' | 'clusters' | 'arrange'| 'viewAll', { waitForRender?: boolean, checkResult: (doc:Doc) => any; setDoc: (doc:Doc) => void;}> = new Map([ ['grid', { - undo: false, - checkResult: (doc:Doc) => doc._backgroundGridShow, - setDoc: (doc:Doc) => doc._backgroundGridShow = !doc._backgroundGridShow, + checkResult: (doc:Doc) => doc._freeform_backgroundGrid, + setDoc: (doc:Doc) => doc._freeform_backgroundGrid = !doc._freeform_backgroundGrid, }], - ['snapline', { - undo: false, - checkResult: (doc:Doc) => doc.showSnapLines, - setDoc: (doc:Doc) => doc._showSnapLines = !doc._showSnapLines, + ['snaplines', { + checkResult: (doc:Doc) => doc._freeform_snapLines, + setDoc: (doc:Doc) => doc._freeform_snapLines = !doc._freeform_snapLines, }], ['viewAll', { - undo: false, - checkResult: (doc:Doc) => doc._fitContentsToBox, - setDoc: (doc:Doc) => doc._fitContentsToBox = !doc._fitContentsToBox, + checkResult: (doc:Doc) => doc._freeform_fitContentsToBox, + setDoc: (doc:Doc) => doc._freeform_fitContentsToBox = !doc._freeform_fitContentsToBox, }], ['clusters', { - undo: false, - checkResult: (doc:Doc) => doc._useClusters, - setDoc: (doc:Doc) => doc._useClusters = !doc._useClusters, + waitForRender: true, // flags that undo batch should terminate after a re-render giving the script the chance to fire + checkResult: (doc:Doc) => doc._freeform_useClusters, + setDoc: (doc:Doc) => doc._freeform_useClusters = !doc._freeform_useClusters, }], ['arrange', { - undo: true, + waitForRender: true, // flags that undo batch should terminate after a re-render giving the script the chance to fire checkResult: (doc:Doc) => doc._autoArrange, setDoc: (doc:Doc) => doc._autoArrange = !doc._autoArrange, }], - ]); + ['flashcards', { + checkResult: (doc:Doc) => Doc.UserDoc().defaultToFlashcards, + setDoc: (doc:Doc) => Doc.UserDoc().defaultToFlashcards = !Doc.UserDoc().defaultToFlashcards, + }], + ]); if (checkResult) { return map.get(attr)?.checkResult(selected) ? Colors.MEDIUM_BLUE : 'transparent'; } - const batch = map.get(attr)?.undo ? UndoManager.StartBatch('set feature') : { end: () => {} }; + const batch = map.get(attr)?.waitForRender ? UndoManager.StartBatch('set freeform attribute') : { end: () => {} }; SelectionManager.Docs().map(dv => map.get(attr)?.setDoc(dv)); setTimeout(() => batch.end(), 100); }); @@ -754,8 +758,8 @@ export function createInkGroup(inksToGroup?: Doc[], isSubGroup?: boolean) { action(d => { const x = NumCast(d.x); const y = NumCast(d.y); - const width = d[WidthSym](); - const height = d[HeightSym](); + const width = d[Width](); + const height = d[Height](); bounds.push({ x, y, width, height }); }) ); @@ -792,8 +796,8 @@ export function createInkGroup(inksToGroup?: Doc[], isSubGroup?: boolean) { // TODO: nda - this is the code to actually get a new grouped collection const newCollection = marqViewRef?.getCollection(selected, undefined, true); if (newCollection) { - newCollection.height = newCollection[HeightSym](); - newCollection.width = newCollection[WidthSym](); + newCollection.height = newCollection[Height](); + newCollection.width = newCollection[Width](); } // nda - bug: when deleting a stroke before leaving writing mode, delete the stroke from unprocessed ink docs @@ -850,8 +854,8 @@ ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'fillColor' | ' // prettier-ignore const map: Map<'inkMask' | 'fillColor' | 'strokeWidth' | 'strokeColor', { checkResult: () => any; setInk: (doc: Doc) => void; setMode: () => void }> = new Map([ ['inkMask', { - checkResult: () => ((selected?.type === DocumentType.INK ? BoolCast(selected.isInkMask) : ActiveIsInkMask()) ? Colors.MEDIUM_BLUE : 'transparent'), - setInk: (doc: Doc) => (doc.isInkMask = !doc.isInkMask), + checkResult: () => ((selected?.type === DocumentType.INK ? BoolCast(selected.stroke_isInkMask) : ActiveIsInkMask()) ? Colors.MEDIUM_BLUE : 'transparent'), + setInk: (doc: Doc) => (doc.stroke_isInkMask = !doc.stroke_isInkMask), setMode: () => selected?.type !== DocumentType.INK && SetActiveIsInkMask(!ActiveIsInkMask()), }], ['fillColor', { @@ -860,8 +864,8 @@ ScriptingGlobals.add(function setInkProperty(option: 'inkMask' | 'fillColor' | ' setMode: () => SetActiveFillColor(StrCast(value)), }], [ 'strokeWidth', { - checkResult: () => (selected?.type === DocumentType.INK ? NumCast(selected.strokeWidth) : ActiveInkWidth()), - setInk: (doc: Doc) => (doc.strokeWidth = NumCast(value)), + checkResult: () => (selected?.type === DocumentType.INK ? NumCast(selected.stroke_width) : ActiveInkWidth()), + setInk: (doc: Doc) => (doc.stroke_width = NumCast(value)), setMode: () => SetActiveInkWidth(value.toString()), }], ['strokeColor', { @@ -889,7 +893,7 @@ ScriptingGlobals.add(function webSetURL(url: string, checkResult?: boolean) { if (checkResult) { return StrCast(selected.rootDoc.data, Cast(selected.rootDoc.data, WebField, null)?.url?.href); } - (selected.ComponentView as WebBox).submitURL(url); + selected.ComponentView?.setData?.(url); //selected.rootDoc.data = new WebField(url); } }); @@ -914,23 +918,32 @@ ScriptingGlobals.add(function webBack(checkResult?: boolean) { ScriptingGlobals.add(function toggleSchemaPreview(checkResult?: boolean) { const selected = SelectionManager.Docs().lastElement(); if (checkResult && selected) { - const result: boolean = NumCast(selected.schemaPreviewWidth) > 0; + const result: boolean = NumCast(selected.schema_previewWidth) > 0; if (result) return Colors.MEDIUM_BLUE; else return 'transparent'; } else if (selected) { - if (NumCast(selected.schemaPreviewWidth) > 0) { - selected.schemaPreviewWidth = 0; + if (NumCast(selected.schema_previewWidth) > 0) { + selected.schema_previewWidth = 0; } else { - selected.schemaPreviewWidth = 200; + selected.schema_previewWidth = 200; } } }); +ScriptingGlobals.add(function toggleSingleLineSchema(checkResult?: boolean) { + const selected = SelectionManager.Docs().lastElement(); + if (checkResult && selected) { + return NumCast(selected._schema_singleLine) > 0 ? Colors.MEDIUM_BLUE : 'transparent'; + } + if (selected) { + selected._schema_singleLine = !selected._schema_singleLine; + } +}); /** STACK * groupBy */ ScriptingGlobals.add(function setGroupBy(key: string, checkResult?: boolean) { - SelectionManager.Docs().map(doc => (doc._fontFamily = key)); + SelectionManager.Docs().map(doc => (doc._text_fontFamily = key)); const editorView = RichTextMenu.Instance.TextView?.EditorView; if (checkResult) { return StrCast((editorView ? RichTextMenu.Instance : Doc.UserDoc()).fontFamily); diff --git a/src/client/views/nodes/formattedText/DashDocView.tsx b/src/client/views/nodes/formattedText/DashDocView.tsx index c05a30d1a..48f4c2afd 100644 --- a/src/client/views/nodes/formattedText/DashDocView.tsx +++ b/src/client/views/nodes/formattedText/DashDocView.tsx @@ -2,8 +2,9 @@ import { action, IReactionDisposer, observable, reaction } from 'mobx'; import { observer } from 'mobx-react'; import { NodeSelection } from 'prosemirror-state'; import * as ReactDOM from 'react-dom/client'; -import { Doc, HeightSym, WidthSym } from '../../../../fields/Doc'; -import { Cast, NumCast, StrCast } from '../../../../fields/Types'; +import { Doc } from '../../../../fields/Doc'; +import { Height, Width } from '../../../../fields/DocSymbols'; +import { NumCast, StrCast } from '../../../../fields/Types'; import { emptyFunction, returnFalse, Utils } from '../../../../Utils'; import { DocServer } from '../../../DocServer'; import { Docs, DocUtils } from '../../../documents/Documents'; @@ -41,18 +42,33 @@ export class DashDocView { this.root = ReactDOM.createRoot(this.dom); this.root.render( - <DashDocViewInternal docId={node.attrs.docId} alias={node.attrs.alias} width={node.attrs.width} height={node.attrs.height} hidden={node.attrs.hidden} fieldKey={node.attrs.fieldKey} tbox={tbox} view={view} node={node} getPos={getPos} /> + <DashDocViewInternal + docId={node.attrs.docId} + embedding={node.attrs.embedding} + width={node.attrs.width} + height={node.attrs.height} + hidden={node.attrs.hidden} + fieldKey={node.attrs.fieldKey} + tbox={tbox} + view={view} + node={node} + getPos={getPos} + /> ); } destroy() { - setTimeout(this.root.unmount); + setTimeout(() => { + try { + this.root.unmount(); + } catch {} + }); } selectNode() {} } interface IDashDocViewInternal { docId: string; - alias: string; + embedding: string; tbox: FormattedTextBox; width: string; height: string; @@ -69,25 +85,18 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> { _textBox: FormattedTextBox; @observable _dashDoc: Doc | undefined; @observable _finalLayout: any; - @observable _resolvedDataDoc: any; @observable _width: number = 0; @observable _height: number = 0; updateDoc = action((dashDoc: Doc) => { this._dashDoc = dashDoc; - this._finalLayout = this.props.docId ? dashDoc : Doc.expandTemplateLayout(Doc.Layout(dashDoc), dashDoc); + this._finalLayout = dashDoc; - if (this._finalLayout) { - if (!Doc.AreProtosEqual(this._finalLayout, dashDoc)) { - this._finalLayout.rootDocument = dashDoc.aliasOf; - } - this._resolvedDataDoc = Cast(this._finalLayout.resolvedDataDoc, Doc, null); - } if (this.props.width !== (this._dashDoc?._width ?? '') + 'px' || this.props.height !== (this._dashDoc?._height ?? '') + 'px') { try { this._width = NumCast(this._dashDoc?._width); this._height = NumCast(this._dashDoc?._height); - // bcz: an exception will be thrown if two aliases are open at the same time when a doc view comment is made + // bcz: an exception will be thrown if two embeddings are open at the same time when a doc view comment is made this.props.view.dispatch( this.props.view.state.tr.setNodeMarkup(this.props.getPos(), null, { ...this.props.node.attrs, @@ -105,15 +114,15 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> { super(props); this._textBox = this.props.tbox; - DocServer.GetRefField(this.props.docId + this.props.alias).then(async dashDoc => { + DocServer.GetRefField(this.props.docId + this.props.embedding).then(async dashDoc => { if (!(dashDoc instanceof Doc)) { - this.props.alias && + this.props.embedding && DocServer.GetRefField(this.props.docId).then(async dashDocBase => { if (dashDocBase instanceof Doc) { - const aliasedDoc = Doc.MakeAlias(dashDocBase, this.props.docId + this.props.alias); - aliasedDoc.layoutKey = 'layout'; - this.props.fieldKey && DocUtils.makeCustomViewClicked(aliasedDoc, Docs.Create.StackingDocument, this.props.fieldKey, undefined); - this.updateDoc(aliasedDoc); + const embedding = Doc.MakeEmbedding(dashDocBase, this.props.docId + this.props.embedding); + embedding.layout_fieldKey = 'layout'; + this.props.fieldKey && DocUtils.makeCustomViewClicked(embedding, Docs.Create.StackingDocument, this.props.fieldKey, undefined); + this.updateDoc(embedding); } }); } else { @@ -191,7 +200,6 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> { onWheel={e => e.preventDefault()}> <DocumentView Document={this._finalLayout} - DataDoc={this._resolvedDataDoc} addDocument={returnFalse} rootSelected={returnFalse} //{this._textBox.props.isSelected} removeDocument={this.removeDoc} @@ -203,14 +211,14 @@ export class DashDocViewInternal extends React.Component<IDashDocViewInternal> { addDocTab={this._textBox.props.addDocTab} pinToPres={returnFalse} renderDepth={this._textBox.props.renderDepth + 1} - PanelWidth={this._finalLayout[WidthSym]} - PanelHeight={this._finalLayout[HeightSym]} + PanelWidth={this._finalLayout[Width]} + PanelHeight={this._finalLayout[Height]} focus={this.outerFocus} whenChildContentsActiveChanged={returnFalse} bringToFront={emptyFunction} dontRegisterView={false} - docFilters={this.props.tbox?.props.docFilters} - docRangeFilters={this.props.tbox?.props.docRangeFilters} + childFilters={this.props.tbox?.props.childFilters} + childFiltersByRanges={this.props.tbox?.props.childFiltersByRanges} searchFilterDocs={this.props.tbox?.props.searchFilterDocs} /> </div> diff --git a/src/client/views/nodes/formattedText/DashFieldView.tsx b/src/client/views/nodes/formattedText/DashFieldView.tsx index bf6fa2ec6..b4fb7a44e 100644 --- a/src/client/views/nodes/formattedText/DashFieldView.tsx +++ b/src/client/views/nodes/formattedText/DashFieldView.tsx @@ -2,18 +2,17 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; import { action, computed, IReactionDisposer, observable } from 'mobx'; import { observer } from 'mobx-react'; -import { NodeSelection } from 'prosemirror-state'; import * as ReactDOM from 'react-dom/client'; -import { DataSym, Doc, DocListCast, Field } from '../../../../fields/Doc'; +import { Doc } from '../../../../fields/Doc'; import { List } from '../../../../fields/List'; import { listSpec } from '../../../../fields/Schema'; import { SchemaHeaderField } from '../../../../fields/SchemaHeaderField'; -import { ComputedField } from '../../../../fields/ScriptField'; import { Cast, StrCast } from '../../../../fields/Types'; -import { emptyFunction, returnFalse, setupMoveUpEvents } from '../../../../Utils'; +import { emptyFunction, returnFalse, returnZero, setupMoveUpEvents } from '../../../../Utils'; import { DocServer } from '../../../DocServer'; import { CollectionViewType } from '../../../documents/DocumentTypes'; import { AntimodeMenu, AntimodeMenuProps } from '../../AntimodeMenu'; +import { SchemaTableCell } from '../../collections/collectionSchema/SchemaTableCell'; import { OpenWhere } from '../DocumentView'; import './DashFieldView.scss'; import { FormattedTextBox } from './FormattedTextBox'; @@ -70,10 +69,10 @@ export class DashFieldView { } catch {} }); } - deselectNode() { + @action deselectNode() { this.dom.classList.remove('ProseMirror-selectednode'); } - selectNode() { + @action selectNode() { this.dom.classList.add('ProseMirror-selectednode'); } } @@ -98,6 +97,7 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna _fieldKey: string; _fieldStringRef = React.createRef<HTMLSpanElement>(); @observable _dashDoc: Doc | undefined; + @observable _expanded = false; constructor(props: IDashFieldViewInternal) { super(props); @@ -114,139 +114,46 @@ export class DashFieldViewInternal extends React.Component<IDashFieldViewInterna this._reactionDisposer?.(); } - public static multiValueDelimeter = ';'; - public static fieldContent(textBoxDoc: Doc, dashDoc: Doc, fieldKey: string) { - const dashVal = dashDoc[fieldKey] ?? dashDoc[DataSym][fieldKey] ?? ''; - const fval = dashVal instanceof List ? dashVal.join(DashFieldViewInternal.multiValueDelimeter) : StrCast(dashVal).startsWith(':=') || dashVal === '' ? Doc.Layout(textBoxDoc)[fieldKey] : dashVal; - return { boolVal: Cast(fval, 'boolean', null), strVal: Field.toString(fval as Field) || '' }; - } - // set the display of the field's value (checkbox for booleans, span of text for strings) @computed get fieldValueContent() { - if (this._dashDoc) { - const { boolVal, strVal } = DashFieldViewInternal.fieldContent(this._textBoxDoc, this._dashDoc, this._fieldKey); - // field value is a boolean, so use a checkbox or similar widget to display it - if (boolVal === true || boolVal === false) { - return ( - <input - className="dashFieldView-fieldCheck" - type="checkbox" - checked={boolVal} - onChange={e => { - if (this._fieldKey.startsWith('_')) Doc.Layout(this._textBoxDoc)[this._fieldKey] = e.target.checked; - Doc.SetInPlace(this._dashDoc!, this._fieldKey, e.target.checked, true); - }} - /> - ); - } // field value is a string, so display it as an editable span - else { - // bcz: this is unfortunate, but since this React component is nested within a non-React text box (prosemirror), we can't - // use React events. Essentially, React events occur after native events have been processed, so corresponding React events - // will never fire because Prosemirror has handled the native events. So we add listeners for native events here. - return ( - <span - className="dashFieldView-fieldSpan" - contentEditable={!this.props.unclickable()} - style={{ display: strVal.length < 2 ? 'inline-block' : undefined }} - suppressContentEditableWarning={true} - defaultValue={strVal} - ref={r => { - r?.addEventListener('keydown', e => this.fieldSpanKeyDown(e, r)); - r?.addEventListener('blur', e => r && this.updateText(r.textContent!, false)); - r?.addEventListener( - 'pointerdown', - action(e => { - // let target = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span> - // while (target && !target.dataset?.targethrefs) target = target.parentElement; - this.props.tbox.EditorView!.dispatch(this.props.tbox.EditorView!.state.tr.setSelection(new NodeSelection(this.props.tbox.EditorView!.state.doc.resolve(this.props.getPos())))); - // FormattedTextBoxComment.update(this.props.tbox, this.props.tbox.EditorView!, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc); - // e.stopPropagation(); - }) - ); - }}> - {strVal} - </span> - ); - } - } + return !this._dashDoc ? null : ( + <div onClick={action(e => (this._expanded = !this.props.editable ? !this._expanded : true))} style={{ fontSize: 'smaller', width: this.props.hideKey ? this.props.tbox.props.PanelWidth() - 20 : undefined }}> + <SchemaTableCell + Document={this._dashDoc} + col={0} + deselectCell={emptyFunction} + selectCell={emptyFunction} + maxWidth={this.props.hideKey ? undefined : () => 100} + columnWidth={this.props.hideKey ? () => this.props.tbox.props.PanelWidth() - 20 : returnZero} + selectedCell={() => [this._dashDoc!, 0]} + fieldKey={this._fieldKey} + rowHeight={returnZero} + isRowActive={() => this._expanded && this.props.editable} + padding={0} + getFinfo={emptyFunction} + setColumnValues={returnFalse} + allowCRs={true} + oneLine={!this._expanded} + finishEdit={action(() => (this._expanded = false))} + /> + </div> + ); } - // we need to handle all key events on the input span or else they will propagate to prosemirror. - @action - fieldSpanKeyDown = (e: KeyboardEvent, span: HTMLSpanElement) => { - if (e.key === 'c' && (e.ctrlKey || e.metaKey)) { - navigator.clipboard.writeText(window.getSelection()?.toString() || ''); - return; - } - if (e.key === 'Enter') { - // handle the enter key by "submitting" the current text to Dash's database. - this.updateText(span.textContent!, true); - e.preventDefault(); // prevent default to avoid a newline from being generated and wiping out this field view - } - if (e.key === 'a' && (e.ctrlKey || e.metaKey)) { - // handle ctrl-A to select all the text within the span - if (window.getSelection) { - const range = document.createRange(); - range.selectNodeContents(span); - window.getSelection()!.removeAllRanges(); - window.getSelection()!.addRange(range); - } - e.preventDefault(); //prevent default so that all the text in the prosemirror text box isn't selected - } - if (!this.props.editable) { - e.preventDefault(); - } - e.stopPropagation(); // we need to handle all events or else they will propagate to prosemirror. - }; - - @action - updateText = (nodeText: string, forceMatch: boolean) => { - if (nodeText) { - const newText = nodeText.startsWith(':=') || nodeText.startsWith('=:=') ? ':=-computed-' : nodeText; - // look for a document whose id === the fieldKey being displayed. If there's a match, then that document - // holds the different enumerated values for the field in the titles of its collected documents. - // if there's a partial match from the start of the input text, complete the text --- TODO: make this an auto suggest box and select from a drop down. - DocServer.GetRefField(this._fieldKey).then(options => { - let modText = ''; - options instanceof Doc && DocListCast(options.data).forEach(opt => (forceMatch ? StrCast(opt.title).startsWith(newText) : StrCast(opt.title) === newText) && (modText = StrCast(opt.title))); - if (modText) { - // elementfieldSpan.innerHTML = this._dashDoc![this._fieldKey as string] = modText; - Doc.SetInPlace(this._dashDoc!, this._fieldKey, modText, true); - } // if the text starts with a ':=' then treat it as an expression by making a computed field from its value storing it in the key - else if (nodeText.startsWith(':=')) { - this._dashDoc![DataSym][this._fieldKey] = ComputedField.MakeFunction(nodeText.substring(2)); - } else if (nodeText.startsWith('=:=')) { - Doc.Layout(this._textBoxDoc)[this._fieldKey] = ComputedField.MakeFunction(nodeText.substring(3)); - } else { - if (Number(newText).toString() === newText) { - if (this._fieldKey.startsWith('_')) Doc.Layout(this._textBoxDoc)[this._fieldKey] = Number(newText); - Doc.SetInPlace(this._dashDoc!, this._fieldKey, Number(newText), true); - } else { - const splits = newText.split(DashFieldViewInternal.multiValueDelimeter); - if (!this._textBoxDoc[this._fieldKey]) { - const strVal = splits.length > 1 ? new List<string>(splits) : newText; - if (this._fieldKey.startsWith('_')) Doc.Layout(this._textBoxDoc)[this._fieldKey] = strVal; - Doc.SetInPlace(this._dashDoc!, this._fieldKey, strVal, true); - } - } - } - }); - } - }; - createPivotForField = (e: React.MouseEvent) => { let container = this.props.tbox.props.DocumentView?.().props.docViewPath().lastElement(); if (container) { - const alias = Doc.MakeAlias(container.props.Document); - alias._viewType = CollectionViewType.Time; - let list = Cast(alias._columnHeaders, listSpec(SchemaHeaderField)); + const embedding = Doc.MakeEmbedding(container.rootDoc); + embedding._type_collection = CollectionViewType.Time; + const colHdrKey = '_' + container.LayoutFieldKey + '_columnHeaders'; + let list = Cast(embedding[colHdrKey], listSpec(SchemaHeaderField)); if (!list) { - alias._columnHeaders = list = new List<SchemaHeaderField>(); + embedding[colHdrKey] = list = new List<SchemaHeaderField>(); } list.map(c => c.heading).indexOf(this._fieldKey) === -1 && list.push(new SchemaHeaderField(this._fieldKey, '#f1efeb')); list.map(c => c.heading).indexOf('text') === -1 && list.push(new SchemaHeaderField('text', '#f1efeb')); - alias._pivotField = this._fieldKey.startsWith('#') ? 'tags' : this._fieldKey; - this.props.tbox.props.addDocTab(alias, OpenWhere.addRight); + embedding._pivotField = this._fieldKey.startsWith('#') ? 'tags' : this._fieldKey; + this.props.tbox.props.addDocTab(embedding, OpenWhere.addRight); } }; diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.scss b/src/client/views/nodes/formattedText/FormattedTextBox.scss index b5a3c5d84..109b62e6f 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.scss +++ b/src/client/views/nodes/formattedText/FormattedTextBox.scss @@ -41,7 +41,7 @@ audiotag:hover { flex-direction: row; transition: opacity 1s; width: 100%; - position: absolute; + position: relative; top: 0; left: 0; } diff --git a/src/client/views/nodes/formattedText/FormattedTextBox.tsx b/src/client/views/nodes/formattedText/FormattedTextBox.tsx index 555944438..f4cecb1dc 100644 --- a/src/client/views/nodes/formattedText/FormattedTextBox.tsx +++ b/src/client/views/nodes/formattedText/FormattedTextBox.tsx @@ -12,7 +12,7 @@ import { Fragment, Mark, Node, Slice } from 'prosemirror-model'; import { EditorState, NodeSelection, Plugin, TextSelection, Transaction } from 'prosemirror-state'; import { EditorView } from 'prosemirror-view'; import { DateField } from '../../../../fields/DateField'; -import { AclAdmin, AclAugment, AclEdit, CssSym, Doc, DocListCast, Field, ForceServerWrite, HeightSym, Opt, UpdatingFromServer, WidthSym } from '../../../../fields/Doc'; +import { AclAdmin, AclAugment, AclEdit, AclSelfEdit, CssSym, Doc, DocListCast, Field, ForceServerWrite, HeightSym, Opt, StrListCast, UpdatingFromServer, WidthSym } from '../../../../fields/Doc'; import { Id } from '../../../../fields/FieldSymbols'; import { InkTool } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; @@ -74,8 +74,11 @@ const translateGoogleApi = require('translate-google-api'); export const GoogleRef = 'googleDocId'; type PullHandler = (exportState: Opt<GoogleApiClientUtils.Docs.ImportResult>, dataDoc: Doc) => void; +export interface FormattedTextBoxProps { + allowScroll?: boolean; +} @observer -export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps>() { +export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps & FormattedTextBoxProps>() { public static LayoutString(fieldStr: string) { return FieldView.LayoutString(FormattedTextBox, fieldStr); } @@ -115,38 +118,38 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps return this._editorView; } public get SidebarKey() { - return this.fieldKey + '-sidebar'; + return this.fieldKey + '_sidebar'; } @computed get allSidebarDocs() { return DocListCast(this.dataDoc[this.SidebarKey]); } @computed get noSidebar() { - return this.props.docViewPath().lastElement()?.props.hideDecorationTitle || this.props.noSidebar || this.Document._noSidebar; + return this.props.docViewPath().lastElement()?.props.hideDecorationTitle || this.props.noSidebar || this.Document._layout_noSidebar; } - @computed get sidebarWidthPercent() { - return this._showSidebar ? '20%' : StrCast(this.layoutDoc._sidebarWidthPercent, '0%'); + @computed get layout_sidebarWidthPercent() { + return this._showSidebar ? '20%' : StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%'); } @computed get sidebarColor() { - return StrCast(this.layoutDoc.sidebarColor, StrCast(this.layoutDoc[this.fieldKey + '-backgroundColor'], '#e4e4e4')); + return StrCast(this.layoutDoc.sidebar_color, StrCast(this.layoutDoc[this.fieldKey + '_backgroundColor'], '#e4e4e4')); } - @computed get autoHeight() { - return (this.props.forceAutoHeight || this.layoutDoc._autoHeight) && !this.props.ignoreAutoHeight; + @computed get layout_autoHeight() { + return (this.props.forceAutoHeight || this.layoutDoc._layout_autoHeight) && !this.props.ignoreAutoHeight; } @computed get textHeight() { - return NumCast(this.rootDoc[this.fieldKey + '-height']); + return NumCast(this.rootDoc[this.fieldKey + '_height']); } @computed get scrollHeight() { - return NumCast(this.rootDoc[this.fieldKey + '-scrollHeight']); + return NumCast(this.rootDoc[this.fieldKey + '_scrollHeight']); } @computed get sidebarHeight() { - return !this.sidebarWidth() ? 0 : NumCast(this.rootDoc[this.SidebarKey + '-height']); + return !this.sidebarWidth() ? 0 : NumCast(this.rootDoc[this.SidebarKey + '_height']); } @computed get titleHeight() { return this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.HeaderMargin) || 0; } - @computed get autoHeightMargins() { - return this.titleHeight + NumCast(this.layoutDoc._autoHeightMargins); + @computed get layout_autoHeightMargins() { + return this.titleHeight + NumCast(this.layoutDoc._layout_autoHeightMargins); } @computed get _recording() { return this.dataDoc?.mediaState === 'recording'; @@ -210,8 +213,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps public RemoveLinkFromDoc(linkDoc?: Doc) { this.unhighlightSearchTerms(); const state = this._editorView?.state; - const a1 = linkDoc?.anchor1 as Doc; - const a2 = linkDoc?.anchor2 as Doc; + const a1 = linkDoc?.link_anchor_1 as Doc; + const a2 = linkDoc?.link_anchor_2 as Doc; if (state && a1 && a2 && this._editorView) { this.removeDocument(a1); this.removeDocument(a2); @@ -240,7 +243,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps getAnchor = (addAsAnnotation: boolean, pinProps?: PinProps) => { if (!pinProps && this._editorView?.state.selection.empty) return this.rootDoc; - const anchor = Docs.Create.TextanchorDocument({ annotationOn: this.rootDoc, unrendered: true }); + const anchor = Docs.Create.TextConfigDocument({ annotationOn: this.rootDoc }); this.addDocument(anchor); this.makeLinkAnchor(anchor, OpenWhere.addRight, undefined, 'Anchored Selection', false, addAsAnnotation); PresBox.pinDocView(anchor, { pinDocLayout: pinProps?.pinDocLayout, pinData: { ...(pinProps?.pinData ?? {}), scrollable: true } }, this.rootDoc); @@ -252,18 +255,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps AnchorMenu.Instance.Status = 'marquee'; AnchorMenu.Instance.OnClick = (e: PointerEvent) => { - !this.layoutDoc.showSidebar && this.toggleSidebar(); + !this.layoutDoc.layout_showSidebar && this.toggleSidebar(); setTimeout(() => this._sidebarRef.current?.anchorMenuClick(this.makeLinkAnchor(undefined, OpenWhere.addRight, undefined, 'Anchored Selection', true))); // give time for sidebarRef to be created }; AnchorMenu.Instance.OnAudio = (e: PointerEvent) => { - !this.layoutDoc.showSidebar && this.toggleSidebar(); + !this.layoutDoc.layout_showSidebar && this.toggleSidebar(); const anchor = this.makeLinkAnchor(undefined, OpenWhere.addRight, undefined, 'Anchored Selection', true, true); setTimeout(() => { const target = this._sidebarRef.current?.anchorMenuClick(anchor); if (target) { anchor.followLinkAudio = true; DocumentViewInternal.recordAudioAnnotation(Doc.GetProto(target), Doc.LayoutFieldKey(target)); - target.title = ComputedField.MakeFunction(`self["text-audioAnnotations-text"].lastElement()`); + target.title = ComputedField.MakeFunction(`self["text_audioAnnotations_text"].lastElement()`); } }); }; @@ -303,7 +306,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const curProto = Cast(Cast(dataDoc.proto, Doc, null)?.[this.fieldKey], RichTextField, null); // the default text inherited from a prototype const curLayout = this.rootDoc !== this.layoutDoc ? Cast(this.layoutDoc[this.fieldKey], RichTextField, null) : undefined; // the default text stored in a layout template const json = JSON.stringify(state.toJSON()); - const effectiveAcl = GetEffectiveAcl(this.rootDoc); + const effectiveAcl = GetEffectiveAcl(dataDoc); const removeSelection = (json: string | undefined) => (json?.indexOf('"storedMarks"') === -1 ? json?.replace(/"selection":.*/, '') : json?.replace(/"selection":"\"storedMarks\""/, '"storedMarks"')); @@ -317,29 +320,25 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps dataDoc.tags = accumTags.length ? new List<string>(Array.from(new Set<string>(accumTags))) : undefined; let unchanged = true; - if (this._applyingChange !== this.fieldKey && removeSelection(json) !== removeSelection(curProto?.Data)) { + if (this._applyingChange !== this.fieldKey && removeSelection(newJson) !== removeSelection(prevData?.Data)) { this._applyingChange = this.fieldKey; - const textChange = curText !== Cast(dataDoc[this.fieldKey], RichTextField)?.Text; - textChange && (dataDoc[this.fieldKey + '-lastModified'] = new DateField(new Date(Date.now()))); - if ((!curTemp && !curProto) || curText || json.includes('dash')) { + const textChange = newText !== prevData?.Text; + textChange && (dataDoc[this.fieldKey + '_modificationDate'] = new DateField(new Date(Date.now()))); + if ((!prevData && !protoData) || newText || (!newText && !protoData)) { // if no template, or there's text that didn't come from the layout template, write it to the document. (if this is driven by a template, then this overwrites the template text which is intended) - if (removeSelection(json) !== removeSelection(curLayout?.Data)) { + if (removeSelection(newJson) !== removeSelection(prevLayoutData?.Data)) { const numstring = NumCast(dataDoc[this.fieldKey], null); - if (numstring !== undefined) { - dataDoc[this.fieldKey] = Number(curText); - } else { - dataDoc[this.fieldKey] = new RichTextField(json, curText); - } - dataDoc[this.fieldKey + '-noTemplate'] = true; //(curTemp?.Text || "") !== curText; // mark the data field as being split from the template if it has been edited - textChange && ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: curText }); + dataDoc[this.fieldKey] = numstring !== undefined ? Number(newText) : new RichTextField(newJson, newText); + dataDoc[this.fieldKey + '_noTemplate'] = true; //(curTemp?.Text || "") !== curText; // mark the data field as being split from the template if it has been edited + textChange && ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: newText }); unchanged = false; } } else { // if we've deleted all the text in a note driven by a template, then restore the template data dataDoc[this.fieldKey] = undefined; - this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse((curProto || curTemp).Data))); - dataDoc[this.fieldKey + '-noTemplate'] = undefined; // mark the data field as not being split from any template it might have - ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: curText }); + this._editorView.updateState(EditorState.fromJSON(this.config, JSON.parse((protoData || prevData).Data))); + dataDoc[this.fieldKey + '_noTemplate'] = undefined; // mark the data field as not being split from any template it might have + ScriptCast(this.layoutDoc.onTextChanged, null)?.script.run({ this: this.layoutDoc, self: this.rootDoc, text: newText }); unchanged = false; } this._applyingChange = ''; @@ -368,7 +367,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps let linkAnchor; let link; LinkManager.Links(this.dataDoc).forEach((l, i) => { - const anchor = (l.anchor1 as Doc).annotationOn ? (l.anchor1 as Doc) : (l.anchor2 as Doc).annotationOn ? (l.anchor2 as Doc) : undefined; + const anchor = (l.link_anchor_1 as Doc).annotationOn ? (l.link_anchor_1 as Doc) : (l.link_anchor_2 as Doc).annotationOn ? (l.link_anchor_2 as Doc) : undefined; if (anchor && (anchor.annotationOn as Doc).mediaState === 'recording') { linkTime = NumCast(anchor._timecodeToShow /* audioStart */); linkAnchor = anchor; @@ -404,7 +403,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps autoLink = () => { const newAutoLinks = new Set<Doc>(); - const oldAutoLinks = LinkManager.Links(this.props.Document).filter(link => link.linkRelationship === LinkManager.AutoKeywords); + const oldAutoLinks = LinkManager.Links(this.props.Document).filter(link => link.link_relationship === LinkManager.AutoKeywords); if (this._editorView?.state.doc.textContent) { const isNodeSel = this._editorView.state.selection instanceof NodeSelection; const f = this._editorView.state.selection.from; @@ -416,7 +415,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps tr = tr.setSelection(isNodeSel && false ? new NodeSelection(tr.doc.resolve(f)) : new TextSelection(tr.doc.resolve(f), tr.doc.resolve(t))); this._editorView?.dispatch(tr); } - oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.anchor2 !== this.rootDoc).forEach(LinkManager.Instance.deleteLink); + oldAutoLinks.filter(oldLink => !newAutoLinks.has(oldLink) && oldLink.link_anchor_2 !== this.rootDoc).forEach(LinkManager.Instance.deleteLink); }; updateTitle = () => { @@ -425,7 +424,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps !this.props.dontRegisterView && // (this.props.Document.isTemplateForField === "text" || !this.props.Document.isTemplateForField) && // only update the title if the data document's data field is changing (title.startsWith('-') || title.startsWith('@')) && this._editorView && - !this.dataDoc['title-custom'] && + !this.dataDoc.title_custom && (Doc.LayoutFieldKey(this.rootDoc) === this.fieldKey || this.fieldKey === 'text') ) { let node = this._editorView.state.doc; @@ -457,8 +456,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps if (node.firstChild === null && !node.marks.find((m: Mark) => m.type.name === schema.marks.noAutoLinkAnchor.name) && node.marks.find((m: Mark) => m.type.name === schema.marks.splitter.name)) { alink = alink ?? - (LinkManager.Links(this.Document).find(link => Doc.AreProtosEqual(Cast(link.anchor1, Doc, null), this.rootDoc) && Doc.AreProtosEqual(Cast(link.anchor2, Doc, null), target)) || - DocUtils.MakeLink(this.props.Document, target, { linkRelationship: LinkManager.AutoKeywords })!); + (LinkManager.Links(this.Document).find(link => Doc.AreProtosEqual(Cast(link.link_anchor_1, Doc, null), this.rootDoc) && Doc.AreProtosEqual(Cast(link.link_anchor_2, Doc, null), target)) || + DocUtils.MakeLink(this.props.Document, target, { link_relationship: LinkManager.AutoKeywords })!); newAutoLinks.add(alink); const allAnchors = [{ href: Doc.localServerPath(target), title: 'a link', anchorId: this.props.Document[Id] }]; allAnchors.push(...(node.marks.find((m: Mark) => m.type.name === schema.marks.autoLinkAnchor.name)?.attrs.allAnchors ?? [])); @@ -525,7 +524,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this.setupEditor(this.config, this.fieldKey); this._dropDisposer = DragManager.MakeDropTarget(ele, this.drop.bind(this), this.layoutDoc); } - // if (this.autoHeight) this.tryUpdateScrollHeight(); + // if (this.layout_autoHeight) this.tryUpdateScrollHeight(); }; @undoBatch @@ -545,17 +544,17 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } else if (de.embedKey) { const target = dragData.droppedDocuments[0]; const node = schema.nodes.dashDoc.create({ - width: target[WidthSym](), - height: target[HeightSym](), + width: target[Width](), + height: target[Height](), title: 'dashDoc', docId: target[Id], float: 'unset', }); - if (!['alias', 'copy'].includes((dragData.dropAction ?? '') as any)) { + if (!['embed', 'copy'].includes((dragData.dropAction ?? '') as any)) { dragData.removeDocument?.(dragData.draggedDocuments[0]); } - target._fitContentsToBox = true; - target.context = this.rootDoc; + target._freeform_fitContentsToBox = true; + target.embedContainer = this.rootDoc; const view = this._editorView!; view.dispatch(view.state.tr.insert(view.posAtCoords({ left: de.x, top: de.y })!.pos, node)); e.stopPropagation(); @@ -612,7 +611,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps if (Array.from(highlights).join('') === FormattedTextBox._globalHighlightsCache) return; setTimeout(() => (FormattedTextBox._globalHighlightsCache = Array.from(highlights).join(''))); clearStyleSheetRules(FormattedTextBox._userStyleSheet); - if (highlights.includes('Audio Tags')) { + if (!highlights.includes('Audio Tags')) { addStyleSheetRule(FormattedTextBox._userStyleSheet, 'audiotag', { display: 'none' }, ''); } if (highlights.includes('Text from Others')) { @@ -647,24 +646,24 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const hr = Math.round(Date.now() / 1000 / 60 / 60); numberRange(10).map(i => addStyleSheetRule(FormattedTextBox._userStyleSheet, 'UM-hr-' + (hr - i), { opacity: ((10 - i - 1) / 10).toString() })); } - this.layoutDoc[CssSym] = this.layoutDoc[CssSym] + 1; // css changes happen outside of react/mobx. so we need to set a flag that will notify anyone intereted in layout changes triggered by css changes (eg., CollectionLinkView) + this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css changes happen outside of react/mobx. so we need to set a flag that will notify anyone intereted in layout changes triggered by css changes (eg., CollectionLinkView) }; @observable _showSidebar = false; @computed get SidebarShown() { - return this._showSidebar || this.layoutDoc._showSidebar ? true : false; + return this._showSidebar || this.layoutDoc._layout_showSidebar ? true : false; } @action toggleSidebar = (preview: boolean = false) => { const prevWidth = 1 - this.sidebarWidth() / Number(getComputedStyle(this._ref.current!).width.replace('px', '')); if (preview) this._showSidebar = true; - else this.layoutDoc._showSidebar = (this.layoutDoc._sidebarWidthPercent = StrCast(this.layoutDoc._sidebarWidthPercent, '0%') === '0%' ? '50%' : '0%') !== '0%'; + else this.layoutDoc._layout_showSidebar = (this.layoutDoc._layout_sidebarWidthPercent = StrCast(this.layoutDoc._layout_sidebarWidthPercent, '0%') === '0%' ? '50%' : '0%') !== '0%'; this.layoutDoc._width = !preview && this.SidebarShown ? NumCast(this.layoutDoc._width) * 2 : Math.max(20, NumCast(this.layoutDoc._width) * prevWidth); }; sidebarDown = (e: React.PointerEvent) => { - const batch = UndoManager.StartBatch('sidebar'); + const batch = UndoManager.StartBatch('toggle sidebar'); setupMoveUpEvents( this, e, @@ -682,22 +681,23 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps .ScreenToLocalTransform() .scale(this.props.NativeDimScaling?.() || 1) .transformDirection(delta[0], delta[1]); - const sidebarWidth = (NumCast(this.layoutDoc._width) * Number(this.sidebarWidthPercent.replace('%', ''))) / 100; - const width = this.layoutDoc[WidthSym]() + localDelta[0]; - this.layoutDoc._sidebarWidthPercent = Math.max(0, (sidebarWidth + localDelta[0]) / width) * 100 + '%'; + const sidebarWidth = (NumCast(this.layoutDoc._width) * Number(this.layout_sidebarWidthPercent.replace('%', ''))) / 100; + const width = this.layoutDoc[Width]() + localDelta[0]; + this.layoutDoc._layout_sidebarWidthPercent = Math.max(0, (sidebarWidth + localDelta[0]) / width) * 100 + '%'; this.layoutDoc.width = width; - this.layoutDoc._showSidebar = this.layoutDoc._sidebarWidthPercent !== '0%'; + this.layoutDoc._layout_showSidebar = this.layoutDoc._layout_sidebarWidthPercent !== '0%'; e.preventDefault(); return false; }; - @undoBatch deleteAnnotation = (anchor: Doc) => { + const batch = UndoManager.StartBatch('delete link'); LinkManager.Instance.deleteLink(LinkManager.Links(anchor)[0]); // const docAnnotations = DocListCast(this.props.dataDoc[this.fieldKey]); // this.props.dataDoc[this.fieldKey] = new List<Doc>(docAnnotations.filter(a => a !== this.annoTextRegion)); // AnchorMenu.Instance.fadeOut(true); this.props.select(false); + setTimeout(batch.end); // wait for reaction to remove link from document }; @undoBatch @@ -730,17 +730,22 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps .split(' ') .filter(h => h); const anchorDoc = Array.from(hrefs).lastElement().replace(Doc.localServerPath(), '').split('?')[0]; + const deleteMarkups = undoBatch(() => { + const sel = editor.state.selection; + editor.dispatch(editor.state.tr.removeMark(sel.from, sel.to, editor.state.schema.marks.linkAnchor)); + }); e.persist(); anchorDoc && DocServer.GetRefField(anchorDoc).then( action(anchor => { + anchor && SelectionManager.SelectSchemaViewDoc(anchor as Doc); AnchorMenu.Instance.Status = 'annotation'; - AnchorMenu.Instance.Delete = () => this.deleteAnnotation(anchor as Doc); + AnchorMenu.Instance.Delete = !anchor && editor.state.selection.empty ? returnFalse : !anchor ? deleteMarkups : () => this.deleteAnnotation(anchor as Doc); AnchorMenu.Instance.Pinned = false; - AnchorMenu.Instance.PinToPres = () => this.pinToPres(anchor as Doc); - AnchorMenu.Instance.MakeTargetToggle = () => this.makeTargetToggle(anchor as Doc); - AnchorMenu.Instance.ShowTargetTrail = () => this.showTargetTrail(anchor as Doc); - AnchorMenu.Instance.IsTargetToggler = () => this.isTargetToggler(anchor as Doc); + AnchorMenu.Instance.PinToPres = !anchor ? returnFalse : () => this.pinToPres(anchor as Doc); + AnchorMenu.Instance.MakeTargetToggle = !anchor ? returnFalse : () => this.makeTargetToggle(anchor as Doc); + AnchorMenu.Instance.ShowTargetTrail = !anchor ? returnFalse : () => this.showTargetTrail(anchor as Doc); + AnchorMenu.Instance.IsTargetToggler = !anchor ? returnFalse : () => this.isTargetToggler(anchor as Doc); AnchorMenu.Instance.jumpTo(e.clientX, e.clientY, true); }) ); @@ -754,7 +759,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps description: 'plain', event: undoBatch(() => { Doc.setNativeView(this.rootDoc); - this.layoutDoc.autoHeightMargins = undefined; + this.layoutDoc.layout_autoHeightMargins = undefined; }), icon: 'eye', }); @@ -762,18 +767,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps description: 'metadata', event: undoBatch(() => { this.dataDoc.layout_meta = Cast(Doc.UserDoc().emptyHeader, Doc, null)?.layout; - this.rootDoc.layoutKey = 'layout_meta'; - setTimeout(() => (this.rootDoc._headerHeight = this.rootDoc._autoHeightMargins = 50), 50); + this.rootDoc.layout_fieldKey = 'layout_meta'; + setTimeout(() => (this.rootDoc._headerHeight = this.rootDoc._layout_autoHeightMargins = 50), 50); }), icon: 'eye', }); - const noteTypesDoc = Cast(Doc.UserDoc()['template-notes'], Doc, null); + const noteTypesDoc = Cast(Doc.UserDoc().template_notes, Doc, null); DocListCast(noteTypesDoc?.data).forEach(note => { const icon: IconProp = StrCast(note.icon) as IconProp; changeItems.push({ description: StrCast(note.title), event: undoBatch(() => { - this.layoutDoc.autoHeightMargins = undefined; + this.layoutDoc.layout_autoHeightMargins = undefined; Doc.setNativeView(this.rootDoc); DocUtils.makeCustomViewClicked(this.rootDoc, Docs.Create.TreeDocument, StrCast(note.title), note); }), @@ -799,11 +804,15 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps ); const uicontrols: ContextMenuProps[] = []; - uicontrols.push({ description: !this.Document._noSidebar ? 'Hide Sidebar Handle' : 'Show Sidebar Handle', event: () => (this.layoutDoc._noSidebar = !this.layoutDoc._noSidebar), icon: !this.Document._noSidebar ? 'eye-slash' : 'eye' }); uicontrols.push({ - description: (this.Document._showAltContentUI ? 'Hide' : 'Show') + ' Alt Content UI', - event: () => (this.layoutDoc._showAltContentUI = !this.layoutDoc._showAltContentUI), - icon: !this.Document._showAltContentUI ? 'eye-slash' : 'eye', + description: !this.Document._layout_noSidebar ? 'Hide Sidebar Handle' : 'Show Sidebar Handle', + event: () => (this.layoutDoc._layout_noSidebar = !this.layoutDoc._layout_noSidebar), + icon: !this.Document._layout_noSidebar ? 'eye-slash' : 'eye', + }); + uicontrols.push({ + description: (this.Document._layout_enableAltContentUI ? 'Hide' : 'Show') + ' Alt Content UI', + event: () => (this.layoutDoc._layout_enableAltContentUI = !this.layoutDoc._layout_enableAltContentUI), + icon: !this.Document._layout_enableAltContentUI ? 'eye-slash' : 'eye', }); uicontrols.push({ description: 'Show Highlights...', noexpand: true, subitems: highlighting, icon: 'hand-point-right' }); !Doc.noviceMode && @@ -834,10 +843,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this.rootDoc.title = this.layoutDoc.isTemplateForField as string; this.rootDoc.isTemplateDoc = false; this.rootDoc.isTemplateForField = ''; - this.rootDoc.layoutKey = 'layout'; + this.rootDoc.layout_fieldKey = 'layout'; MakeTemplate(this.rootDoc, true, title); setTimeout(() => { - this.rootDoc._autoHeight = this.layoutDoc._autoHeight; // autoHeight, width and height + this.rootDoc._layout_autoHeight = this.layoutDoc._layout_autoHeight; // layout_autoHeight, width and height this.rootDoc._width = this.layoutDoc._width || 300; // are stored on the template, since we're getting rid of the old template this.rootDoc._height = this.layoutDoc._height || 200; // we need to copy them over to the root. This should probably apply to all '_' fields this.rootDoc._backgroundColor = Cast(this.layoutDoc._backgroundColor, 'string', null); @@ -845,7 +854,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }, 10); } Doc.UserDoc().defaultTextLayout = new PrefetchProxy(this.rootDoc); - Doc.AddDocToList(Cast(Doc.UserDoc()['template-notes'], Doc, null), 'data', this.rootDoc); + Doc.AddDocToList(Cast(Doc.UserDoc().template_notes, Doc, null), 'data', this.rootDoc); }, icon: 'eye', }); @@ -856,11 +865,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps optionItems.push({ description: `Generate Dall-E Image`, event: () => this.generateImage(), icon: 'star' }); optionItems.push({ description: `Ask GPT-3`, event: () => this.askGPT(), icon: 'lightbulb' }); optionItems.push({ - description: !this.Document._singleLine ? 'Create New Doc on Carriage Return' : 'Allow Carriage Returns', - event: () => (this.layoutDoc._singleLine = !this.layoutDoc._singleLine), - icon: !this.Document._singleLine ? 'grip-lines' : 'bars', + description: !this.Document._createDocOnCR ? 'Create New Doc on Carriage Return' : 'Allow Carriage Returns', + event: () => (this.layoutDoc._createDocOnCR = !this.layoutDoc._createDocOnCR), + icon: !this.Document._createDocOnCR ? 'grip-lines' : 'bars', }); - !Doc.noviceMode && optionItems.push({ description: `${this.Document._autoHeight ? 'Lock' : 'Auto'} Height`, event: () => (this.layoutDoc._autoHeight = !this.layoutDoc._autoHeight), icon: this.Document._autoHeight ? 'lock' : 'unlock' }); + !Doc.noviceMode && + optionItems.push({ + description: `${this.Document._layout_autoHeight ? 'Lock' : 'Auto'} Height`, + event: () => (this.layoutDoc._layout_autoHeight = !this.layoutDoc._layout_autoHeight), + icon: this.Document._layout_autoHeight ? 'lock' : 'unlock', + }); optionItems.push({ description: `show markdown options`, event: RTFMarkup.Instance.open, icon: 'text' }); !options && cm.addItem({ description: 'Options...', subitems: optionItems, icon: 'eye' }); this._downX = this._downY = Number.NaN; @@ -900,18 +914,18 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps y: NumCast(this.rootDoc.y), _height: 200, _width: 200, - 'data-nativeWidth': result.nativeWidth, - 'data-nativeHeight': result.nativeHeight, + data_nativeWidth: result.nativeWidth, + data_nativeHeight: result.nativeHeight, }); - if (DocListCast(Doc.MyOverlayDocs?.data).includes(this.rootDoc)) { + if (Doc.IsInMyOverlay(this.rootDoc)) { newDoc.overlayX = this.rootDoc.x; newDoc.overlayY = NumCast(this.rootDoc.y) + NumCast(this.rootDoc._height); - Doc.AddDocToList(Doc.MyOverlayDocs, undefined, newDoc); + Doc.AddToMyOverlay(newDoc); } else { this.props.addDocument?.(newDoc); } // Create link between prompt and image - DocUtils.MakeLink(this.rootDoc, newDoc, { linkRelationship: 'Image Prompt' }); + DocUtils.MakeLink(this.rootDoc, newDoc, { link_relationship: 'Image Prompt' }); } } catch (err) { console.log(err); @@ -948,14 +962,14 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps if (this._editorView && this._recordingStart) { if (this._break) { const textanchorFunc = () => { - const tanch = Docs.Create.TextanchorDocument({ title: 'dictation anchor', unrendered: true }); + const tanch = Docs.Create.TextConfigDocument({ title: 'dictation anchor' }); return this.addDocument(tanch) ? tanch : undefined; }; const link = DocUtils.MakeLinkToActiveAudio(textanchorFunc, false).lastElement(); if (link) { Doc.GetProto(link).isDictation = true; - const audioanchor = Cast(link.anchor2, Doc, null); - const textanchor = Cast(link.anchor1, Doc, null); + const audioanchor = Cast(link.link_anchor_2, Doc, null); + const textanchor = Cast(link.link_anchor_1, Doc, null); if (audioanchor) { audioanchor.backgroundColor = 'tan'; const audiotag = this._editorView.state.schema.nodes.audiotag.create({ @@ -986,7 +1000,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const splitter = state.schema.marks.splitter.create({ id: Utils.GenerateGuid() }); let tr = state.tr.addMark(sel.from, sel.to, splitter); if (sel.from !== sel.to) { - const anchor = anchorDoc ?? Docs.Create.TextanchorDocument({ title: '#' + this._editorView?.state.doc.textBetween(sel.from, sel.to), annotationOn: this.dataDoc, unrendered: true }); + const anchor = + anchorDoc ?? + Docs.Create.TextConfigDocument({ + // + title: 'text(' + this._editorView?.state.doc.textBetween(sel.from, sel.to) + ')', + annotationOn: this.dataDoc, + }); const href = targetHref ?? Doc.localServerPath(anchor); if (anchor !== anchorDoc && addAsAnnotation) this.addDocument(anchor); tr.doc.nodesBetween(sel.from, sel.to, (node: any, pos: number, parent: any) => { @@ -1010,7 +1030,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } getView = async (doc: Doc) => { - if (DocListCast(this.rootDoc[this.SidebarKey]).find(anno => Doc.AreProtosEqual(doc.unrendered ? DocCast(doc.annotationOn) : doc, anno))) { + if (DocListCast(this.rootDoc[this.SidebarKey]).find(anno => Doc.AreProtosEqual(doc.layout_unrendered ? DocCast(doc.annotationOn) : doc, anno))) { !this.SidebarShown && this.toggleSidebar(false); setTimeout(() => this._sidebarRef?.current?.makeDocUnfiltered(doc)); } @@ -1079,11 +1099,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } }; - // if the scroll height has changed and we're in autoHeight mode, then we need to update the textHeight component of the doc. + // if the scroll height has changed and we're in layout_autoHeight mode, then we need to update the textHeight component of the doc. // Since we also monitor all component height changes, this will update the document's height. resetNativeHeight = (scrollHeight: number) => { const nh = this.layoutDoc.isTemplateForField ? 0 : NumCast(this.layoutDoc._nativeHeight); - this.rootDoc[this.fieldKey + '-height'] = scrollHeight; + this.rootDoc[this.fieldKey + '_height'] = scrollHeight; if (nh) this.layoutDoc._nativeHeight = scrollHeight; }; @@ -1094,9 +1114,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps !this.props.dontSelectOnLoad && this.props.setContentView?.(this); // this tells the DocumentView that this AudioBox is the "content" of the document. this allows the DocumentView to indirectly call getAnchor() on the AudioBox when making a link. this._cachedLinks = LinkManager.Links(this.Document); this._disposers.breakupDictation = reaction(() => DocumentManager.Instance.RecordingEvent, this.breakupDictation); - this._disposers.autoHeight = reaction( - () => this.autoHeight, - autoHeight => autoHeight && this.tryUpdateScrollHeight() + this._disposers.layout_autoHeight = reaction( + () => this.layout_autoHeight, + layout_autoHeight => layout_autoHeight && this.tryUpdateScrollHeight() ); this._disposers.highlights = reaction( () => Array.from(FormattedTextBox._globalHighlights).slice(), @@ -1108,16 +1128,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps width => this.tryUpdateScrollHeight() ); this._disposers.scrollHeight = reaction( - () => ({ scrollHeight: this.scrollHeight, autoHeight: this.autoHeight, width: NumCast(this.layoutDoc._width) }), - ({ width, scrollHeight, autoHeight }) => width && autoHeight && this.resetNativeHeight(scrollHeight), + () => ({ scrollHeight: this.scrollHeight, layout_autoHeight: this.layout_autoHeight, width: NumCast(this.layoutDoc._width) }), + ({ width, scrollHeight, layout_autoHeight }) => width && layout_autoHeight && this.resetNativeHeight(scrollHeight), { fireImmediately: true } ); this._disposers.componentHeights = reaction( - // set the document height when one of the component heights changes and autoHeight is on - () => ({ sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, autoHeight: this.autoHeight, marginsHeight: this.autoHeightMargins }), - ({ sidebarHeight, textHeight, autoHeight, marginsHeight }) => { + // set the document height when one of the component heights changes and layout_autoHeight is on + () => ({ sidebarHeight: this.sidebarHeight, textHeight: this.textHeight, layout_autoHeight: this.layout_autoHeight, marginsHeight: this.layout_autoHeightMargins }), + ({ sidebarHeight, textHeight, layout_autoHeight, marginsHeight }) => { const newHeight = this.contentScaling * (marginsHeight + Math.max(sidebarHeight, textHeight)); - if (autoHeight && newHeight && newHeight !== this.rootDoc.height && !this.props.dontRegisterView) { + if (layout_autoHeight && newHeight && newHeight !== this.rootDoc.height && !this.props.dontRegisterView) { this.props.setHeight?.(newHeight); } }, @@ -1142,8 +1162,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps this._disposers.editorState = reaction( () => { const dataDoc = Doc.IsDelegateField(DocCast(this.layoutDoc?.proto), this.fieldKey) ? DocCast(this.layoutDoc?.proto) : this?.dataDoc; - const whichDoc = !this.dataDoc || !this.layoutDoc ? undefined : dataDoc?.[this.fieldKey + '-noTemplate'] || !this.layoutDoc[this.fieldKey] ? dataDoc : this.layoutDoc; - return !whichDoc ? undefined : { data: Cast(whichDoc[this.fieldKey], RichTextField, null), str: Field.toString(whichDoc[this.fieldKey]) }; + const whichDoc = !this.dataDoc || !this.layoutDoc ? undefined : dataDoc?.[this.fieldKey + '_noTemplate'] || !this.layoutDoc[this.fieldKey] ? dataDoc : this.layoutDoc; + return !whichDoc ? undefined : { data: Cast(whichDoc[this.fieldKey], RichTextField, null), str: Field.toString(DocCast(whichDoc[this.fieldKey])) }; }, incomingValue => { if (this._editorView && this._applyingChange !== this.fieldKey) { @@ -1188,7 +1208,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps () => this.props.isSelected(), action(selected => { if (FormattedTextBox._globalHighlights.has('Bold Text')) { - this.layoutDoc[CssSym] = this.layoutDoc[CssSym] + 1; // css change happens outside of mobx/react, so this will notify anyone interested in the layout that it has changed + this.layoutDoc[DocCss] = this.layoutDoc[DocCss] + 1; // css change happens outside of mobx/react, so this will notify anyone interested in the layout that it has changed } if (RichTextMenu.Instance?.view === this._editorView && !selected) { RichTextMenu.Instance?.updateMenu(undefined, undefined, undefined); @@ -1220,7 +1240,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } var quickScroll: string | undefined = ''; this._disposers.scroll = reaction( - () => NumCast(this.layoutDoc._scrollTop), + () => NumCast(this.layoutDoc._layout_scrollTop), pos => { if (!this._ignoreScroll && this._scrollRef.current && !this.props.dontSelectOnLoad) { const viewTrans = quickScroll ?? StrCast(this.Document._viewTransition); @@ -1297,7 +1317,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } }, 0); dataDoc.title = exportState.title; - this.dataDoc['title-custom'] = true; + this.dataDoc.title_custom = true; dataDoc.googleDocUnchanged = true; } else { delete dataDoc[GoogleRef]; @@ -1354,7 +1374,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps view.state.schema.marks.linkAnchor.create({ allAnchors: [{ href: `/doc/${this.rootDoc[Id]}`, title: this.rootDoc.title, anchorId: `${this.rootDoc[Id]}` }], location: 'add:right', - title: `from: ${DocCast(pdfAnchor.context).title}`, + title: `from: ${DocCast(pdfAnchor.embedContainer).title}`, noPreview: true, docref: false, }), @@ -1363,7 +1383,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps ]), ]); - const link = DocUtils.MakeLink(pdfAnchor, this.rootDoc, { linkRelationship: 'PDF pasted' }); + const link = DocUtils.MakeLink(pdfAnchor, this.rootDoc, { link_relationship: 'PDF pasted' }); if (link) { view.dispatch(view.state.tr.replaceSelectionWith(dashField, false).scrollIntoView().setMeta('paste', true).setMeta('uiEvent', 'paste')); } @@ -1451,7 +1471,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps if (startupText) { dispatch(state.tr.insertText(startupText)); } - const textAlign = StrCast(this.dataDoc['text-align'], StrCast(Doc.UserDoc().textAlign, 'left')); + const textAlign = StrCast(this.dataDoc.text_align, StrCast(Doc.UserDoc().textAlign, 'left')); if (textAlign !== 'left') { selectAll(this._editorView.state, tr => { this._editorView!.dispatch(tr.replaceSelectionWith(state.schema.nodes.paragraph.create({ align: textAlign }))); @@ -1462,11 +1482,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } const selectOnLoad = this.rootDoc[Id] === FormattedTextBox.SelectOnLoad && (!LightboxView.LightboxDoc || LightboxView.IsLightboxDocView(this.props.docViewPath())); - if (selectOnLoad && !this.props.dontRegisterView && !this.props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) { + if (this._editorView && selectOnLoad && !this.props.dontRegisterView && !this.props.dontSelectOnLoad && this.isActiveTab(this.ProseRef)) { const selLoadChar = FormattedTextBox.SelectOnLoadChar; FormattedTextBox.SelectOnLoad = ''; this.props.select(false); - if (selLoadChar && this._editorView) { + if (selLoadChar) { const $from = this._editorView.state.selection.anchor ? this._editorView.state.doc.resolve(this._editorView.state.selection.anchor - 1) : undefined; const mark = schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }); const curMarks = this._editorView.state.storedMarks ?? $from?.marksAcross(this._editorView.state.selection.$head) ?? []; @@ -1476,10 +1496,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps .insertText(FormattedTextBox.SelectOnLoadChar, this._editorView.state.doc.content.size - 1, this._editorView.state.doc.content.size) .setStoredMarks(storedMarks); this._editorView.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(tr.doc.content.size)))); - } else if (this._editorView && curText && !FormattedTextBox.DontSelectInitialText) { + } else if (curText && !FormattedTextBox.DontSelectInitialText) { selectAll(this._editorView.state, this._editorView?.dispatch); - this.startUndoTypingBatch(); - } else if (this._editorView) { + } else { this._editorView.dispatch(this._editorView.state.tr.addStoredMark(schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) }))); } } @@ -1488,18 +1507,20 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps if (this._editorView) { const tr = this._editorView.state.tr; const { from, to } = tr.selection; - // for some reason, the selection is sometimes lost in the sidebar view when prosemirror syncs the seledtion with the dom, so reset the selectoin after the document has ben fully instantiated. + // for some reason, the selection is sometimes lost in the sidebar view when prosemirror syncs the seledtion with the dom, so reset the selection after the document has ben fully instantiated. if (FormattedTextBox.DontSelectInitialText) setTimeout(() => this._editorView?.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(from), tr.doc.resolve(to)))), 250); - this._editorView.state.storedMarks = [ - ...(this._editorView.state.storedMarks ?? []), - ...(!this._editorView.state.storedMarks?.some(mark => mark.type === schema.marks.user_mark) ? [schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })] : []), - ...(Doc.UserDoc().fontColor !== 'transparent' && Doc.UserDoc().fontColor ? [schema.mark(schema.marks.pFontColor, { color: StrCast(Doc.UserDoc().fontColor) })] : []), - ...(Doc.UserDoc().fontStyle === 'italics' ? [schema.mark(schema.marks.em)] : []), - ...(Doc.UserDoc().textDecoration === 'underline' ? [schema.mark(schema.marks.underline)] : []), - ...(Doc.UserDoc().fontFamily ? [schema.mark(schema.marks.pFontFamily, { family: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FontFamily) })] : []), - ...(Doc.UserDoc().fontSize ? [schema.mark(schema.marks.pFontSize, { fontSize: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FontSize) })] : []), - ...(Doc.UserDoc().fontWeight === 'bold' ? [schema.mark(schema.marks.strong)] : []), - ]; + this._editorView.dispatch( + this._editorView.state.tr.setStoredMarks([ + ...(this._editorView.state.storedMarks ?? []), + ...(!this._editorView.state.storedMarks?.some(mark => mark.type === schema.marks.user_mark) ? [schema.marks.user_mark.create({ userid: Doc.CurrentUserEmail, modified: Math.floor(Date.now() / 1000) })] : []), + ...(Doc.UserDoc().fontColor !== 'transparent' && Doc.UserDoc().fontColor ? [schema.mark(schema.marks.pFontColor, { color: StrCast(Doc.UserDoc().fontColor) })] : []), + ...(Doc.UserDoc().fontStyle === 'italics' ? [schema.mark(schema.marks.em)] : []), + ...(Doc.UserDoc().textDecoration === 'underline' ? [schema.mark(schema.marks.underline)] : []), + ...(Doc.UserDoc().fontFamily ? [schema.mark(schema.marks.pFontFamily, { family: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FontFamily) })] : []), + ...(Doc.UserDoc().fontSize ? [schema.mark(schema.marks.pFontSize, { fontSize: this.props.styleProvider?.(this.rootDoc, this.props, StyleProp.FontSize) })] : []), + ...(Doc.UserDoc().fontWeight === 'bold' ? [schema.mark(schema.marks.strong)] : []), + ]) + ); if (FormattedTextBox.PasteOnLoad) { const pdfAnchorId = FormattedTextBox.PasteOnLoad.clipboardData?.getData('dash/pdfAnchor'); FormattedTextBox.PasteOnLoad = undefined; @@ -1524,7 +1545,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } else (e.nativeEvent as any).handledByInnerReactInstance = true; if (this.Document.forceActive) e.stopPropagation(); - this.tryUpdateScrollHeight(); // if a doc a fitWidth doc is being viewed in different context (eg freeform & lightbox), then it will have conflicting heights. so when the doc is clicked on, we want to make sure it has the appropriate height for the selected view. + this.tryUpdateScrollHeight(); // if a doc a fitWidth doc is being viewed in different embedContainer (eg freeform & lightbox), then it will have conflicting heights. so when the doc is clicked on, we want to make sure it has the appropriate height for the selected view. if ((e.target as any).tagName === 'AUDIOTAG') { e.preventDefault(); e.stopPropagation(); @@ -1575,7 +1596,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps if (!state.selection.empty && !(state.selection instanceof NodeSelection)) this.setupAnchorMenu(); else if (this.props.isContentActive(true)) { const pcords = editor.posAtCoords({ left: e.clientX, top: e.clientY }); - !this.props.isSelected(true) && editor.dispatch(state.tr.setSelection(new TextSelection(state.doc.resolve(pcords?.pos || 0)))); + // !this.props.isSelected(true) && + editor.dispatch(state.tr.setSelection(new TextSelection(state.doc.resolve(pcords?.pos || 0)))); let target = e.target as any; // hrefs are stored on the dataset of the <a> node that wraps the hyerlink <span> while (target && !target.dataset?.targethrefs) target = target.parentElement; FormattedTextBoxComment.update(this, editor, undefined, target?.dataset?.targethrefs, target?.dataset.linkdoc, target?.dataset.nopreview === 'true'); @@ -1611,7 +1633,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps onFocused = (e: React.FocusEvent): void => { //applyDevTools.applyDevTools(this._editorView); this.ProseRef?.children[0] === e.nativeEvent.target && this._editorView && RichTextMenu.Instance?.updateMenu(this._editorView, undefined, this.props); - this.startUndoTypingBatch(); + e.stopPropagation(); }; onClick = (e: React.MouseEvent): void => { @@ -1686,13 +1708,11 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } } startUndoTypingBatch() { - !this._undoTyping && (this._undoTyping = UndoManager.StartBatch('undoTyping')); + !this._undoTyping && (this._undoTyping = UndoManager.StartBatch('text edits on ' + this.rootDoc.title)); } public endUndoTypingBatch() { - const wasUndoing = this._undoTyping; this._undoTyping?.end(); this._undoTyping = undefined; - return wasUndoing; } @action @@ -1720,13 +1740,13 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const state = this._editorView!.state; const curText = state.doc.textBetween(0, state.doc.content.size, ' \n'); - if (this.layoutDoc.sidebarViewType === 'translation' && !this.fieldKey.includes('translation') && curText.endsWith(' ') && curText !== this._lastText) { + if (this.layoutDoc.sidebar_collectionType === 'translation' && !this.fieldKey.includes('translation') && curText.endsWith(' ') && curText !== this._lastText) { try { translateGoogleApi(curText, { from: 'en', to: 'es' }).then((result1: any) => { setTimeout( () => translateGoogleApi(result1[0], { from: 'es', to: 'en' }).then((result: any) => { - this.dataDoc[this.fieldKey + '-translation'] = result1 + '\r\n\r\n' + result[0]; + this.dataDoc[this.fieldKey + '_translation'] = result1 + '\r\n\r\n' + result[0]; }), 1000 ); @@ -1736,10 +1756,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } this._lastText = curText; } - if (StrCast(this.rootDoc.title).startsWith('@') && !this.dataDoc['title-custom']) { + if (StrCast(this.rootDoc.title).startsWith('@') && !this.dataDoc.title_custom) { UndoManager.RunInBatch(() => { - this.dataDoc['title-custom'] = true; - this.dataDoc.showTitle = 'title'; + this.dataDoc.title_custom = true; + this.dataDoc.layout_showTitle = 'title'; const tr = this._editorView!.state.tr; this._editorView?.dispatch(tr.setSelection(new TextSelection(tr.doc.resolve(0), tr.doc.resolve(StrCast(this.rootDoc.title).length + 2))).deleteSelection()); }, 'titler'); @@ -1802,7 +1822,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps if (!LinkDocPreview.LinkInfo && this._scrollRef.current) { if (!this.props.dontSelectOnLoad) { this._ignoreScroll = true; - this.layoutDoc._scrollTop = this._scrollRef.current.scrollTop; + this.layoutDoc._layout_scrollTop = this._scrollRef.current.scrollTop; this._ignoreScroll = false; e.stopPropagation(); e.preventDefault(); @@ -1813,13 +1833,16 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps const margins = 2 * NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0); const children = this.ProseRef?.children.length ? Array.from(this.ProseRef.children[0].children) : undefined; if (children) { - const proseHeight = !this.ProseRef - ? 0 - : children.reduce((p, child) => p + Number(getComputedStyle(child).height.replace('px', '')) + Number(getComputedStyle(child).marginTop.replace('px', '')) + Number(getComputedStyle(child).marginBottom.replace('px', '')), margins); - const scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.docMaxAutoHeight, proseHeight), proseHeight); + const toNum = (val: string) => Number(val.replace('px', '').replace('auto', '0')); + const toHgt = (node: Element) => { + const { height, marginTop, marginBottom } = getComputedStyle(node); + return toNum(height) + Math.max(0, toNum(marginTop)) + Math.max(0, toNum(marginBottom)); + }; + const proseHeight = !this.ProseRef ? 0 : children.reduce((p, child) => p + toHgt(child), margins); + const scrollHeight = this.ProseRef && Math.min(NumCast(this.layoutDoc.layout_maxAutoHeight, proseHeight), proseHeight); if (this.props.setHeight && scrollHeight && !this.props.dontRegisterView) { // if top === 0, then the text box is growing upward (as the overlay caption) which doesn't contribute to the height computation - const setScrollHeight = () => (this.rootDoc[this.fieldKey + '-scrollHeight'] = scrollHeight); + const setScrollHeight = () => (this.rootDoc[this.fieldKey + '_scrollHeight'] = scrollHeight); if (this.rootDoc === this.layoutDoc || this.layoutDoc.resolvedDataDoc) { setScrollHeight(); } else { @@ -1828,21 +1851,21 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps } } }; - fitContentsToBox = () => BoolCast(this.props.Document._fitContentsToBox); - sidebarContentScaling = () => (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1); + fitContentsToBox = () => BoolCast(this.props.Document._freeform_fitContentsToBox); + sidebarContentScaling = () => (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1); sidebarAddDocument = (doc: Doc | Doc[], sidebarKey: string = this.SidebarKey) => { - if (!this.layoutDoc._showSidebar) this.toggleSidebar(); + if (!this.layoutDoc._layout_showSidebar) this.toggleSidebar(); return this.addDocument(doc, sidebarKey); }; sidebarMoveDocument = (doc: Doc | Doc[], targetCollection: Doc | undefined, addDocument: (doc: Doc | Doc[]) => boolean) => this.moveDocument(doc, targetCollection, addDocument, this.SidebarKey); sidebarRemDocument = (doc: Doc | Doc[]) => this.removeDocument(doc, this.SidebarKey); - setSidebarHeight = (height: number) => (this.rootDoc[this.SidebarKey + '-height'] = height); - sidebarWidth = () => (Number(this.sidebarWidthPercent.substring(0, this.sidebarWidthPercent.length - 1)) / 100) * this.props.PanelWidth(); + setSidebarHeight = (height: number) => (this.rootDoc[this.SidebarKey + '_height'] = height); + sidebarWidth = () => (Number(this.layout_sidebarWidthPercent.substring(0, this.layout_sidebarWidthPercent.length - 1)) / 100) * this.props.PanelWidth(); sidebarScreenToLocal = () => this.props .ScreenToLocalTransform() .translate(-(this.props.PanelWidth() - this.sidebarWidth()) / (this.props.NativeDimScaling?.() || 1), 0) - .scale(1 / NumCast(this.layoutDoc._viewScale, 1) / (this.props.NativeDimScaling?.() || 1)); + .scale(1 / NumCast(this.layoutDoc._freeform_scale, 1) / (this.props.NativeDimScaling?.() || 1)); @computed get audioHandle() { return !this._recording ? null : ( @@ -1876,7 +1899,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps color, opacity: annotated ? 1 : undefined, }}> - <FontAwesomeIcon icon={'comment-alt'} /> + <FontAwesomeIcon icon="comment-alt" /> </div> ); } @@ -1928,25 +1951,25 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps fitContentsToBox={this.fitContentsToBox} noSidebar={true} treeViewHideTitle={true} - fieldKey={this.layoutDoc.sidebarViewType === 'translation' ? `${this.fieldKey}-translation` : `${this.fieldKey}-sidebar`} + fieldKey={this.layoutDoc.sidebar_collectionType === 'translation' ? `${this.fieldKey}_translation` : `${this.fieldKey}_sidebar`} /> </div> ); }; return ( - <div className={'formattedTextBox-sidebar' + (Doc.ActiveTool !== InkTool.None ? '-inking' : '')} style={{ width: `${this.sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> - {renderComponent(StrCast(this.layoutDoc.sidebarViewType))} + <div className={'formattedTextBox-sidebar' + (Doc.ActiveTool !== InkTool.None ? '-inking' : '')} style={{ width: `${this.layout_sidebarWidthPercent}`, backgroundColor: `${this.sidebarColor}` }}> + {renderComponent(StrCast(this.layoutDoc.sidebar_collectionType))} </div> ); } cycleAlternateText = () => { - if (this.layoutDoc._showAltContentUI) { - const usePath = this.rootDoc[`${this.props.fieldKey}-usePath`]; - this.rootDoc[`_${this.props.fieldKey}-usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : undefined; + if (this.layoutDoc._layout_enableAltContentUI) { + const usePath = this.rootDoc[`${this.props.fieldKey}_usePath`]; + this.rootDoc[`_${this.props.fieldKey}_usePath`] = usePath === undefined ? 'alternate' : usePath === 'alternate' ? 'alternate:hover' : undefined; } }; @computed get overlayAlternateIcon() { - const usePath = this.rootDoc[`${this.props.fieldKey}-usePath`]; + const usePath = this.rootDoc[`${this.props.fieldKey}_usePath`]; return ( <Tooltip title={ @@ -1978,41 +2001,42 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps ); } @computed get fieldKey() { - const usePath = StrCast(this.rootDoc[`${this.props.fieldKey}-usePath`]); - return this.props.fieldKey + (usePath && (!usePath.includes(':hover') || this._isHovering) ? `-${usePath.replace(':hover', '')}` : ''); + const usePath = StrCast(this.rootDoc[`${this.props.fieldKey}_usePath`]); + return this.props.fieldKey + (usePath && (!usePath.includes(':hover') || this._isHovering) ? `_${usePath.replace(':hover', '')}` : ''); } @observable _isHovering = false; + onPassiveWheel = (e: WheelEvent) => { + // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this) + if (this.props.isContentActive() && !this.props.allowScroll) { + if (!NumCast(this.layoutDoc._layout_scrollTop) && e.deltaY <= 0) e.preventDefault(); + e.stopPropagation(); + } + }; + _oldWheel: any; render() { TraceMobx(); const active = this.props.isContentActive() || this.props.isSelected(); const selected = active; - const scale = (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._viewScale, 1); - const rounded = StrCast(this.layoutDoc.borderRounding) === '100%' ? '-rounded' : ''; + const scale = (this.props.NativeDimScaling?.() || 1) * NumCast(this.layoutDoc._freeform_scale, 1); + const rounded = StrCast(this.layoutDoc.layout_borderRounding) === '100%' ? '-rounded' : ''; const interactive = (Doc.ActiveTool === InkTool.None || SnappingManager.GetIsDragging()) && (this.layoutDoc.z || !this.layoutDoc._lockedPosition); if (!selected && FormattedTextBoxComment.textBox === this) setTimeout(FormattedTextBoxComment.Hide); const minimal = this.props.ignoreAutoHeight; const paddingX = NumCast(this.layoutDoc._xMargin, this.props.xPadding || 0); const paddingY = NumCast(this.layoutDoc._yMargin, this.props.yPadding || 0); - const selPad = (selected && !this.layoutDoc._singleLine) || minimal ? Math.min(paddingY, Math.min(paddingX, 10)) : 0; - const selPaddingClass = selected && !this.layoutDoc._singleLine && paddingY >= 10 ? '-selected' : ''; + const selPad = (selected && !this.layoutDoc._createDocOnCR) || minimal ? Math.min(paddingY, Math.min(paddingX, 10)) : 0; + const selPaddingClass = selected && !this.layoutDoc._createDocOnCR && paddingY >= 10 ? '-selected' : ''; const styleFromLayoutString = Doc.styleFromLayoutString(this.rootDoc, this.layoutDoc, this.props, scale); // this converts any expressions in the format string to style props. e.g., <FormattedTextBox height='{this._headerHeight}px' > return styleFromLayoutString?.height === '0px' ? null : ( <div className="formattedTextBox" onPointerEnter={action(() => (this._isHovering = true))} onPointerLeave={action(() => (this._isHovering = false))} - ref={r => - r?.addEventListener( - 'wheel', // if scrollTop is 0, then don't let wheel trigger scroll on any container (which it would since onScroll won't be triggered on this) - (e: WheelEvent) => { - if (this.props.isContentActive()) { - if (!NumCast(this.layoutDoc._scrollTop) && e.deltaY <= 0) e.preventDefault(); - e.stopPropagation(); - } - }, - { passive: false } - ) - } + ref={r => { + this._oldWheel?.removeEventListener('wheel', this.onPassiveWheel); + this._oldWheel = r; + r?.addEventListener('wheel', this.onPassiveWheel, { passive: false }); + }} style={{ ...(this.props.dontScale ? {} @@ -2024,7 +2048,7 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }), display: !SnappingManager.GetIsDragging() && this.props.thumbShown?.() ? 'none' : undefined, transition: 'inherit', - // overflowY: this.layoutDoc._autoHeight ? "hidden" : undefined, + // overflowY: this.layoutDoc._layout_autoHeight ? "hidden" : undefined, color: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.Color), fontSize: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontSize), fontFamily: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.FontFamily), @@ -2035,8 +2059,8 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps className="formattedTextBox-cont" ref={this._ref} style={{ - overflow: this.autoHeight && this.props.CollectionFreeFormDocumentView?.() ? 'hidden' : undefined, //x this breaks viewing an autoHeight doc in its own tab, or in the lightbox - height: this.props.height || (this.autoHeight && this.props.renderDepth && !this.props.suppressSetHeight ? 'max-content' : undefined), + overflow: this.layout_autoHeight && this.props.CollectionFreeFormDocumentView?.() ? 'hidden' : undefined, //x this breaks viewing an layout_autoHeight doc in its own tab, or in the lightbox + height: this.props.height || (this.layout_autoHeight && this.props.renderDepth && !this.props.suppressSetHeight ? 'max-content' : undefined), pointerEvents: interactive ? undefined : 'none', }} onContextMenu={this.specificContextMenu} @@ -2052,9 +2076,9 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps className={`formattedTextBox-outer${selected ? '-selected' : ''}`} ref={this._scrollRef} style={{ - width: this.props.dontSelectOnLoad ? '100%' : `calc(100% - ${this.sidebarWidthPercent})`, + width: this.props.dontSelectOnLoad ? '100%' : `calc(100% - ${this.layout_sidebarWidthPercent})`, pointerEvents: !active && !SnappingManager.GetIsDragging() ? 'none' : undefined, - overflow: this.layoutDoc._singleLine ? 'hidden' : this.layoutDoc._autoHeight ? 'visible' : undefined, + overflow: this.layoutDoc._createDocOnCR ? 'hidden' : this.layoutDoc._layout_autoHeight ? 'visible' : undefined, }} onScroll={this.onScroll} onDrop={this.ondrop}> @@ -2071,10 +2095,10 @@ export class FormattedTextBox extends ViewBoxAnnotatableComponent<FieldViewProps }} /> </div> - {this.noSidebar || this.props.dontSelectOnLoad || !this.SidebarShown || this.sidebarWidthPercent === '0%' ? null : this.sidebarCollection} - {this.noSidebar || this.Document._noSidebar || this.props.dontSelectOnLoad || this.Document._singleLine ? null : this.sidebarHandle} + {this.noSidebar || this.props.dontSelectOnLoad || !this.SidebarShown || this.layout_sidebarWidthPercent === '0%' ? null : this.sidebarCollection} + {this.noSidebar || this.Document._layout_noSidebar || this.props.dontSelectOnLoad || this.Document._createDocOnCR ? null : this.sidebarHandle} {this.audioHandle} - {this.layoutDoc._showAltContentUI ? this.overlayAlternateIcon : null} + {this.layoutDoc._layout_enableAltContentUI ? this.overlayAlternateIcon : null} </div> </div> ); diff --git a/src/client/views/nodes/formattedText/RichTextMenu.tsx b/src/client/views/nodes/formattedText/RichTextMenu.tsx index f0caa1f4f..7c3e4baad 100644 --- a/src/client/views/nodes/formattedText/RichTextMenu.tsx +++ b/src/client/views/nodes/formattedText/RichTextMenu.tsx @@ -138,7 +138,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this.activeListType = this.getActiveListStyle(); this._activeAlignment = this.getActiveAlignment(); - this._activeFontFamily = !activeFamilies.length ? StrCast(this.TextView?.Document.fontFamily, StrCast(Doc.UserDoc().fontFamily, 'Arial')) : activeFamilies.length === 1 ? String(activeFamilies[0]) : 'various'; + this._activeFontFamily = !activeFamilies.length ? StrCast(this.TextView?.Document._text_fontFamily, StrCast(Doc.UserDoc().fontFamily, 'Arial')) : activeFamilies.length === 1 ? String(activeFamilies[0]) : 'various'; this._activeFontSize = !activeSizes.length ? StrCast(this.TextView?.Document.fontSize, StrCast(Doc.UserDoc().fontSize, '10px')) : activeSizes[0]; this._activeFontColor = !activeColors.length ? StrCast(this.TextView?.Document.fontColor, StrCast(Doc.UserDoc().fontColor, 'black')) : activeColors.length > 0 ? String(activeColors[0]) : '...'; this._activeHighlightColor = !activeHighlights.length ? '' : activeHighlights.length > 0 ? String(activeHighlights[0]) : '...'; @@ -221,7 +221,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { m.type === state.schema.marks.marker && activeHighlights.add(String(m.attrs.highlight)); }); } else if (SelectionManager.Views().some(dv => dv.ComponentView instanceof EquationBox)) { - SelectionManager.Views().forEach(dv => StrCast(dv.rootDoc._fontSize) && activeSizes.add(StrCast(dv.rootDoc._fontSize))); + SelectionManager.Views().forEach(dv => StrCast(dv.rootDoc._text_fontSize) && activeSizes.add(StrCast(dv.rootDoc._text_fontSize))); } return { activeFamilies: Array.from(activeFamilies), activeSizes: Array.from(activeSizes), activeColors: Array.from(activeColors), activeHighlights: Array.from(activeHighlights) }; } @@ -345,8 +345,8 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { this.view.focus(); } } else if (SelectionManager.Views().some(dv => dv.ComponentView instanceof EquationBox)) { - SelectionManager.Views().forEach(dv => (dv.rootDoc._fontSize = fontSize)); - } else Doc.UserDoc()._fontSize = fontSize; + SelectionManager.Views().forEach(dv => (dv.rootDoc._text_fontSize = fontSize)); + } else Doc.UserDoc().fontSize = fontSize; this.updateMenu(this.view, undefined, this.props); }; @@ -355,7 +355,7 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { const fmark = this.view.state.schema.marks.pFontFamily.create({ family: family }); this.setMark(fmark, this.view.state, (tx: any) => this.view!.dispatch(tx.addStoredMark(fmark)), true); this.view.focus(); - } else Doc.UserDoc()._fontFamily = family; + } else Doc.UserDoc().fontFamily = family; this.updateMenu(this.view, undefined, this.props); }; @@ -623,15 +623,15 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { if (linkclicked) { const linkDoc = await DocServer.GetRefField(linkclicked); if (linkDoc instanceof Doc) { - const anchor1 = await Cast(linkDoc.anchor1, Doc); - const anchor2 = await Cast(linkDoc.anchor2, Doc); + const link_anchor_1 = await Cast(linkDoc.link_anchor_1, Doc); + const link_anchor_2 = await Cast(linkDoc.link_anchor_2, Doc); const currentDoc = SelectionManager.Docs().lastElement(); - if (currentDoc && anchor1 && anchor2) { - if (Doc.AreProtosEqual(currentDoc, anchor1)) { - return StrCast(anchor2.title); + if (currentDoc && link_anchor_1 && link_anchor_2) { + if (Doc.AreProtosEqual(currentDoc, link_anchor_1)) { + return StrCast(link_anchor_2.title); } - if (Doc.AreProtosEqual(currentDoc, anchor2)) { - return StrCast(anchor1.title); + if (Doc.AreProtosEqual(currentDoc, link_anchor_2)) { + return StrCast(link_anchor_1.title); } } } @@ -758,11 +758,11 @@ export class RichTextMenu extends AntimodeMenu<AntimodeMenuProps> { // <div className="collectionMenu-divider" key="divider 3" /> // {[this.createMarksDropdown(this.activeFontSize, this.fontSizeOptions, "font size", action((val: string) => { // this.activeFontSize = val; - // SelectionManager.Views().map(dv => dv.props.Document._fontSize = val); + // SelectionManager.Views().map(dv => dv.props.Document._text_fontSize = val); // })), // this.createMarksDropdown(this.activeFontFamily, this.fontFamilyOptions, "font family", action((val: string) => { // this.activeFontFamily = val; - // SelectionManager.Views().map(dv => dv.props.Document._fontFamily = val); + // SelectionManager.Views().map(dv => dv.props.Document._text_fontFamily = val); // })), // <div className="collectionMenu-divider" key="divider 4" />, // this.createNodesDropdown(this.activeListType, this.listTypeOptions, "list type", () => ({})), diff --git a/src/client/views/nodes/formattedText/RichTextRules.ts b/src/client/views/nodes/formattedText/RichTextRules.ts index cad56b14b..ac1e7ce5d 100644 --- a/src/client/views/nodes/formattedText/RichTextRules.ts +++ b/src/client/views/nodes/formattedText/RichTextRules.ts @@ -1,11 +1,11 @@ import { ellipsis, emDash, InputRule, smartQuotes, textblockTypeInputRule } from 'prosemirror-inputrules'; import { NodeSelection, TextSelection } from 'prosemirror-state'; -import { DataSym, Doc, StrListCast } from '../../../../fields/Doc'; +import { Doc, StrListCast } from '../../../../fields/Doc'; +import { DocData } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { List } from '../../../../fields/List'; import { ComputedField } from '../../../../fields/ScriptField'; -import { NumCast, StrCast } from '../../../../fields/Types'; -import { normalizeEmail } from '../../../../fields/util'; +import { NumCast } from '../../../../fields/Types'; import { Utils } from '../../../../Utils'; import { DocServer } from '../../../DocServer'; import { Docs, DocUtils } from '../../../documents/Documents'; @@ -76,14 +76,23 @@ export class RichTextRules { //Create annotation to a field on the text document new InputRule(new RegExp(/>>$/), (state, match, start, end) => { - const textDoc = this.Document[DataSym]; + const textDoc = this.Document[DocData]; const numInlines = NumCast(textDoc.inlineTextCount); textDoc.inlineTextCount = numInlines + 1; const inlineFieldKey = 'inline' + numInlines; // which field on the text document this annotation will write to const inlineLayoutKey = 'layout_' + inlineFieldKey; // the field holding the layout string that will render the inline annotation - const textDocInline = Docs.Create.TextDocument('', { _layoutKey: inlineLayoutKey, _width: 75, _height: 35, annotationOn: textDoc, _fitWidth: true, _autoHeight: true, _fontSize: '9px', title: 'inline comment' }); + const textDocInline = Docs.Create.TextDocument('', { + _layout_fieldKey: inlineLayoutKey, + _width: 75, + _height: 35, + annotationOn: textDoc, + _layout_fitWidth: true, + _layout_autoHeight: true, + _text_fontSize: '9px', + title: 'inline comment', + }); textDocInline.title = inlineFieldKey; // give the annotation its own title - textDocInline['title-custom'] = true; // And make sure that it's 'custom' so that editing text doesn't change the title of the containing doc + textDocInline.title_custom = true; // And make sure that it's 'custom' so that editing text doesn't change the title of the containing doc textDocInline.isTemplateForField = inlineFieldKey; // this is needed in case the containing text doc is converted to a template at some point textDocInline.proto = textDoc; // make the annotation inherit from the outer text doc so that it can resolve any nested field references, e.g., [[field]] textDocInline._textContext = ComputedField.MakeFunction(`copyField(self.${inlineFieldKey})`); @@ -249,7 +258,7 @@ export class RichTextRules { this.TextBox.EditorView?.dispatch(rstate.tr.setSelection(new TextSelection(rstate.doc.resolve(start), rstate.doc.resolve(end - 3)))); } - DocUtils.MakeLink(this.TextBox.getAnchor(true), target, { linkRelationship: 'portal to:portal from' }); + DocUtils.MakeLink(this.TextBox.getAnchor(true), target, { link_relationship: 'portal to:portal from' }); const fstate = this.TextBox.EditorView?.state; if (fstate && selection) { @@ -268,7 +277,7 @@ export class RichTextRules { } if (value !== '' && value !== undefined) { const num = value.match(/^[0-9.]$/); - this.Document[DataSym][fieldKey] = value === 'true' ? true : value === 'false' ? false : num ? Number(value) : value; + this.Document[DocData][fieldKey] = value === 'true' ? true : value === 'false' ? false : num ? Number(value) : value; } const fieldView = state.schema.nodes.dashField.create({ fieldKey, docId, hideKey: false }); return state.tr.setSelection(new TextSelection(state.doc.resolve(start), state.doc.resolve(end))).replaceSelectionWith(fieldView, true); @@ -299,37 +308,15 @@ export class RichTextRules { return tr.setSelection(new NodeSelection(tr.doc.resolve(tr.selection.$from.pos - 1))); }), - // create an inline view of a document {{ <layoutKey> : <Doc> }} - // {{:Doc}} => show default view of document - // {{<layout>}} => show layout for this doc - // {{<layout> : Doc}} => show layout for another doc - new InputRule(new RegExp(/\{\{([a-zA-Z_ \-0-9]*)(\([a-zA-Z0-9…._/\-]*\))?(:[a-zA-Z_@\.\? \-0-9]+)?\}\}$/), (state, match, start, end) => { - const fieldKey = match[1] || ''; - const fieldParam = match[2]?.replace('…', '...') || ''; - const rawdocid = match[3]?.substring(1); - const docId = rawdocid ? (!rawdocid.includes('@') ? normalizeEmail(Doc.CurrentUserEmail) + '@' + rawdocid : rawdocid) : undefined; - if (!fieldKey && !docId) return state.tr; - docId && - DocServer.GetRefField(docId).then(docx => { - if (!(docx instanceof Doc && docx)) { - Docs.Create.FreeformDocument([], { title: rawdocid, _width: 500, _height: 500 }, docId); - } - }); - const node = (state.doc.resolve(start) as any).nodeAfter; - const dashDoc = schema.nodes.dashDoc.create({ width: 75, height: 75, title: 'dashDoc', docId, fieldKey: fieldKey + fieldParam, float: 'unset', alias: Utils.GenerateGuid() }); - const sm = state.storedMarks || undefined; - return node ? state.tr.replaceRangeWith(start, end, dashDoc).setStoredMarks([...node.marks, ...(sm ? sm : [])]) : state.tr; - }), - // create an inline view of a tag stored under the '#' field new InputRule(new RegExp(/#([a-zA-Z_\-]+[a-zA-Z_\-0-9]*)\s$/), (state, match, start, end) => { const tag = match[1]; if (!tag) return state.tr; - //this.Document[DataSym]['#' + tag] = '#' + tag; - const tags = StrListCast(this.Document[DataSym].tags); + //this.Document[DocData]['#' + tag] = '#' + tag; + const tags = StrListCast(this.Document[DocData].tags); if (!tags.includes(tag)) { tags.push(tag); - this.Document[DataSym].tags = new List<string>(tags); + this.Document[DocData].tags = new List<string>(tags); } const fieldView = state.schema.nodes.dashField.create({ fieldKey: '#' + tag }); return state.tr diff --git a/src/client/views/nodes/formattedText/marks_rts.ts b/src/client/views/nodes/formattedText/marks_rts.ts index 5b47e8a70..7e17008bb 100644 --- a/src/client/views/nodes/formattedText/marks_rts.ts +++ b/src/client/views/nodes/formattedText/marks_rts.ts @@ -46,7 +46,7 @@ export const marks: { [index: string]: MarkSpec } = { toDOM(node: any) { const targethrefs = node.attrs.allAnchors.reduce((p: string, item: { href: string; title: string; anchorId: string }) => (p ? p + ' ' + item.href : item.href), ''); const anchorids = node.attrs.allAnchors.reduce((p: string, item: { href: string; title: string; anchorId: string }) => (p ? p + ' ' + item.anchorId : item.anchorId), ''); - return ['a', { class: anchorids, 'data-targethrefs': targethrefs, 'data-linkdoc': node.attrs.linkDoc, title: node.attrs.title, location: node.attrs.location, style: `background: lightBlue` }, 0]; + return ['a', { class: anchorids, 'data-targethrefs': targethrefs, 'data-noPreview': 'true', 'data-linkdoc': node.attrs.linkDoc, title: node.attrs.title, location: node.attrs.location, style: `background: lightBlue` }, 0]; }, }, noAutoLinkAnchor: { diff --git a/src/client/views/nodes/formattedText/nodes_rts.ts b/src/client/views/nodes/formattedText/nodes_rts.ts index 6c9d5d31a..f27fb18e2 100644 --- a/src/client/views/nodes/formattedText/nodes_rts.ts +++ b/src/client/views/nodes/formattedText/nodes_rts.ts @@ -247,7 +247,7 @@ export const nodes: { [index: string]: NodeSpec } = { hidden: { default: false }, // whether dashComment node has toggle the dashDoc's display off fieldKey: { default: '' }, docId: { default: '' }, - alias: { default: '' }, + embedding: { default: '' }, }, group: 'inline', draggable: false, diff --git a/src/client/views/nodes/trails/PresBox.scss b/src/client/views/nodes/trails/PresBox.scss index eb91c82f3..bf56b4d9e 100644 --- a/src/client/views/nodes/trails/PresBox.scss +++ b/src/client/views/nodes/trails/PresBox.scss @@ -951,6 +951,7 @@ margin-right: unset; height: 100%; position: relative; + user-select: none; } select { diff --git a/src/client/views/nodes/trails/PresBox.tsx b/src/client/views/nodes/trails/PresBox.tsx index bd2be8f11..505e39d97 100644 --- a/src/client/views/nodes/trails/PresBox.tsx +++ b/src/client/views/nodes/trails/PresBox.tsx @@ -3,9 +3,10 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; import { action, computed, IReactionDisposer, observable, ObservableSet, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; -import { AnimationSym, Doc, DocListCast, Field, FieldResult, Opt, StrListCast } from '../../../../fields/Doc'; +import { Doc, DocListCast, Field, FieldResult, Opt, StrListCast } from '../../../../fields/Doc'; +import { Animation } from '../../../../fields/DocSymbols'; import { Copy, Id } from '../../../../fields/FieldSymbols'; -import { InkField, InkTool } from '../../../../fields/InkField'; +import { InkField } from '../../../../fields/InkField'; import { List } from '../../../../fields/List'; import { ObjectField } from '../../../../fields/ObjectField'; import { listSpec } from '../../../../fields/Schema'; @@ -41,7 +42,7 @@ export interface pinDataTypes { scrollable?: boolean; dataviz?: number[]; pannable?: boolean; - viewType?: boolean; + type_collection?: boolean; inkable?: boolean; filters?: boolean; pivot?: boolean; @@ -103,10 +104,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @observable _presKeyEvents: boolean = false; @observable _forceKeyEvents: boolean = false; @computed get isTreeOrStack() { - return [CollectionViewType.Tree, CollectionViewType.Stacking].includes(StrCast(this.layoutDoc._viewType) as any); + return [CollectionViewType.Tree, CollectionViewType.Stacking].includes(StrCast(this.layoutDoc._type_collection) as any); } @computed get isTree() { - return this.layoutDoc._viewType === CollectionViewType.Tree; + return this.layoutDoc._type_collection === CollectionViewType.Tree; } @computed get presFieldKey() { return StrCast(this.layoutDoc.presFieldKey, 'data'); @@ -128,14 +129,14 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } public static targetRenderedDoc = (doc: Doc) => { const targetDoc = Cast(doc?.presentationTargetDoc, Doc, null); - return targetDoc?.unrendered ? DocCast(targetDoc.annotationOn) : targetDoc; + return targetDoc?.layout_unrendered ? DocCast(targetDoc.annotationOn) : targetDoc; }; @computed get scrollable() { - if ([DocumentType.PDF, DocumentType.WEB, DocumentType.RTF].includes(this.targetDoc.type as DocumentType) || this.targetDoc._viewType === CollectionViewType.Stacking) return true; + if ([DocumentType.PDF, DocumentType.WEB, DocumentType.RTF].includes(this.targetDoc.type as DocumentType) || this.targetDoc._type_collection === CollectionViewType.Stacking) return true; return false; } @computed get panable() { - if ((this.targetDoc.type === DocumentType.COL && this.targetDoc._viewType === CollectionViewType.Freeform) || this.targetDoc.type === DocumentType.IMG) return true; + if ((this.targetDoc.type === DocumentType.COL && this.targetDoc._type_collection === CollectionViewType.Freeform) || this.targetDoc.type === DocumentType.IMG) return true; return false; } @computed get selectedDocumentView() { @@ -196,7 +197,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.turnOffEdit(true); this._disposers.selection = reaction( () => SelectionManager.Views(), - views => views.some(view => view.props.Document === this.rootDoc) && this.updateCurrentPresentation() + views => (!PresBox.Instance || views.some(view => view.props.Document === this.rootDoc)) && this.updateCurrentPresentation(), + { fireImmediately: true } ); this._disposers.editing = reaction( () => this.layoutDoc.presStatus === PresStatus.Edit, @@ -273,8 +275,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { progressivizedItems = (doc: Doc) => { const targetList = PresBox.targetRenderedDoc(doc); if (doc.presIndexed !== undefined && targetList) { - const listItems = (Cast(targetList[Doc.LayoutFieldKey(targetList)], listSpec(Doc), null)?.filter(d => d instanceof Doc) as Doc[]) ?? DocListCast(targetList[Doc.LayoutFieldKey(targetList) + '-annotations']); - return listItems.filter(doc => !doc.unrendered); + const listItems = (Cast(targetList[Doc.LayoutFieldKey(targetList)], listSpec(Doc), null)?.filter(d => d instanceof Doc) as Doc[]) ?? DocListCast(targetList[Doc.LayoutFieldKey(targetList) + '_annotations']); + return listItems.filter(doc => !doc.layout_unrendered); } }; // Called when the user activates 'next' - to move to the next part of the pres. trail @@ -330,7 +332,6 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @action back = () => { const activeItem: Doc = this.activeItem; - const prevItem = Cast(this.childDocs[Math.max(0, this.itemIndex - 1)], Doc, null); let prevSelected = this.itemIndex; // Functionality for group with up let didZoom = activeItem.presMovement; @@ -376,25 +377,25 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { static pinDataTypes(target?: Doc): pinDataTypes { const targetType = target?.type as any; const inkable = [DocumentType.INK].includes(targetType); - const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(targetType) || target?._viewType === CollectionViewType.Stacking; - const pannable = [DocumentType.IMG, DocumentType.PDF].includes(targetType) || (targetType === DocumentType.COL && target?._viewType === CollectionViewType.Freeform); + const scrollable = [DocumentType.PDF, DocumentType.RTF, DocumentType.WEB].includes(targetType) || target?._type_collection === CollectionViewType.Stacking; + const pannable = [DocumentType.IMG, DocumentType.PDF].includes(targetType) || (targetType === DocumentType.COL && target?._type_collection === CollectionViewType.Freeform); const temporal = [DocumentType.AUDIO, DocumentType.VID].includes(targetType); const clippable = [DocumentType.COMPARISON].includes(targetType); const datarange = [DocumentType.FUNCPLOT].includes(targetType); const dataview = [DocumentType.INK, DocumentType.COL, DocumentType.IMG, DocumentType.RTF].includes(targetType) && target?.activeFrame === undefined; const poslayoutview = [DocumentType.COL].includes(targetType) && target?.activeFrame === undefined; - const viewType = targetType === DocumentType.COL; + const type_collection = targetType === DocumentType.COL; const filters = true; const pivot = true; const dataannos = false; - return { scrollable, pannable, inkable, viewType, pivot, filters, temporal, clippable, dataview, datarange, poslayoutview, dataannos }; + return { scrollable, pannable, inkable, type_collection, pivot, filters, temporal, clippable, dataview, datarange, poslayoutview, dataannos }; } @action playAnnotation = (anno: AudioField) => {}; @action static restoreTargetDocView(bestTargetView: Opt<DocumentView>, activeItem: Doc, transTime: number, pinDocLayout: boolean = BoolCast(activeItem.presPinLayout), pinDataTypes?: pinDataTypes, targetDoc?: Doc) { - const bestTarget = bestTargetView?.rootDoc ?? (targetDoc?.unrendered ? DocCast(targetDoc?.annotationOn) : targetDoc); + const bestTarget = bestTargetView?.rootDoc ?? (targetDoc?.layout_unrendered ? DocCast(targetDoc?.annotationOn) : targetDoc); if (!bestTarget || activeItem === bestTarget) return; let changed = false; if (pinDocLayout) { @@ -419,7 +420,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const activeFrame = activeItem.presActiveFrame ?? activeItem.presCurrentFrame; if (activeFrame !== undefined) { const transTime = NumCast(activeItem.presTransition, 500); - const acontext = activeItem.presActiveFrame !== undefined ? DocCast(DocCast(activeItem.presentationTargetDoc).context) : DocCast(activeItem.presentationTargetDoc); + const acontext = activeItem.presActiveFrame !== undefined ? DocCast(DocCast(activeItem.presentationTargetDoc).embedContainer) : DocCast(activeItem.presentationTargetDoc); const context = DocCast(acontext)?.annotationOn ? DocCast(DocCast(acontext).annotationOn) : acontext; if (context) { const ffview = DocumentManager.Instance.getFirstDocumentView(context)?.props.CollectionFreeFormDocumentView?.().props.CollectionFreeFormView; @@ -429,6 +430,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } } } + if ((pinDataTypes?.dataview && activeItem.presData !== undefined) || (!pinDataTypes && activeItem.presData !== undefined)) { + bestTarget._dataTransition = `all ${transTime}ms`; + const fkey = Doc.LayoutFieldKey(bestTarget); + const setData = bestTargetView?.ComponentView?.setData; + if (setData) setData(activeItem.presData); + else Doc.GetProto(bestTarget)[fkey] = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData; + bestTarget[fkey + '_usePath'] = activeItem.presUsePath; + setTimeout(() => (bestTarget._dataTransition = undefined), transTime + 10); + } if (pinDataTypes?.datarange || (!pinDataTypes && activeItem.presXRange !== undefined)) { if (bestTarget.xRange !== activeItem.presXRange) { bestTarget.xRange = (activeItem.presXRange as ObjectField)?.[Copy](); @@ -440,14 +450,15 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } } if (pinDataTypes?.clippable || (!pinDataTypes && activeItem.presClipWidth !== undefined)) { - if (bestTarget._clipWidth !== activeItem.presClipWidth) { - bestTarget._clipWidth = activeItem.presClipWidth; + const fkey = '_' + Doc.LayoutFieldKey(bestTarget); + if (bestTarget[fkey + '_clipWidth'] !== activeItem.presClipWidth) { + bestTarget[fkey + '_clipWidth'] = activeItem.presClipWidth; changed = true; } } if (pinDataTypes?.temporal || (!pinDataTypes && activeItem.presStartTime !== undefined)) { - if (bestTarget._currentTimecode !== activeItem.presStartTime) { - bestTarget._currentTimecode = activeItem.presStartTime; + if (bestTarget._layout_currentTimecode !== activeItem.presStartTime) { + bestTarget._layout_currentTimecode = activeItem.presStartTime; changed = true; } } @@ -469,16 +480,16 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { changed = true; } } - if ((pinDataTypes?.viewType && activeItem.presViewType !== undefined) || (!pinDataTypes && activeItem.presViewType !== undefined)) { - if (bestTarget._viewType !== activeItem.presViewType) { - bestTarget._viewType = activeItem.presViewType; + if ((pinDataTypes?.type_collection && activeItem.presViewType !== undefined) || (!pinDataTypes && activeItem.presViewType !== undefined)) { + if (bestTarget._type_collection !== activeItem.presViewType) { + bestTarget._type_collection = activeItem.presViewType; changed = true; } } if ((pinDataTypes?.filters && activeItem.presDocFilters !== undefined) || (!pinDataTypes && activeItem.presDocFilters !== undefined)) { - if (bestTarget.docFilters !== activeItem.presDocFilters) { - bestTarget.docFilters = ObjectField.MakeCopy(activeItem.presDocFilters as ObjectField) || new List<string>([]); + if (bestTarget.childFilters !== activeItem.presDocFilters) { + bestTarget.childFilters = ObjectField.MakeCopy(activeItem.presDocFilters as ObjectField) || new List<string>([]); changed = true; } } @@ -495,8 +506,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } if (pinDataTypes?.scrollable || (!pinDataTypes && activeItem.presViewScroll !== undefined)) { - if (bestTarget._scrollTop !== activeItem.presViewScroll) { - bestTarget._scrollTop = activeItem.presViewScroll; + if (bestTarget._layout_scrollTop !== activeItem.presViewScroll) { + bestTarget._layout_scrollTop = activeItem.presViewScroll; changed = true; const contentBounds = Cast(activeItem.presPinViewBounds, listSpec('number')); if (contentBounds) { @@ -507,26 +518,19 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } if (pinDataTypes?.dataannos || (!pinDataTypes && activeItem.presAnnotations !== undefined)) { const fkey = Doc.LayoutFieldKey(bestTarget); - const oldItems = DocListCast(bestTarget[fkey + '-annotations']).filter(doc => doc.unrendered); + const oldItems = DocListCast(bestTarget[fkey + '_annotations']).filter(doc => doc.layout_unrendered); const newItems = DocListCast(activeItem.presAnnotations).map(doc => { doc.hidden = false; return doc; }); - const hiddenItems = DocListCast(bestTarget[fkey + '-annotations']) - .filter(doc => !doc.unrendered && !newItems.includes(doc)) + const hiddenItems = DocListCast(bestTarget[fkey + '_annotations']) + .filter(doc => !doc.layout_unrendered && !newItems.includes(doc)) .map(doc => { doc.hidden = true; return doc; }); const newList = new List<Doc>([...oldItems, ...hiddenItems, ...newItems]); - Doc.GetProto(bestTarget)[fkey + '-annotations'] = newList; - } - if ((pinDataTypes?.dataview && activeItem.presData !== undefined) || (!pinDataTypes && activeItem.presData !== undefined)) { - bestTarget._dataTransition = `all ${transTime}ms`; - const fkey = Doc.LayoutFieldKey(bestTarget); - Doc.GetProto(bestTarget)[fkey] = activeItem.presData instanceof ObjectField ? activeItem.presData[Copy]() : activeItem.presData; - bestTarget[fkey + '-usePath'] = activeItem.presUsePath; - setTimeout(() => (bestTarget._dataTransition = undefined), transTime + 10); + Doc.GetProto(bestTarget)[fkey + '_annotations'] = newList; } if (pinDataTypes?.poslayoutview || (!pinDataTypes && activeItem.presPinLayoutData !== undefined)) { changed = true; @@ -558,20 +562,20 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const contentBounds = Cast(activeItem.presPinViewBounds, listSpec('number')); if (contentBounds) { const viewport = { panX: (contentBounds[0] + contentBounds[2]) / 2, panY: (contentBounds[1] + contentBounds[3]) / 2, width: contentBounds[2] - contentBounds[0], height: contentBounds[3] - contentBounds[1] }; - bestTarget._panX = viewport.panX; - bestTarget._panY = viewport.panY; + bestTarget._freeform_panX = viewport.panX; + bestTarget._freeform_panY = viewport.panY; const dv = DocumentManager.Instance.getDocumentView(bestTarget); if (dv) { changed = true; const computedScale = NumCast(activeItem.presZoom, 1) * Math.min(dv.props.PanelWidth() / viewport.width, dv.props.PanelHeight() / viewport.height); - activeItem.presMovement === PresMovement.Zoom && (bestTarget._viewScale = computedScale); + activeItem.presMovement === PresMovement.Zoom && (bestTarget._freeform_scale = computedScale); dv.ComponentView?.brushView?.(viewport); } } else { - if (bestTarget._panX !== activeItem.presPanX || bestTarget._panY !== activeItem.presPanY || bestTarget._viewScale !== activeItem.presViewScale) { - bestTarget._panX = activeItem.presPanX ?? bestTarget._panX; - bestTarget._panY = activeItem.presPanY ?? bestTarget._panY; - bestTarget._viewScale = activeItem.presViewScale ?? bestTarget._viewScale; + if (bestTarget._freeform_panX !== activeItem.presPanX || bestTarget._freeform_panY !== activeItem.presPanY || bestTarget._freeform_scale !== activeItem.presViewScale) { + bestTarget._freeform_panX = activeItem.presPanX ?? bestTarget._freeform_panX; + bestTarget._freeform_panY = activeItem.presPanY ?? bestTarget._freeform_panY; + bestTarget._freeform_scale = activeItem.presViewScale ?? bestTarget._freeform_scale; changed = true; } } @@ -600,7 +604,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { pinProps.pinData.scrollable || pinProps.pinData.temporal || pinProps.pinData.pannable || - pinProps.pinData.viewType || + pinProps.pinData.type_collection || pinProps.pinData.clippable || pinProps.pinData.datarange || pinProps.pinData.dataview || @@ -608,12 +612,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { pinProps?.activeFrame !== undefined; const fkey = Doc.LayoutFieldKey(targetDoc); if (pinProps.pinData.dataview) { - pinDoc.presUsePath = targetDoc[fkey + '-usePath']; + pinDoc.presUsePath = targetDoc[fkey + '_usePath']; pinDoc.presData = targetDoc[fkey] instanceof ObjectField ? (targetDoc[fkey] as ObjectField)[Copy]() : targetDoc.data; } if (pinProps.pinData.dataannos) { const fkey = Doc.LayoutFieldKey(targetDoc); - pinDoc.presAnnotations = new List<Doc>(DocListCast(Doc.GetProto(targetDoc)[fkey + '-annotations']).filter(doc => !doc.unrendered)); + pinDoc.presAnnotations = new List<Doc>(DocListCast(Doc.GetProto(targetDoc)[fkey + '_annotations']).filter(doc => !doc.layout_unrendered)); } if (pinProps.pinData.inkable) { pinDoc.presFillColor = targetDoc.fillColor; @@ -621,8 +625,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { pinDoc.presWidth = targetDoc._width; pinDoc.presHeight = targetDoc._height; } - if (pinProps.pinData.scrollable) pinDoc.presViewScroll = targetDoc._scrollTop; - if (pinProps.pinData.clippable) pinDoc.presClipWidth = targetDoc._clipWidth; + if (pinProps.pinData.scrollable) pinDoc.presViewScroll = targetDoc._layout_scrollTop; + if (pinProps.pinData.clippable) { + const fkey = Doc.LayoutFieldKey(targetDoc); + pinDoc.presClipWidth = targetDoc[fkey + '_clipWidth']; + } if (pinProps.pinData.datarange) { pinDoc.presXRange = undefined; //targetDoc?.xrange; pinDoc.presYRange = undefined; //targetDoc?.yrange; @@ -643,17 +650,17 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { }) ) ); - if (pinProps.pinData.viewType) pinDoc.presViewType = targetDoc._viewType; - if (pinProps.pinData.filters) pinDoc.presDocFilters = ObjectField.MakeCopy(targetDoc.docFilters as ObjectField); + if (pinProps.pinData.type_collection) pinDoc.presViewType = targetDoc._type_collection; + if (pinProps.pinData.filters) pinDoc.presDocFilters = ObjectField.MakeCopy(targetDoc.childFilters as ObjectField); if (pinProps.pinData.pivot) pinDoc.presPivotField = targetDoc._pivotField; if (pinProps.pinData.pannable) { - pinDoc.presPanX = NumCast(targetDoc._panX); - pinDoc.presPanY = NumCast(targetDoc._panY); - pinDoc.presViewScale = NumCast(targetDoc._viewScale, 1); + pinDoc.presPanX = NumCast(targetDoc._freeform_panX); + pinDoc.presPanY = NumCast(targetDoc._freeform_panY); + pinDoc.presViewScale = NumCast(targetDoc._freeform_scale, 1); } if (pinProps.pinData.temporal) { - pinDoc.presStartTime = targetDoc._currentTimecode; - const duration = NumCast(pinDoc[`${Doc.LayoutFieldKey(pinDoc)}-duration`], NumCast(targetDoc.presStartTime) + 0.1); + pinDoc.presStartTime = targetDoc._layout_currentTimecode; + const duration = NumCast(pinDoc[`${Doc.LayoutFieldKey(pinDoc)}_duration`], NumCast(targetDoc.presStartTime) + 0.1); pinDoc.presEndTime = NumCast(targetDoc.clipEnd, duration); } } @@ -661,7 +668,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { // If pinWithView option set then update scale and x / y props of slide const bounds = pinProps.pinViewport; pinDoc.presPinView = true; - pinDoc.presViewScale = NumCast(targetDoc._viewScale, 1); + pinDoc.presViewScale = NumCast(targetDoc._freeform_scale, 1); pinDoc.presPanX = bounds.left + bounds.width / 2; pinDoc.presPanY = bounds.top + bounds.height / 2; pinDoc.presPinViewBounds = new List<number>([bounds.left, bounds.top, bounds.left + bounds.width, bounds.top + bounds.height]); @@ -681,7 +688,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const finished = () => { afterNav?.(); console.log('Finish Slide Nav: ' + targetDoc.title); - targetDoc[AnimationSym] = undefined; + targetDoc[Animation] = undefined; }; const selViewCache = Array.from(this.selectedArray); const dragViewCache = Array.from(this._dragArray); @@ -727,7 +734,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } } if (targetDoc) { - if (activeItem.presentationTargetDoc instanceof Doc) activeItem.presentationTargetDoc[AnimationSym] = undefined; + if (activeItem.presentationTargetDoc instanceof Doc) activeItem.presentationTargetDoc[Animation] = undefined; DocumentManager.Instance.AddViewRenderedCb(LightboxView.LightboxDoc, dv => { // if target or the doc it annotates is not in the lightbox, then close the lightbox @@ -791,7 +798,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const savedStates = docs.map(doc => { switch (doc.type) { case DocumentType.COL: - if (doc._viewType === CollectionViewType.Freeform) return { type: CollectionViewType.Freeform, doc, x: NumCast(doc.panX), y: NumCast(doc.panY), s: NumCast(doc.viewScale) }; + if (doc._type_collection === CollectionViewType.Freeform) return { type: CollectionViewType.Freeform, doc, x: NumCast(doc.freeform_panX), y: NumCast(doc.freeform_panY), s: NumCast(doc.freeform_scale) }; break; case DocumentType.INK: if (doc.data instanceof InkField) { @@ -809,9 +816,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { case CollectionViewType.Freeform: { const { x, y, s, doc } = savedState!; - doc._panX = x; - doc._panY = y; - doc._viewScale = s; + doc._freeform_panX = x; + doc._freeform_panY = y; + doc._freeform_scale = s; } break; case DocumentType.INK: @@ -827,7 +834,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } }); LightboxView.SetLightboxDoc(undefined); - Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, this.rootDoc); + Doc.RemFromMyOverlay(this.rootDoc); return PresStatus.Edit; }; }; @@ -923,8 +930,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { return PresBox.OpenPresMinimized(this.rootDoc, [pt[0] + (this.props.PanelWidth() - 250), pt[1] + 10]); }; exitMinimize = () => { - if (DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) { - Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, this.rootDoc); + if (Doc.IsInMyOverlay(this.layoutDoc)) { + Doc.RemFromMyOverlay(this.rootDoc); CollectionDockingView.AddSplit(this.rootDoc, OpenWhereMod.right); } return PresStatus.Edit; @@ -936,7 +943,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { doc.overlayY = pt[1]; doc._height = 30; doc._width = PresBox.minimizedWidth; - Doc.AddDocToList(Doc.MyOverlayDocs, undefined, doc); + Doc.AddToMyOverlay(doc); PresBox.Instance?.initializePresState(PresBox.Instance.itemIndex); return (doc.presStatus = PresStatus.Manual); } @@ -948,11 +955,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @undoBatch viewChanged = action((e: React.ChangeEvent) => { //@ts-ignore - const viewType = e.target.selectedOptions[0].value as CollectionViewType; - this.layoutDoc.presFieldKey = this.fieldKey + (viewType === CollectionViewType.Tree ? '-linearized' : ''); + const type_collection = e.target.selectedOptions[0].value as CollectionViewType; + this.layoutDoc.presFieldKey = this.fieldKey + (type_collection === CollectionViewType.Tree ? '-linearized' : ''); // pivot field may be set by the user in timeline view (or some other way) -- need to reset it here - [CollectionViewType.Tree || CollectionViewType.Stacking].includes(viewType) && (this.rootDoc._pivotField = undefined); - this.rootDoc._viewType = viewType; + [CollectionViewType.Tree || CollectionViewType.Stacking].includes(type_collection) && (this.rootDoc._pivotField = undefined); + this.rootDoc._type_collection = type_collection; if (this.isTreeOrStack) { this.layoutDoc._gridGap = 0; } @@ -1006,17 +1013,10 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { return false; } } else { - if (!doc.aliasOf) { - const original = Doc.MakeAlias(doc); - TabDocView.PinDoc(original, {}); - setTimeout(() => this.removeDocument(doc), 0); - return false; - } else { - if (!doc.presentationTargetDoc) doc.title = doc.title + ' - Slide'; - doc.aliasOf instanceof Doc && (doc.presentationTargetDoc = doc.aliasOf); - doc.presMovement = PresMovement.Zoom; - if (this._expandBoolean) doc.presExpandInlineButton = true; - } + if (!doc.presentationTargetDoc) doc.title = doc.title + ' - Slide'; + doc.presentationTargetDoc = doc.createdFrom; // dropped document will be a new embedding of an embedded document somewhere else. + doc.presMovement = PresMovement.Zoom; + if (this._expandBoolean) doc.presExpandInlineButton = true; } }); return true; @@ -1084,7 +1084,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { this.rootDoc._itemIndex = index; } } else this.gotoDocument(this.childDocs.indexOf(doc), this.activeItem); - this.updateCurrentPresentation(DocCast(doc.context)); + this.updateCurrentPresentation(DocCast(doc.embedContainer)); }; //Command click @@ -1160,7 +1160,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { } break; case 'Escape': - if (DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc)) { + if (Doc.IsInMyOverlay(this.layoutDoc)) { this.exitClicked(); } else if (this.layoutDoc.presStatus === PresStatus.Edit) { this.clearSelectedArray(); @@ -1238,7 +1238,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { .filter(doc => Cast(doc.presentationTargetDoc, Doc, null)) .forEach((doc, index) => { const tagDoc = Cast(doc.presentationTargetDoc, Doc, null); - const srcContext = Cast(tagDoc.context, Doc, null); + const srcContext = Cast(tagDoc.embedContainer, Doc, null); const width = NumCast(tagDoc._width) / 10; const height = Math.max(NumCast(tagDoc._height) / 10, 15); const edge = Math.max(width, height); @@ -1298,7 +1298,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { /** * Method called for viewing paths which adds a single line with * points at the center of each document added. - * Design choice: When this is called it sets _fitContentsToBox as true so the + * Design choice: When this is called it sets _freeform_fitContentsToBox as true so the * user can have an overview of all of the documents in the collection. * (Design needed for when documents in presentation trail are in another * collection) @@ -1429,6 +1429,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { min={min} max={max} value={value} + readOnly={true} style={{ marginLeft: hmargin, marginRight: hmargin, width: `calc(100% - ${2 * (hmargin ?? 0)}px)` }} className={`toolbar-slider ${active ? '' : 'none'}`} onPointerDown={e => { @@ -1502,7 +1503,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="ribbon-doubleButton"> <div className="presBox-subheading">Slide Duration</div> <div className="ribbon-property"> - <input className="presBox-input" type="number" value={duration} onKeyDown={e => e.stopPropagation()} onChange={e => this.updateDurationTime(e.target.value)} /> s + <input className="presBox-input" type="number" readOnly={true} value={duration} onKeyDown={e => e.stopPropagation()} onChange={e => this.updateDurationTime(e.target.value)} /> s </div> <div className="ribbon-propertyUpDown"> <div className="ribbon-propertyUpDownItem" onClick={() => this.updateDurationTime(String(duration), 1000)}> @@ -1551,7 +1552,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { // a progressivized slide doesn't have sub-slides, but rather iterates over the data list of the target being progressivized. // to avoid creating a new slide to correspond to each of the target's data list, we create a computedField to refernce the target's data list. let dataField = Doc.LayoutFieldKey(tagDoc); - if (Cast(tagDoc[dataField], listSpec(Doc), null)?.filter(d => d instanceof Doc) === undefined) dataField = dataField + '-annotations'; + if (Cast(tagDoc[dataField], listSpec(Doc), null)?.filter(d => d instanceof Doc) === undefined) dataField = dataField + '_annotations'; if (DocCast(activeItem.presentationTargetDoc).annotationOn) activeItem.data = ComputedField.MakeFunction(`self.presentationTargetDoc.annotationOn["${dataField}"]`); else activeItem.data = ComputedField.MakeFunction(`self.presentationTargetDoc["${dataField}"]`); @@ -1655,7 +1656,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="ribbon-doubleButton" style={{ display: activeItem.presMovement === PresMovement.Zoom ? 'inline-flex' : 'none' }}> <div className="presBox-subheading">Zoom (% screen filled)</div> <div className="ribbon-property"> - <input className="presBox-input" type="number" value={zoom} onChange={e => this.updateZoom(e.target.value)} />% + <input className="presBox-input" type="number" readOnly={true} value={zoom} onChange={e => this.updateZoom(e.target.value)} />% </div> <div className="ribbon-propertyUpDown"> <div className="ribbon-propertyUpDownItem" onClick={() => this.updateZoom(String(zoom), 0.1)}> @@ -1670,7 +1671,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { <div className="ribbon-doubleButton" style={{ display: 'inline-flex' }}> <div className="presBox-subheading">Transition Time</div> <div className="ribbon-property"> - <input className="presBox-input" type="number" value={transitionSpeed} onKeyDown={e => e.stopPropagation()} onChange={action(e => this.updateTransitionTime(e.target.value))} /> s + <input className="presBox-input" type="number" readOnly={true} value={transitionSpeed} onKeyDown={e => e.stopPropagation()} onChange={action(e => this.updateTransitionTime(e.target.value))} /> s </div> <div className="ribbon-propertyUpDown"> <div className="ribbon-propertyUpDownItem" onClick={() => this.updateTransitionTime(String(transitionSpeed), 1000)}> @@ -1741,7 +1742,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { const activeItem = this.activeItem; if (activeItem && this.targetDoc) { const clipStart = NumCast(activeItem.clipStart); - const clipEnd = NumCast(activeItem.clipEnd, NumCast(activeItem[Doc.LayoutFieldKey(activeItem) + '-duration'])); + const clipEnd = NumCast(activeItem.clipEnd, NumCast(activeItem[Doc.LayoutFieldKey(activeItem) + '_duration'])); return ( <div className={'presBox-ribbon'} onClick={e => e.stopPropagation()} onPointerUp={e => e.stopPropagation()} onPointerDown={e => e.stopPropagation()}> <div> @@ -1757,6 +1758,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { className="presBox-input" style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }} type="number" + readOnly={true} value={NumCast(activeItem.presStartTime).toFixed(2)} onKeyDown={e => e.stopPropagation()} onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { @@ -1783,6 +1785,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { onKeyDown={e => e.stopPropagation()} style={{ textAlign: 'center', width: '100%', height: 15, fontSize: 10 }} type="number" + readOnly={true} value={NumCast(activeItem.presEndTime).toFixed(2)} onChange={action((e: React.ChangeEvent<HTMLInputElement>) => { activeItem.presEndTime = Number(e.target.value); @@ -2065,20 +2068,20 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { createTemplate = (layout: string, input?: string) => { const x = this.activeItem && this.targetDoc ? NumCast(this.targetDoc.x) : 0; const y = this.activeItem && this.targetDoc ? NumCast(this.targetDoc.y) + NumCast(this.targetDoc._height) + 20 : 0; - const title = () => Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 58, _fontSize: '24pt' }); - const subtitle = () => Docs.Create.TextDocument('Click to change subtitle', { title: 'Slide subtitle', _width: 380, _height: 50, x: 10, y: 118, _fontSize: '16pt' }); - const header = () => Docs.Create.TextDocument('Click to change header', { title: 'Slide header', _width: 380, _height: 65, x: 10, y: 80, _fontSize: '20pt' }); - const contentTitle = () => Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 10, _fontSize: '24pt' }); - const content = () => Docs.Create.TextDocument('Click to change text', { title: 'Slide text', _width: 380, _height: 145, x: 10, y: 70, _fontSize: '14pt' }); - const content1 = () => Docs.Create.TextDocument('Click to change text', { title: 'Column 1', _width: 185, _height: 140, x: 10, y: 80, _fontSize: '14pt' }); - const content2 = () => Docs.Create.TextDocument('Click to change text', { title: 'Column 2', _width: 185, _height: 140, x: 205, y: 80, _fontSize: '14pt' }); + const title = () => Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 58, _text_fontSize: '24pt' }); + const subtitle = () => Docs.Create.TextDocument('Click to change subtitle', { title: 'Slide subtitle', _width: 380, _height: 50, x: 10, y: 118, _text_fontSize: '16pt' }); + const header = () => Docs.Create.TextDocument('Click to change header', { title: 'Slide header', _width: 380, _height: 65, x: 10, y: 80, _text_fontSize: '20pt' }); + const contentTitle = () => Docs.Create.TextDocument('Click to change title', { title: 'Slide title', _width: 380, _height: 60, x: 10, y: 10, _text_fontSize: '24pt' }); + const content = () => Docs.Create.TextDocument('Click to change text', { title: 'Slide text', _width: 380, _height: 145, x: 10, y: 70, _text_fontSize: '14pt' }); + const content1 = () => Docs.Create.TextDocument('Click to change text', { title: 'Column 1', _width: 185, _height: 140, x: 10, y: 80, _text_fontSize: '14pt' }); + const content2 = () => Docs.Create.TextDocument('Click to change text', { title: 'Column 2', _width: 185, _height: 140, x: 205, y: 80, _text_fontSize: '14pt' }); // prettier-ignore switch (layout) { case 'blank': return Docs.Create.FreeformDocument([], { title: input ? input : 'Blank slide', _width: 400, _height: 225, x, y }); - case 'title': return Docs.Create.FreeformDocument([title(), subtitle()], { title: input ? input : 'Title slide', _width: 400, _height: 225, _fitContentsToBox: true, x, y }); - case 'header': return Docs.Create.FreeformDocument([header()], { title: input ? input : 'Section header', _width: 400, _height: 225, _fitContentsToBox: true, x, y }); - case 'content': return Docs.Create.FreeformDocument([contentTitle(), content()], { title: input ? input : 'Title and content', _width: 400, _height: 225, _fitContentsToBox: true, x, y }); - case 'twoColumns': return Docs.Create.FreeformDocument([contentTitle(), content1(), content2()], { title: input ? input : 'Title and two columns', _width: 400, _height: 225, _fitContentsToBox: true, x, y }) + case 'title': return Docs.Create.FreeformDocument([title(), subtitle()], { title: input ? input : 'Title slide', _width: 400, _height: 225, _layout_fitContentsToBox: true, x, y }); + case 'header': return Docs.Create.FreeformDocument([header()], { title: input ? input : 'Section header', _width: 400, _height: 225, _layout_fitContentsToBox: true, x, y }); + case 'content': return Docs.Create.FreeformDocument([contentTitle(), content()], { title: input ? input : 'Title and content', _width: 400, _height: 225, _layout_fitContentsToBox: true, x, y }); + case 'twoColumns': return Docs.Create.FreeformDocument([contentTitle(), content1(), content2()], { title: input ? input : 'Title and two columns', _width: 400, _height: 225, _layout_fitContentsToBox: true, x, y }) } }; @@ -2127,12 +2130,11 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @computed get toolbar() { const propIcon = SettingsManager.propertiesWidth > 0 ? 'angle-double-right' : 'angle-double-left'; const propTitle = SettingsManager.propertiesWidth > 0 ? 'Close Presentation Panel' : 'Open Presentation Panel'; - const mode = StrCast(this.rootDoc._viewType) as CollectionViewType; + const mode = StrCast(this.rootDoc._type_collection) as CollectionViewType; const isMini: boolean = this.toolbarWidth <= 100; - const inOverlay = DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc); const activeColor = Colors.LIGHT_BLUE; const inactiveColor = Colors.WHITE; - return mode === CollectionViewType.Carousel3D || inOverlay ? null : ( + return mode === CollectionViewType.Carousel3D || Doc.IsInMyOverlay(this.rootDoc) ? null : ( <div id="toolbarContainer" className={'presBox-toolbar'}> {/* <Tooltip title={<><div className="dash-tooltip">{"Add new slide"}</div></>}><div className={`toolbar-button ${this.newDocumentTools ? "active" : ""}`} onClick={action(() => this.newDocumentTools = !this.newDocumentTools)}> <FontAwesomeIcon icon={"plus"} /> @@ -2171,11 +2173,12 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { * presentPanel: The button to start the presentation / open minimized view of the presentation */ @computed get topPanel() { - const mode = StrCast(this.rootDoc._viewType) as CollectionViewType; + const mode = StrCast(this.rootDoc._type_collection) as CollectionViewType; const isMini: boolean = this.toolbarWidth <= 100; - const inOverlay = DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc); return ( - <div className={`presBox-buttons${inOverlay ? ' inOverlay' : ''}`} style={{ background: Doc.ActivePresentation === this.rootDoc ? Colors.LIGHT_BLUE : undefined, display: !this.rootDoc._chromeHidden ? 'none' : undefined }}> + <div + className={`presBox-buttons${Doc.IsInMyOverlay(this.rootDoc) ? ' inOverlay' : ''}`} + style={{ background: Doc.ActivePresentation === this.rootDoc ? Colors.LIGHT_BLUE : undefined, display: !this.rootDoc._chromeHidden ? 'none' : undefined }}> {isMini ? null : ( <select className="presBox-viewPicker" style={{ display: this.layoutDoc.presStatus === 'edit' ? 'block' : 'none' }} onPointerDown={e => e.stopPropagation()} onChange={this.viewChanged} value={mode}> <option onPointerDown={StopEvent} value={CollectionViewType.Stacking}> @@ -2225,7 +2228,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { @computed get playButtons() { const presEnd = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1 && (this.activeItem.presIndexed === undefined || NumCast(this.activeItem.presIndexed) === (this.progressivizedItems(this.activeItem)?.length ?? 0)); const presStart: boolean = !this.layoutDoc.presLoop && this.itemIndex === 0; - const inOverlay = DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc); + const inOverlay = Doc.IsInMyOverlay(this.rootDoc); // Case 1: There are still other frames and should go through all frames before going to next slide return ( <div className="presPanelOverlay" style={{ display: this.layoutDoc.presStatus !== 'edit' ? 'inline-flex' : 'none' }}> @@ -2396,10 +2399,9 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { render() { // needed to ensure that the childDocs are loaded for looking up fields this.childDocs.slice(); - const mode = StrCast(this.rootDoc._viewType) as CollectionViewType; + const mode = StrCast(this.rootDoc._type_collection) as CollectionViewType; const presEnd = !this.layoutDoc.presLoop && this.itemIndex === this.childDocs.length - 1 && (this.activeItem.presIndexed === undefined || NumCast(this.activeItem.presIndexed) === (this.progressivizedItems(this.activeItem)?.length ?? 0)); const presStart = !this.layoutDoc.presLoop && this.itemIndex === 0; - const inOverlay = DocListCast(Doc.MyOverlayDocs?.data).includes(this.layoutDoc); return this.props.addDocTab === returnFalse ? ( // bcz: hack!! - addDocTab === returnFalse only when this is being rendered by the OverlayView which means the doc is a mini player <div className="miniPres" onClick={e => e.stopPropagation()} onPointerEnter={action(e => (this._forceKeyEvents = true))}> <div @@ -2442,7 +2444,7 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { </div> </div> ) : ( - <div className="presBox-cont" style={{ minWidth: inOverlay ? PresBox.minimizedWidth : undefined }}> + <div className="presBox-cont" style={{ minWidth: Doc.IsInMyOverlay(this.rootDoc) ? PresBox.minimizedWidth : undefined }}> {this.topPanel} {this.toolbar} {this.newDocumentToolbarDropdown} @@ -2456,9 +2458,8 @@ export class PresBox extends ViewBoxBaseComponent<FieldViewProps>() { childIgnoreNativeSize={true} moveDocument={returnFalse} ignoreUnrendered={true} - //childFitWidth={returnTrue} + //childLayoutFitWidth={returnTrue} childOpacity={returnOne} - //childLayoutString={PresElementBox.LayoutString('data')} childClickScript={PresBox.navigateToDocScript} childLayoutTemplate={this.childLayoutTemplate} childXPadding={Doc.IsComicStyle(this.rootDoc) ? 20 : undefined} diff --git a/src/client/views/nodes/trails/PresElementBox.tsx b/src/client/views/nodes/trails/PresElementBox.tsx index 62e5314c3..34e069046 100644 --- a/src/client/views/nodes/trails/PresElementBox.tsx +++ b/src/client/views/nodes/trails/PresElementBox.tsx @@ -1,19 +1,20 @@ import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { Tooltip } from '@material-ui/core'; -import { action, computed, IReactionDisposer, observable, reaction } from 'mobx'; +import { action, computed, IReactionDisposer, observable, reaction, runInAction } from 'mobx'; import { observer } from 'mobx-react'; -import { Doc, DocListCast, HeightSym, Opt, WidthSym } from '../../../../fields/Doc'; +import { Doc, DocListCast, Opt } from '../../../../fields/Doc'; +import { Height, Width } from '../../../../fields/DocSymbols'; import { Id } from '../../../../fields/FieldSymbols'; import { List } from '../../../../fields/List'; import { Cast, DocCast, NumCast, StrCast } from '../../../../fields/Types'; import { emptyFunction, returnEmptyDoclist, returnFalse, returnTrue, setupMoveUpEvents } from '../../../../Utils'; -import { Docs, DocUtils } from '../../../documents/Documents'; +import { Docs } from '../../../documents/Documents'; import { CollectionViewType } from '../../../documents/DocumentTypes'; import { DocumentManager } from '../../../util/DocumentManager'; import { DragManager } from '../../../util/DragManager'; import { SettingsManager } from '../../../util/SettingsManager'; import { Transform } from '../../../util/Transform'; -import { undoBatch } from '../../../util/UndoManager'; +import { undoable, undoBatch } from '../../../util/UndoManager'; import { ViewBoxBaseComponent } from '../../DocComponent'; import { EditableView } from '../../EditableView'; import { Colors } from '../../global/globalEnums'; @@ -61,7 +62,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { } componentDidMount() { - this.layoutDoc.hideLinkButton = true; + this.layoutDoc.layout_hideLinkButton = true; this._heightDisposer = reaction( () => ({ expand: this.rootDoc.presExpandInlineButton, height: this.collapsedHeight }), ({ expand, height }) => (this.layoutDoc._height = height + (expand ? this.expandViewHeight : 0)), @@ -106,8 +107,8 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { ScreenToLocalTransform={Transform.Identity} renderDepth={this.props.renderDepth + 1} docViewPath={returnEmptyDoclist} - docFilters={this.props.docFilters} - docRangeFilters={this.props.docRangeFilters} + childFilters={this.props.childFilters} + childFiltersByRanges={this.props.childFiltersByRanges} searchFilterDocs={this.props.searchFilterDocs} rootSelected={returnTrue} addDocument={returnFalse} @@ -192,7 +193,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { const dragData = new DragManager.DocumentDragData(this.presBoxView?.sortArray() ?? []); if (!dragData.draggedDocuments.length) dragData.draggedDocuments.push(this.rootDoc); dragData.dropAction = 'move'; - dragData.treeViewDoc = this.presBox?._viewType === CollectionViewType.Tree ? this.presBox : undefined; // this.props.DocumentView?.()?.props.treeViewDoc; + dragData.treeViewDoc = this.presBox?._type_collection === CollectionViewType.Tree ? this.presBox : undefined; // this.props.DocumentView?.()?.props.treeViewDoc; dragData.moveDocument = this.props.moveDocument; const dragItem: HTMLElement[] = []; if (dragArray.length === 1) { @@ -263,16 +264,15 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { } }; - @undoBatch - removeItem = action((e: React.MouseEvent) => { + removePresentationItem = undoable((e: React.MouseEvent) => { e.stopPropagation(); if (this.presBox && this.indexInPres < (this.presBoxView?.itemIndex || 0)) { - this.presBox.itemIndex = (this.presBoxView?.itemIndex || 0) - 1; + runInAction(() => (this.presBox!.itemIndex = (this.presBoxView?.itemIndex || 0) - 1)); } this.props.removeDocument?.(this.rootDoc); this.presBoxView?.removeFromSelectedArray(this.rootDoc); this.removeAllRecordingInOverlay(); - }); + }, 'Remove doc from pres trail'); // set the value/title of the individual pres element @undoBatch @@ -319,15 +319,13 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { // a previously recorded video will have timecode defined static videoIsRecorded = (activeItem: Opt<Doc>) => { const casted = Cast(activeItem?.recording, Doc, null); - return casted && 'currentTimecode' in casted; + return casted && 'layout_currentTimecode' in casted; }; removeAllRecordingInOverlay = () => { - DocListCast(Doc.MyOverlayDocs.data).forEach(doc => { - if (doc.slides === this.rootDoc) { - Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, doc); - } - }); + DocListCast(Doc.MyOverlayDocs.data) + .filter(doc => doc.slides === this.rootDoc) + .forEach(Doc.RemFromMyOverlay); }; static removeEveryExistingRecordingInOverlay = () => { @@ -335,9 +333,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { DocListCast(Doc.MyOverlayDocs.data).forEach(doc => { if (doc.slides !== null) { // if it's a recording video, don't remove from overlay (user can lose data) - if (!PresElementBox.videoIsRecorded(DocCast(doc.slides))) return; - - Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, doc); + if (PresElementBox.videoIsRecorded(DocCast(doc.slides))) { + Doc.RemFromMyOverlay(doc); + } } }); }; @@ -357,7 +355,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { if (!iconClick) PresElementBox.removeEveryExistingRecordingInOverlay(); if (activeItem.recording) { - Doc.AddDocToList(Doc.MyOverlayDocs, undefined, DocCast(activeItem.recording)); + Doc.AddToMyOverlay(DocCast(activeItem.recording)); } }; @@ -369,7 +367,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { // if we already have an existing recording this.showRecording(activeItem, true); // // if we already have an existing recording - // Doc.AddDocToList(Doc.MyOverlayDocs, undefined, Cast(activeItem.recording, Doc, null)); + // Doc.AddToMyOverlay(Cast(activeItem.recording, Doc, null)); } else { // Remove every recording that already exists in overlay view // this is a design decision to clear to focus in on the recoding mode @@ -379,11 +377,11 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { const recording = Docs.Create.WebCamDocument('', { _width: 384, _height: 216, - hideDocumentButtonBar: true, - hideDecorationTitle: true, - hideOpenButton: true, + layout_hideDocumentButtonBar: true, + layout_hideDecorationTitle: true, + layout_hideOpenButton: true, // hideDeleteButton: true, - cloneFieldFilter: new List<string>(['system']), + cloneFieldFilter: new List<string>(['isSystem']), }); // attach the recording to the slide, and attach the slide to the recording @@ -391,9 +389,9 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { activeItem.recording = recording; // make recording box appear in the bottom right corner of the screen - recording.overlayX = window.innerWidth - recording[WidthSym]() - 20; - recording.overlayY = window.innerHeight - recording[HeightSym]() - 20; - Doc.AddDocToList(Doc.MyOverlayDocs, undefined, recording); + recording.overlayX = window.innerWidth - recording[Width]() - 20; + recording.overlayY = window.innerHeight - recording[Height]() - 20; + Doc.AddToMyOverlay(recording); } }; @@ -476,7 +474,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { ); items.push( <Tooltip key="trash" title={<div className="dash-tooltip">Remove from presentation</div>}> - <div className={'slideButton'} onClick={this.removeItem}> + <div className={'slideButton'} onClick={this.removePresentationItem}> <FontAwesomeIcon icon={'trash'} onPointerDown={e => e.stopPropagation()} /> </div> </Tooltip> @@ -524,7 +522,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { style={{ display: 'infline-block', backgroundColor: this.props.styleProvider?.(this.layoutDoc, this.props, StyleProp.BackgroundColor), - //boxShadow: presBoxColor && presBoxColor !== 'white' && presBoxColor !== 'transparent' ? (isCurrent ? '0 0 0px 1.5px' + presBoxColor : undefined) : undefined, + //layout_boxShadow: presBoxColor && presBoxColor !== 'white' && presBoxColor !== 'transparent' ? (isCurrent ? '0 0 0px 1.5px' + presBoxColor : undefined) : undefined, border: presBoxColor && presBoxColor !== 'white' && presBoxColor !== 'transparent' ? (isCurrent ? presBoxColor + ' solid 2.5px' : undefined) : undefined, }}> <div @@ -546,7 +544,7 @@ export class PresElementBox extends ViewBoxBaseComponent<FieldViewProps>() { } }} onClick={e => e.stopPropagation()}>{`${this.indexInPres + 1}. `}</div> - <EditableView ref={this._titleRef} editing={!isSelected ? false : undefined} contents={activeItem.title} overflow={'ellipsis'} GetValue={() => StrCast(activeItem.title)} SetValue={this.onSetValue} /> + <EditableView ref={this._titleRef} oneLine={true} editing={!isSelected ? false : undefined} contents={activeItem.title} overflow={'ellipsis'} GetValue={() => StrCast(activeItem.title)} SetValue={this.onSetValue} /> </div> {/* <Tooltip title={<><div className="dash-tooltip">{"Movement speed"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.transition}</div></Tooltip> */} {/* <Tooltip title={<><div className="dash-tooltip">{"Duration"}</div></>}><div className="presItem-time" style={{ display: showMore ? "block" : "none" }}>{this.duration}</div></Tooltip> */} diff --git a/src/client/views/pdf/AnchorMenu.tsx b/src/client/views/pdf/AnchorMenu.tsx index d6dddf71a..5480600b0 100644 --- a/src/client/views/pdf/AnchorMenu.tsx +++ b/src/client/views/pdf/AnchorMenu.tsx @@ -395,22 +395,25 @@ export class AnchorMenu extends AntimodeMenu<AntimodeMenuProps> { ) : ( <> <Tooltip key="trash" title={<div className="dash-tooltip">Remove Link Anchor</div>}> - <button className="antimodeMenu-button" onPointerDown={this.Delete}> + <button className="antimodeMenu-button" style={{ display: this.Delete === returnFalse ? 'none' : undefined }} onPointerDown={this.Delete}> <FontAwesomeIcon icon="trash-alt" size="lg" /> </button> </Tooltip> <Tooltip key="Pin" title={<div className="dash-tooltip">Pin to Presentation</div>}> - <button className="antimodeMenu-button" onPointerDown={this.PinToPres}> + <button className="antimodeMenu-button" style={{ display: this.PinToPres === returnFalse ? 'none' : undefined }} onPointerDown={this.PinToPres}> <FontAwesomeIcon icon="map-pin" size="lg" /> </button> </Tooltip> <Tooltip key="trail" title={<div className="dash-tooltip">Show Linked Trail</div>}> - <button className="antimodeMenu-button" onPointerDown={this.ShowTargetTrail}> + <button className="antimodeMenu-button" style={{ display: this.ShowTargetTrail === returnFalse ? 'none' : undefined }} onPointerDown={this.ShowTargetTrail}> <FontAwesomeIcon icon="taxi" size="lg" /> </button> </Tooltip> <Tooltip key="toggle" title={<div className="dash-tooltip">make target visibility toggle on click</div>}> - <button className="antimodeMenu-button" style={{ color: this.IsTargetToggler() ? 'black' : 'white', backgroundColor: this.IsTargetToggler() ? 'white' : 'black' }} onPointerDown={this.MakeTargetToggle}> + <button + className="antimodeMenu-button" + style={{ display: this.IsTargetToggler === returnFalse ? 'none' : undefined, color: this.IsTargetToggler() ? 'black' : 'white', backgroundColor: this.IsTargetToggler() ? 'white' : 'black' }} + onPointerDown={this.MakeTargetToggle}> <FontAwesomeIcon icon="thumbtack" size="lg" /> </button> </Tooltip> diff --git a/src/client/views/pdf/GPTPopup/GPTPopup.tsx b/src/client/views/pdf/GPTPopup/GPTPopup.tsx index cc0072a94..8bd060d4f 100644 --- a/src/client/views/pdf/GPTPopup/GPTPopup.tsx +++ b/src/client/views/pdf/GPTPopup/GPTPopup.tsx @@ -51,8 +51,8 @@ export class GPTPopup extends React.Component<GPTPopupProps> { const newDoc = Docs.Create.TextDocument(this.props.text.trim(), { _width: 200, _height: 50, - _fitWidth: true, - _autoHeight: true, + _layout_fitWidth: true, + _layout_autoHeight: true, }); this.addDoc(newDoc, this.sidebarId); }; diff --git a/src/client/views/pdf/PDFViewer.tsx b/src/client/views/pdf/PDFViewer.tsx index fce67e7fc..dd202418b 100644 --- a/src/client/views/pdf/PDFViewer.tsx +++ b/src/client/views/pdf/PDFViewer.tsx @@ -2,7 +2,8 @@ import { action, computed, IReactionDisposer, observable, ObservableMap, reactio import { observer } from 'mobx-react'; import * as Pdfjs from 'pdfjs-dist'; import 'pdfjs-dist/web/pdf_viewer.css'; -import { Doc, DocListCast, Field, HeightSym, Opt } from '../../../fields/Doc'; +import { Doc, DocListCast, Field, Opt } from '../../../fields/Doc'; +import { Height } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { InkTool } from '../../../fields/InkField'; import { Cast, NumCast, StrCast } from '../../../fields/Types'; @@ -90,7 +91,7 @@ export class PDFViewer extends React.Component<IViewerProps> { @observable isAnnotating = false; // key where data is stored @computed get allAnnotations() { - return DocUtils.FilterDocs(DocListCast(this.props.dataDoc[this.props.fieldKey + '-annotations']), this.props.docFilters(), this.props.docRangeFilters()); + return DocUtils.FilterDocs(DocListCast(this.props.dataDoc[this.props.fieldKey + '_annotations']), this.props.childFilters(), this.props.childFiltersByRanges()); } @computed get inlineTextAnnotations() { return this.allAnnotations.filter(a => a.textInlineAnnotations); @@ -101,12 +102,12 @@ export class PDFViewer extends React.Component<IViewerProps> { this.setupPdfJsViewer(); this._mainCont.current?.addEventListener('scroll', e => ((e.target as any).scrollLeft = 0)); - this._disposers.autoHeight = reaction( - () => this.props.layoutDoc._autoHeight, - autoHeight => { - if (autoHeight) { - this.props.layoutDoc._nativeHeight = NumCast(this.props.Document[this.props.fieldKey + '-nativeHeight']); - this.props.setHeight?.(NumCast(this.props.Document[this.props.fieldKey + '-nativeHeight']) * (this.props.NativeDimScaling?.() || 1)); + this._disposers.layout_autoHeight = reaction( + () => this.props.layoutDoc._layout_autoHeight, + layout_autoHeight => { + if (layout_autoHeight) { + this.props.layoutDoc._nativeHeight = NumCast(this.props.Document[this.props.fieldKey + '_nativeHeight']); + this.props.setHeight?.(NumCast(this.props.Document[this.props.fieldKey + '_nativeHeight']) * (this.props.NativeDimScaling?.() || 1)); } } ); @@ -123,7 +124,7 @@ export class PDFViewer extends React.Component<IViewerProps> { { fireImmediately: true } ); this._disposers.curPage = reaction( - () => Cast(this.props.Document._curPage, 'number', null), + () => Cast(this.props.Document._layout_curPage, 'number', null), page => page !== undefined && page !== this._pdfViewer?.currentPageNumber && this.gotoPage(page), { fireImmediately: true } ); @@ -181,14 +182,14 @@ export class PDFViewer extends React.Component<IViewerProps> { let focusSpeed: Opt<number>; if (doc !== this.props.rootDoc && mainCont) { const windowHeight = this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); - const scrollTo = Utils.scrollIntoView(scrollTop, doc[HeightSym](), NumCast(this.props.layoutDoc._scrollTop), windowHeight, windowHeight * 0.1, this._scrollHeight); - if (scrollTo !== undefined && scrollTo !== this.props.layoutDoc._scrollTop) { + const scrollTo = Utils.scrollIntoView(scrollTop, doc[Height](), NumCast(this.props.layoutDoc._layout_scrollTop), windowHeight, windowHeight * 0.1, this._scrollHeight); + if (scrollTo !== undefined && scrollTo !== this.props.layoutDoc._layout_scrollTop) { if (!this._pdfViewer) this._initialScroll = { loc: scrollTo, easeFunc: options.easeFunc }; else if (!options.instant) this._scrollStopper = smoothScroll((focusSpeed = options.zoomTime ?? 500), mainCont, scrollTo, options.easeFunc, this._scrollStopper); else this._mainCont.current?.scrollTo({ top: Math.abs(scrollTo || 0) }); } } else { - this._initialScroll = { loc: NumCast(this.props.layoutDoc._scrollTop), easeFunc: options.easeFunc }; + this._initialScroll = { loc: NumCast(this.props.layoutDoc._layout_scrollTop), easeFunc: options.easeFunc }; } return focusSpeed; }; @@ -208,18 +209,18 @@ export class PDFViewer extends React.Component<IViewerProps> { pagesinit = () => { if (this._pdfViewer._setDocumentViewerElement?.offsetParent) { - runInAction(() => (this._pdfViewer.currentScaleValue = this.props.layoutDoc._viewScale = 1)); - this.gotoPage(NumCast(this.props.Document._curPage, 1)); + runInAction(() => (this._pdfViewer.currentScaleValue = this.props.layoutDoc._freeform_scale = 1)); + this.gotoPage(NumCast(this.props.Document._layout_curPage, 1)); } document.removeEventListener('pagesinit', this.pagesinit); var quickScroll: { loc?: string; easeFunc?: 'ease' | 'linear' } | undefined = { loc: this._initialScroll ? this._initialScroll.loc?.toString() : '', easeFunc: this._initialScroll ? this._initialScroll.easeFunc : undefined }; this._disposers.scale = reaction( - () => NumCast(this.props.layoutDoc._viewScale, 1), + () => NumCast(this.props.layoutDoc._freeform_scale, 1), scale => (this._pdfViewer.currentScaleValue = scale), { fireImmediately: true } ); this._disposers.scroll = reaction( - () => Math.abs(NumCast(this.props.Document._scrollTop)), + () => Math.abs(NumCast(this.props.Document._layout_scrollTop)), pos => { if (!this._ignoreScroll) { this._showWaiting && this.setupPdfJsViewer(); @@ -316,7 +317,7 @@ export class PDFViewer extends React.Component<IViewerProps> { if (this._mainCont.current && !this._forcedScroll) { this._ignoreScroll = true; // the pdf scrolled, so we need to tell the Doc to scroll but we don't want the doc to then try to set the PDF scroll pos (which would interfere with the smooth scroll animation) if (!LinkDocPreview.LinkInfo) { - this.props.layoutDoc._scrollTop = this._mainCont.current.scrollTop; + this.props.layoutDoc._layout_scrollTop = this._mainCont.current.scrollTop; } this._ignoreScroll = false; if (this._scrollTimer) clearTimeout(this._scrollTimer); // wait until a scrolling pause, then create an anchor to audio @@ -371,7 +372,7 @@ export class PDFViewer extends React.Component<IViewerProps> { // if alt+left click, drag and annotate this._downX = e.clientX; this._downY = e.clientY; - if ((this.props.Document._viewScale || 1) !== 1) return; + if ((this.props.Document._freeform_scale || 1) !== 1) return; if ((e.button !== 0 || e.altKey) && this.props.isContentActive(true)) { this._setPreviewCursor?.(e.clientX, e.clientY, true, false); } @@ -421,7 +422,7 @@ export class PDFViewer extends React.Component<IViewerProps> { } // Changing which document to add the annotation to (the currently selected PDF) - GPTPopup.Instance.setSidebarId('data-sidebar'); + GPTPopup.Instance.setSidebarId('data_sidebar'); GPTPopup.Instance.addDoc = this.props.sidebarAddDoc; }; @@ -434,7 +435,7 @@ export class PDFViewer extends React.Component<IViewerProps> { const rect = clientRects.item(i); if (rect?.width && rect.width < this._mainCont.current.clientWidth / this.props.ScreenToLocalTransform().Scale) { const scaleX = this._mainCont.current.offsetWidth / boundingRect.width; - const pdfScale = NumCast(this.props.layoutDoc._viewScale, 1); + const pdfScale = NumCast(this.props.layoutDoc._freeform_scale, 1); const annoBox = document.createElement('div'); annoBox.className = 'marqueeAnnotator-annotationBox'; // transforms the positions from screen onto the pdf div @@ -477,7 +478,7 @@ export class PDFViewer extends React.Component<IViewerProps> { if (e.ctrlKey) { const curScale = Number(this._pdfViewer.currentScaleValue); this._pdfViewer.currentScaleValue = Math.max(1, Math.min(10, curScale - (curScale * e.deltaY) / 1000)); - this.props.layoutDoc._viewScale = Number(this._pdfViewer.currentScaleValue); + this.props.layoutDoc._freeform_scale = Number(this._pdfViewer.currentScaleValue); } } }; @@ -485,12 +486,12 @@ export class PDFViewer extends React.Component<IViewerProps> { pointerEvents = () => (this.props.isContentActive() && this.props.pointerEvents?.() !== 'none' && !MarqueeOptionsMenu.Instance.isShown() ? 'all' : SnappingManager.GetIsDragging() ? undefined : 'none'); @computed get annotationLayer() { return ( - <div className="pdfViewerDash-annotationLayer" style={{ height: Doc.NativeHeight(this.props.Document), transform: `scale(${NumCast(this.props.layoutDoc._viewScale, 1)})` }} ref={this._annotationLayer}> + <div className="pdfViewerDash-annotationLayer" style={{ height: Doc.NativeHeight(this.props.Document), transform: `scale(${NumCast(this.props.layoutDoc._freeform_scale, 1)})` }} ref={this._annotationLayer}> {this.inlineTextAnnotations .sort((a, b) => NumCast(a.y) - NumCast(b.y)) .filter(anno => !anno.hidden) .map(anno => ( - <Annotation {...this.props} fieldKey={this.props.fieldKey + '-annotations'} pointerEvents={this.pointerEvents} showInfo={this.showInfo} dataDoc={this.props.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} /> + <Annotation {...this.props} fieldKey={this.props.fieldKey + '_annotations'} pointerEvents={this.pointerEvents} showInfo={this.showInfo} dataDoc={this.props.dataDoc} anno={anno} key={`${anno[Id]}-annotation`} /> ))} </div> ); @@ -500,7 +501,7 @@ export class PDFViewer extends React.Component<IViewerProps> { return !this._overlayAnnoInfo ? null : ( <div className="pdfViewerDash-overlayAnno" style={{ top: NumCast(this._overlayAnnoInfo.y), left: NumCast(this._overlayAnnoInfo.x) }}> <div className="pdfViewerDash-overlayAnno" style={{ right: -50, background: SharingManager.Instance.users.find(users => users.user.email === this._overlayAnnoInfo!.author)?.userColor }}> - {this._overlayAnnoInfo.author + ' ' + Field.toString(this._overlayAnnoInfo.creationDate as Field)} + {this._overlayAnnoInfo.author + ' ' + Field.toString(this._overlayAnnoInfo.author_date as Field)} </div> </div> ); @@ -508,27 +509,26 @@ export class PDFViewer extends React.Component<IViewerProps> { getScrollHeight = () => this._scrollHeight; showInfo = action((anno: Opt<Doc>) => (this._overlayAnnoInfo = anno)); - scrollXf = () => (this._mainCont.current ? this.props.ScreenToLocalTransform().translate(0, NumCast(this.props.layoutDoc._scrollTop)) : this.props.ScreenToLocalTransform()); - overlayTransform = () => this.scrollXf().scale(1 / NumCast(this.props.layoutDoc._viewScale, 1)); + scrollXf = () => (this._mainCont.current ? this.props.ScreenToLocalTransform().translate(0, NumCast(this.props.layoutDoc._layout_scrollTop)) : this.props.ScreenToLocalTransform()); + overlayTransform = () => this.scrollXf().scale(1 / NumCast(this.props.layoutDoc._freeform_scale, 1)); panelWidth = () => this.props.PanelWidth() / (this.props.NativeDimScaling?.() || 1); panelHeight = () => this.props.PanelHeight() / (this.props.NativeDimScaling?.() || 1); - transparentFilter = () => [...this.props.docFilters(), Utils.IsTransparentFilter()]; - opaqueFilter = () => [...this.props.docFilters(), Utils.noDragsDocFilter, ...(DragManager.docsBeingDragged.length ? [] : [Utils.IsOpaqueFilter()])]; + transparentFilter = () => [...this.props.childFilters(), Utils.IsTransparentFilter()]; + opaqueFilter = () => [...this.props.childFilters(), Utils.noDragsDocFilter, ...(DragManager.docsBeingDragged.length ? [] : [Utils.IsOpaqueFilter()])]; childStyleProvider = (doc: Doc | undefined, props: Opt<DocumentViewProps>, property: string): any => { if (doc instanceof Doc && property === StyleProp.PointerEvents) { - if (doc.textInlineAnnotations) return 'none'; + if (this.inlineTextAnnotations.includes(doc)) return 'none'; return 'all'; } return this.props.styleProvider?.(doc, props, property); }; - renderAnnotations = (docFilters: () => string[], mixBlendMode?: any, display?: string) => ( + renderAnnotations = (childFilters: () => string[], mixBlendMode?: any, display?: string) => ( <div className="pdfViewerDash-overlay" style={{ mixBlendMode: mixBlendMode, display: display, - transform: `scale(${NumCast(this.props.layoutDoc._viewScale, 1)})`, pointerEvents: Doc.ActiveTool !== InkTool.None ? 'all' : undefined, }}> <CollectionFreeFormView @@ -537,10 +537,10 @@ export class PDFViewer extends React.Component<IViewerProps> { NativeHeight={returnZero} setContentView={emptyFunction} // override setContentView to do nothing pointerEvents={SnappingManager.GetIsDragging() ? returnAll : returnNone} // freeform view doesn't get events unless something is being dragged onto it. - childPointerEvents={'all'} // but freeform children need to get events to allow text editing, etc + childPointerEvents="all" // but freeform children need to get events to allow text editing, etc renderDepth={this.props.renderDepth + 1} isAnnotationOverlay={true} - fieldKey={this.props.fieldKey + '-annotations'} + fieldKey={this.props.fieldKey + '_annotations'} getScrollHeight={this.getScrollHeight} setPreviewCursor={this.setPreviewCursor} setBrushViewer={this.setBrushViewer} @@ -549,8 +549,8 @@ export class PDFViewer extends React.Component<IViewerProps> { ScreenToLocalTransform={this.overlayTransform} isAnyChildContentActive={returnFalse} isAnnotationOverlayScrollable={true} - dropAction={'alias'} - docFilters={docFilters} + dropAction="embed" + childFilters={childFilters} select={emptyFunction} bringToFront={emptyFunction} styleProvider={this.childStyleProvider} @@ -587,8 +587,8 @@ export class PDFViewer extends React.Component<IViewerProps> { onPointerDown={this.onPointerDown} onClick={this.onClick} style={{ - overflowX: NumCast(this.props.layoutDoc._viewScale, 1) !== 1 ? 'scroll' : undefined, - height: !this.props.Document._fitWidth && window.screen.width > 600 ? Doc.NativeHeight(this.props.Document) : `100%`, + overflowX: NumCast(this.props.layoutDoc._freeform_scale, 1) !== 1 ? 'scroll' : undefined, + height: !this.props.Document._layout_fitWidth && window.screen.width > 600 ? Doc.NativeHeight(this.props.Document) : `100%`, }}> {this.pdfViewerDiv} {this.annotationLayer} diff --git a/src/client/views/search/SearchBox.tsx b/src/client/views/search/SearchBox.tsx index 32b661bb6..d13c09443 100644 --- a/src/client/views/search/SearchBox.tsx +++ b/src/client/views/search/SearchBox.tsx @@ -2,7 +2,8 @@ import { Tooltip } from '@material-ui/core'; import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; -import { DirectLinksSym, Doc, DocListCast, DocListCastAsync, Field, Opt } from '../../../fields/Doc'; +import { Doc, DocListCast, DocListCastAsync, Field, Opt } from '../../../fields/Doc'; +import { DirectLinks } from '../../../fields/DocSymbols'; import { Id } from '../../../fields/FieldSymbols'; import { DocCast, StrCast } from '../../../fields/Types'; import { DocUtils } from '../../documents/Documents'; @@ -47,7 +48,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { @observable _linkedDocsIn: Map<Doc, Set<Doc>> = new Map<Doc, Set<Doc>>(); @observable _selectedResult: Doc | undefined = undefined; @observable _deletedDocsStatus: boolean = false; - @observable _onlyAliases: boolean = true; + @observable _onlyEmbeddings: boolean = true; /** * This is the constructor for the SearchBox class. @@ -139,8 +140,10 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { visited.push(d); const fieldKey = Doc.LayoutFieldKey(d); const annos = !Field.toString(Doc.LayoutField(d) as Field).includes('CollectionView'); - const data = d[annos ? fieldKey + '-annotations' : fieldKey]; + const data = d[annos ? fieldKey + '_annotations' : fieldKey]; data && newarray.push(...DocListCast(data)); + const sidebar = d[fieldKey + '_sidebar']; + sidebar && newarray.push(...DocListCast(sidebar)); func(depth, d); }); docs = newarray; @@ -166,7 +169,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { .map(async d => { const fieldKey = Doc.LayoutFieldKey(d); const annos = !Field.toString(Doc.LayoutField(d) as Field).includes('CollectionView'); - const data = d[annos ? fieldKey + '-annotations' : fieldKey]; + const data = d[annos ? fieldKey + '_annotations' : fieldKey]; const docs = await DocListCastAsync(data); docs && newarray.push(...docs); func(depth, d); @@ -211,40 +214,40 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { } @action static staticSearchCollection(rootDoc: Opt<Doc>, query: string) { - const blockedTypes = [DocumentType.PRESELEMENT, DocumentType.MARKER, DocumentType.KVP, DocumentType.FILTER, DocumentType.SEARCH, DocumentType.SEARCHITEM, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING]; + const blockedTypes = [DocumentType.PRESELEMENT, DocumentType.CONFIG, DocumentType.KVP, DocumentType.FILTER, DocumentType.SEARCH, DocumentType.SEARCHITEM, DocumentType.FONTICON, DocumentType.BUTTON, DocumentType.SCRIPTING]; const blockedKeys = [ 'x', 'y', 'proto', 'width', - 'autoHeight', + 'layout_autoHeight', 'acl-Override', 'acl-Public', - 'context', + 'embedContainer', 'zIndex', 'height', - 'text-scrollHeight', - 'text-height', + 'text_scrollHeight', + 'text_height', 'cloneFieldFilter', - 'isPrototype', - 'text-annotations', - 'dragFactory-count', - 'text-noTemplate', - 'aliases', - 'system', - 'layoutKey', - 'baseProto', + 'isDataDoc', + 'text_annotations', + 'dragFactory_count', + 'text_noTemplate', + 'proto_embeddings', + 'isSystem', + 'layout_fieldKey', + 'isBaseProto', 'xMargin', 'yMargin', 'links', 'layout', 'layout_keyValue', - 'fitWidth', - 'viewType', - 'title-custom', - 'panX', - 'panY', - 'viewScale', + 'layout_fitWidth', + 'type_collection', + 'title_custom', + 'freeform_panX', + 'freeform_panY', + 'freeform_scale', ]; query = query.toLowerCase(); @@ -290,7 +293,7 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { this._results.forEach((_, doc) => { this._pageRanks.set(doc, 1.0 / this._results.size); - if (Doc.GetProto(doc)[DirectLinksSym].size === 0) { + if (Doc.GetProto(doc)[DirectLinks].size === 0) { this._linkedDocsOut.set(doc, new Set(this._results.keys())); this._results.forEach((_, linkedDoc) => { @@ -299,9 +302,9 @@ export class SearchBox extends ViewBoxBaseComponent<SearchBoxProps>() { } else { const linkedDocSet = new Set<Doc>(); - Doc.GetProto(doc)[DirectLinksSym].forEach(link => { - const d1 = link?.anchor1 as Doc; - const d2 = link?.anchor2 as Doc; + Doc.GetProto(doc)[DirectLinks].forEach(link => { + const d1 = link?.link_anchor_1 as Doc; + const d2 = link?.link_anchor_2 as Doc; if (doc === d1 && this._results.has(d2)) { linkedDocSet.add(d2); this._linkedDocsIn.get(d2)?.add(doc); diff --git a/src/client/views/topbar/TopBar.tsx b/src/client/views/topbar/TopBar.tsx index 9bd2ba5ce..71daad1a9 100644 --- a/src/client/views/topbar/TopBar.tsx +++ b/src/client/views/topbar/TopBar.tsx @@ -4,7 +4,8 @@ import { action, computed, observable } from 'mobx'; import { observer } from 'mobx-react'; import * as React from 'react'; import { FaBug, FaCamera, FaStamp } from 'react-icons/fa'; -import { AclAdmin, Doc } from '../../../fields/Doc'; +import { Doc } from '../../../fields/Doc'; +import { AclAdmin } from '../../../fields/DocSymbols'; import { StrCast } from '../../../fields/Types'; import { GetEffectiveAcl } from '../../../fields/util'; import { DocumentManager } from '../../util/DocumentManager'; diff --git a/src/fields/Doc.ts b/src/fields/Doc.ts index 7cc7e0d2a..824aa014b 100644 --- a/src/fields/Doc.ts +++ b/src/fields/Doc.ts @@ -9,11 +9,40 @@ import { LinkManager } from '../client/util/LinkManager'; import { scriptingGlobal, ScriptingGlobals } from '../client/util/ScriptingGlobals'; import { SelectionManager } from '../client/util/SelectionManager'; import { afterDocDeserialize, autoObject, Deserializable, SerializationHelper } from '../client/util/SerializationHelper'; -import { UndoManager } from '../client/util/UndoManager'; +import { undoable, UndoManager } from '../client/util/UndoManager'; import { decycle } from '../decycler/decycler'; +import * as JSZipUtils from '../JSZipUtils'; import { DashColor, incrementTitleCopy, intersectRect, Utils } from '../Utils'; import { DateField } from './DateField'; -import { Copy, HandleUpdate, Id, OnUpdate, Parent, Self, SelfProxy, ToScriptString, ToString, Update } from './FieldSymbols'; +import { + AclAdmin, + AclAugment, + AclEdit, + AclPrivate, + AclReadonly, + AclSelfEdit, + AclUnset, + Animation, + CachedUpdates, + DirectLinks, + DocAcl, + DocCss, + DocData, + DocFields, + DocLayout, + FieldKeys, + FieldTuples, + ForceServerWrite, + Height, + Highlight, + Initializing, + Self, + SelfProxy, + Update, + UpdatingFromServer, + Width, +} from './DocSymbols'; +import { Copy, HandleUpdate, Id, OnUpdate, Parent, ToScriptString, ToString } from './FieldSymbols'; import { InkField, InkTool } from './InkField'; import { List, ListFieldName } from './List'; import { ObjectField } from './ObjectField'; @@ -26,7 +55,6 @@ import { Cast, DocCast, FieldValue, NumCast, StrCast, ToConstructor } from './Ty import { AudioField, CsvField, ImageField, PdfField, VideoField, WebField } from './URLField'; import { deleteProperty, GetEffectiveAcl, getField, getter, makeEditable, makeReadOnly, normalizeEmail, setter, SharingPermissions, updateFunction } from './util'; import JSZip = require('jszip'); -import * as JSZipUtils from '../JSZipUtils'; export namespace Field { export function toKeyValueString(doc: Doc, key: string): string { const onDelegate = Object.keys(doc).includes(key.replace(/^_/, '')); @@ -41,7 +69,7 @@ export namespace Field { switch (typeof field) { case 'string': if (field.startsWith('{"')) return `'${field}'`; // bcz: hack ... want to quote the string the right way. if there are nested "'s, then use ' instead of ". In this case, test for the start of a JSON string of the format {"property": ... } and use outer 's instead of "s - return `"${field}"`; + return !field.includes('`') ? `\`${field}\`` : `"${field}"`; case 'number': case 'boolean': return String(field); @@ -107,6 +135,7 @@ export const AclUnset = Symbol('AclUnset'); export const AclPrivate = Symbol('AclOwnerOnly'); export const AclReadonly = Symbol('AclReadOnly'); export const AclAugment = Symbol('AclAugment'); +export const AclSelfEdit = Symbol('AclSelfEdit'); export const AclEdit = Symbol('AclEdit'); export const AclAdmin = Symbol('AclAdmin'); export const UpdatingFromServer = Symbol('UpdatingFromServer'); @@ -137,15 +166,16 @@ export const ReverseHierarchyMap: Map<string, { level: aclLevel; acl: symbol }> // this recursively updates all protos as well. export function updateCachedAcls(doc: Doc) { if (!doc) return; + const target = (doc as any)?.__fields ?? doc; const permissions: { [key: string]: symbol } = !target.author || target.author === Doc.CurrentUserEmail ? { 'acl-Me': AclAdmin } : {}; Object.keys(target).filter(key => key.startsWith('acl') && (permissions[key] = ReverseHierarchyMap.get(StrCast(target[key]))!.acl)); - if (Object.keys(permissions).length || doc[AclSym]?.length) { - runInAction(() => (doc[AclSym] = permissions)); + if (Object.keys(permissions).length || doc[DocAcl]?.length) { + runInAction(() => (doc[DocAcl] = permissions)); } if (doc.proto instanceof Promise) { - doc.proto.then(updateCachedAcls); + doc.proto.then(proto => updateCachedAcls(DocCast(proto))); return doc.proto; } } @@ -207,6 +237,15 @@ export class Doc extends RefField { public static get MyTrails() { return DocCast(Doc.ActiveDashboard?.myTrails); } + public static IsInMyOverlay(doc: Doc) { + return DocListCast(Doc.MyOverlayDocs?.data).includes(doc); + } + public static AddToMyOverlay(doc: Doc) { + Doc.AddDocToList(Doc.MyOverlayDocs, undefined, doc); + } + public static RemFromMyOverlay(doc: Doc) { + Doc.RemoveDocFromList(Doc.MyOverlayDocs, undefined, doc); + } public static get MyOverlayDocs() { return DocCast(Doc.UserDoc().myOverlayDocs); } @@ -232,9 +271,9 @@ export class Doc extends RefField { if ( doc && Doc.MyFileOrphans instanceof Doc && - Doc.IsPrototype(doc) && + Doc.IsDataProto(doc) && !Doc.IsSystem(doc) && - ![DocumentType.MARKER, DocumentType.KVP, DocumentType.LINK, DocumentType.LINKANCHOR].includes(doc.type as any) && + ![DocumentType.CONFIG, DocumentType.KVP, DocumentType.LINK, DocumentType.LINKANCHOR].includes(doc.type as any) && !doc.isFolder && !doc.annotationOn ) { @@ -282,25 +321,20 @@ export class Doc extends RefField { set: setter, get: getter, // getPrototypeOf: (target) => Cast(target[SelfProxy].proto, Doc) || null, // TODO this might be able to replace the proto logic in getter - has: (target, key) => GetEffectiveAcl(target) !== AclPrivate && key in target.__fields, + has: (target, key) => GetEffectiveAcl(target) !== AclPrivate && key in target.__fieldTuples, ownKeys: target => { - const obj = {} as any; - if (GetEffectiveAcl(target) !== AclPrivate) Object.assign(obj, target.___fieldKeys); - runInAction(() => (obj.__LAYOUT__ = target.__LAYOUT__)); - return Object.keys(obj); + const keys = GetEffectiveAcl(target) !== AclPrivate ? Object.keys(target[FieldKeys]) : []; + return [...keys, '__LAYOUT__']; }, getOwnPropertyDescriptor: (target, prop) => { - if (prop.toString() === '__LAYOUT__') { + if (prop.toString() === '__LAYOUT__' || !(prop in target[FieldKeys])) { return Reflect.getOwnPropertyDescriptor(target, prop); } - if (prop in target.__fieldKeys) { - return { - configurable: true, //TODO Should configurable be true? - enumerable: true, - value: 0, //() => target.__fields[prop]) - }; - } - return Reflect.getOwnPropertyDescriptor(target, prop); + return { + configurable: true, //TODO Should configurable be true? + enumerable: true, + value: 0, //() => target.__fieldTuples[prop]) + }; }, deleteProperty: deleteProperty, defineProperty: () => { @@ -314,41 +348,35 @@ export class Doc extends RefField { return docProxy; } - proto: Opt<Doc>; [key: string]: FieldResult; @serializable(alias('fields', map(autoObject(), { afterDeserialize: afterDocDeserialize }))) - private get __fields() { - return this.___fields; + private get __fieldTuples() { + return this[FieldTuples]; } - private set __fields(value) { - this.___fields = value; + private set __fieldTuples(value) { + // called by deserializer to set all fields in one shot + this[FieldTuples] = value; for (const key in value) { const field = value[key]; - field !== undefined && (this.__fieldKeys[key] = true); + field !== undefined && (this[FieldKeys][key] = true); if (!(field instanceof ObjectField)) continue; field[Parent] = this[Self]; field[OnUpdate] = updateFunction(this[Self], key, field, this[SelfProxy]); } } - private get __fieldKeys() { - return this.___fieldKeys; - } - private set __fieldKeys(value) { - this.___fieldKeys = value; - } - @observable private ___fields: any = {}; - @observable private ___fieldKeys: any = {}; + @observable private [FieldTuples]: any = {}; + @observable private [FieldKeys]: any = {}; /// all of the raw acl's that have been set on this document. Use GetEffectiveAcl to determine the actual ACL of the doc for editing - @observable public [AclSym]: { [key: string]: symbol } = {}; - @observable public [CssSym]: number = 0; // incrementer denoting a change to CSS layout - @observable public [DirectLinksSym]: Set<Doc> = new Set(); - @observable public [AnimationSym]: Opt<Doc>; - @observable public [HighlightSym]: boolean = false; + @observable public [DocAcl]: { [key: string]: symbol } = {}; + @observable public [DocCss]: number = 0; // incrementer denoting a change to CSS layout + @observable public [DirectLinks]: Set<Doc> = new Set(); + @observable public [Animation]: Opt<Doc>; + @observable public [Highlight]: boolean = false; static __Anim(Doc: Doc) { // for debugging to print AnimationSym field easily. - return Doc[AnimationSym]; + return Doc[Animation]; } private [UpdatingFromServer]: boolean = false; @@ -361,15 +389,15 @@ export class Doc extends RefField { private [Self] = this; private [SelfProxy]: any; - public [FieldsSym] = () => this[Self].___fields; // Object.keys(this).reduce((fields, key) => { fields[key] = this[key]; return fields; }, {} as any); - public [WidthSym] = () => NumCast(this[SelfProxy]._width); - public [HeightSym] = () => NumCast(this[SelfProxy]._height); + public [DocFields] = () => this[Self][FieldTuples]; // Object.keys(this).reduce((fields, key) => { fields[key] = this[key]; return fields; }, {} as any); + public [Width] = () => NumCast(this[SelfProxy]._width); + public [Height] = () => NumCast(this[SelfProxy]._height); public [ToScriptString] = () => `idToDoc("${this[Self][Id]}")`; public [ToString] = () => `Doc(${GetEffectiveAcl(this[SelfProxy]) === AclPrivate ? '-inaccessible-' : this[SelfProxy].title})`; - public get [LayoutSym]() { + public get [DocLayout]() { return this[SelfProxy].__LAYOUT__; } - public get [DataSym]() { + public get [DocData](): Doc { const self = this[SelfProxy]; return self.resolvedDataDoc && !self.isTemplateForField ? self : Doc.GetProto(Cast(Doc.Layout(self).resolvedDataDoc, Doc, null) || self); } @@ -377,7 +405,7 @@ export class Doc extends RefField { const templateLayoutDoc = Cast(Doc.LayoutField(this[SelfProxy]), Doc, null); if (templateLayoutDoc) { let renderFieldKey: any; - const layoutField = templateLayoutDoc[StrCast(templateLayoutDoc.layoutKey, 'layout')]; + const layoutField = templateLayoutDoc[StrCast(templateLayoutDoc.layout_fieldKey, 'layout')]; if (typeof layoutField === 'string') { renderFieldKey = layoutField.split("fieldKey={'")[1].split("'")[0]; //layoutField.split("'")[1]; } else { @@ -506,14 +534,14 @@ export namespace Doc { export function GetT<T extends Field>(doc: Doc, key: string, ctor: ToConstructor<T>, ignoreProto: boolean = false): FieldResult<T> { return Cast(Get(doc, key, ignoreProto), ctor) as FieldResult<T>; } - export function IsPrototype(doc: Doc) { - return GetT(doc, 'isPrototype', 'boolean', true); + export function IsDataProto(doc: Doc) { + return GetT(doc, 'isDataDoc', 'boolean', true); } export function IsBaseProto(doc: Doc) { - return GetT(doc, 'baseProto', 'boolean', true); + return GetT(doc, 'isBaseProto', 'boolean', true); } export function IsSystem(doc: Doc) { - return GetT(doc, 'system', 'boolean', true); + return GetT(doc, 'isSystem', 'boolean', true); } export function IsDelegateField(doc: Doc, fieldKey: string) { return doc && Get(doc, fieldKey, true) !== undefined; @@ -522,17 +550,17 @@ export namespace Doc { if (key.startsWith('_')) key = key.substring(1); const hasProto = Doc.GetProto(doc) !== doc ? Doc.GetProto(doc) : undefined; const onDeleg = Object.getOwnPropertyNames(doc).indexOf(key) !== -1; - const onProto = hasProto && Object.getOwnPropertyNames(doc.proto).indexOf(key) !== -1; + const onProto = hasProto && Object.getOwnPropertyNames(hasProto).indexOf(key) !== -1; if (onDeleg || !hasProto || (!onProto && !defaultProto)) { doc[key] = value; - } else doc.proto![key] = value; + } else hasProto[key] = value; } export function GetAllPrototypes(doc: Doc): Doc[] { const protos: Doc[] = []; let d: Opt<Doc> = doc; while (d) { protos.push(d); - d = FieldValue(d.proto); + d = DocCast(FieldValue(d.proto)); } return protos; } @@ -570,9 +598,9 @@ export namespace Doc { // Gets the data document for the document. Note: this is mis-named -- it does not specifically // return the doc's proto, but rather recursively searches through the proto inheritance chain - // and returns the document who's proto is undefined or whose proto is marked as a base prototype ('isPrototype'). + // and returns the document who's proto is undefined or whose proto is marked as a data doc ('isDataDoc'). export function GetProto(doc: Doc): Doc { - const proto = doc && (Doc.GetT(doc, 'isPrototype', 'boolean', true) ? doc : doc.proto || doc); + const proto = doc && (Doc.GetT(doc, 'isDataDoc', 'boolean', true) ? doc : DocCast(doc.proto, doc)); return proto === doc ? proto : Doc.GetProto(proto); } export function GetDataDoc(doc: Doc): Doc { @@ -586,7 +614,7 @@ export namespace Doc { let proto: Doc | undefined = doc; while (proto) { Object.keys(proto).forEach(key => results.add(key)); - proto = proto.proto; + proto = DocCast(FieldValue(proto.proto)); } return Array.from(results); @@ -663,7 +691,7 @@ export namespace Doc { const bounds = docList.reduce( (bounds, doc) => { const [sptX, sptY] = [NumCast(doc.x), NumCast(doc.y)]; - const [bptX, bptY] = [sptX + doc[WidthSym](), sptY + doc[HeightSym]()]; + const [bptX, bptY] = [sptX + doc[Width](), sptY + doc[Height]()]; return { x: Math.min(sptX, bounds.x), y: Math.min(sptY, bounds.y), @@ -676,25 +704,25 @@ export namespace Doc { return bounds; } - export function MakeAlias(doc: Doc, id?: string) { - const alias = !GetT(doc, 'isPrototype', 'boolean', true) && doc.proto ? Doc.MakeCopy(doc, undefined, id) : Doc.MakeDelegate(doc, id); - const layout = Doc.LayoutField(alias); - if (layout instanceof Doc && layout !== alias && layout === Doc.Layout(alias)) { - Doc.SetLayout(alias, Doc.MakeAlias(layout)); + export function MakeEmbedding(doc: Doc, id?: string) { + const embedding = !GetT(doc, 'isDataDoc', 'boolean', true) && doc.proto ? Doc.MakeCopy(doc, undefined, id) : Doc.MakeDelegate(doc, id); + const layout = Doc.LayoutField(embedding); + if (layout instanceof Doc && layout !== embedding && layout === Doc.Layout(embedding)) { + Doc.SetLayout(embedding, Doc.MakeEmbedding(layout)); } - alias.aliasOf = doc; - alias.aliasNumber = Doc.GetProto(doc).aliasNumber = NumCast(Doc.GetProto(doc).aliasNumber) + 1; - alias.title = ComputedField.MakeFunction(`renameAlias(this)`); - alias.author = Doc.CurrentUserEmail; + embedding.createdFrom = doc; + embedding.proto_embeddingId = Doc.GetProto(doc).proto_embeddingId = NumCast(Doc.GetProto(doc).proto_embeddingId) + 1; + embedding.title = ComputedField.MakeFunction(`renameEmbedding(this)`); + embedding.author = Doc.CurrentUserEmail; - Doc.AddDocToList(Doc.GetProto(doc)[DataSym], 'aliases', alias); + Doc.AddDocToList(Doc.GetProto(doc)[DocData], 'proto_embeddings', embedding); - return alias; + return embedding; } - export function BestAlias(doc: Doc) { - const bestAlias = Doc.GetProto(doc) ? DocListCast(doc.aliases).find(doc => !doc.context && doc.author === Doc.CurrentUserEmail) : doc; - return bestAlias ?? Doc.MakeAlias(doc); + export function BestEmbedding(doc: Doc) { + const bestEmbedding = Doc.GetProto(doc) ? DocListCast(doc.proto_embeddings).find(doc => !doc.embedContainer && doc.author === Doc.CurrentUserEmail) : doc; + return bestEmbedding ?? Doc.MakeEmbedding(doc); } // this lists out all the tag ids that can be in a RichTextField that might contain document ids. @@ -741,7 +769,7 @@ export namespace Doc { if (docAtKey instanceof Doc) { if (pruneDocs.includes(docAtKey)) { // prune doc and do nothing - } else if (!Doc.IsSystem(docAtKey) && (key.startsWith('layout') || ['context', 'annotationOn', 'proto'].includes(key) || ((key === 'anchor1' || key === 'anchor2') && doc.author === Doc.CurrentUserEmail))) { + } else if (!Doc.IsSystem(docAtKey) && (key.startsWith('layout') || ['embedContainer', 'annotationOn', 'proto'].includes(key) || ((key === 'link_anchor_1' || key === 'link_anchor_2') && doc.author === Doc.CurrentUserEmail))) { assignKey(await Doc.makeClone(docAtKey, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks)); } else { assignKey(docAtKey); @@ -759,10 +787,11 @@ export namespace Doc { } }) ); - Array.from(doc[DirectLinksSym]).forEach(async link => { + Array.from(doc[DirectLinks]).forEach(async link => { if ( cloneLinks || - ((cloneMap.has(DocCast(link.anchor1)?.[Id]) || cloneMap.has(DocCast(DocCast(link.anchor1)?.annotationOn)?.[Id])) && (cloneMap.has(DocCast(link.anchor2)?.[Id]) || cloneMap.has(DocCast(DocCast(link.anchor2)?.annotationOn)?.[Id]))) + ((cloneMap.has(DocCast(link.link_anchor_1)?.[Id]) || cloneMap.has(DocCast(DocCast(link.link_anchor_1)?.annotationOn)?.[Id])) && + (cloneMap.has(DocCast(link.link_anchor_2)?.[Id]) || cloneMap.has(DocCast(DocCast(link.link_anchor_2)?.annotationOn)?.[Id]))) ) { linkMap.set(link[Id], await Doc.makeClone(link, cloneMap, linkMap, rtfs, exclusions, pruneDocs, cloneLinks)); } @@ -800,7 +829,7 @@ export namespace Doc { export async function MakeClone(doc: Doc, cloneLinks = true, cloneMap: Map<string, Doc> = new Map()) { const linkMap = new Map<string, Doc>(); const rtfMap: { copy: Doc; key: string; field: RichTextField }[] = []; - const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf'], doc.context ? [DocCast(doc.context)] : [], cloneLinks); + const copy = await Doc.makeClone(doc, cloneMap, linkMap, rtfMap, ['cloneOf'], doc.embedContainer ? [DocCast(doc.embedContainer)] : [], cloneLinks); const repaired = new Set<Doc>(); const linkedDocs = Array.from(linkMap.values()); linkedDocs.map((link: Doc) => LinkManager.Instance.addLink(link, true)); @@ -916,7 +945,7 @@ export namespace Doc { if (templateLayoutDoc.resolvedDataDoc === (targetDoc.rootDocument || Doc.GetProto(targetDoc))) { expandedTemplateLayout = templateLayoutDoc; // reuse an existing template layout if its for the same document with the same params } else { - templateLayoutDoc.resolvedDataDoc && (templateLayoutDoc = Cast(templateLayoutDoc.proto, Doc, null) || templateLayoutDoc); // if the template has already been applied (ie, a nested template), then use the template's prototype + templateLayoutDoc.resolvedDataDoc && (templateLayoutDoc = DocCast(templateLayoutDoc.proto, templateLayoutDoc)); // if the template has already been applied (ie, a nested template), then use the template's prototype if (!targetDoc[expandedLayoutFieldKey]) { _pendingMap.set(targetDoc[Id] + expandedLayoutFieldKey, true); setTimeout( @@ -955,7 +984,7 @@ export namespace Doc { const field = ProxyField.WithoutProxy(() => doc[key]); if (key === 'proto' && copyProto) { if (doc.proto instanceof Doc && overwrite.proto instanceof Doc) { - overwrite[key] = Doc.Overwrite(doc[key]!, overwrite.proto); + overwrite[key] = Doc.Overwrite(doc.proto, overwrite.proto); } } else { if (field instanceof RefField) { @@ -976,41 +1005,40 @@ export namespace Doc { export function MakeCopy(doc: Doc, copyProto: boolean = false, copyProtoId?: string, retitle = false): Doc { const copy = new Doc(copyProtoId, true); updateCachedAcls(copy); - const exclude = Cast(doc.cloneFieldFilter, listSpec('string'), []); - Object.keys(doc).forEach(key => { - if (exclude.includes(key)) return; - const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); - const field = key === 'author' ? Doc.CurrentUserEmail : ProxyField.WithoutProxy(() => doc[key]); - if (key === 'proto' && copyProto) { - if (doc[key] instanceof Doc) { - copy[key] = Doc.MakeCopy(doc[key]!, false); - } - } else { - if (field instanceof RefField) { - copy[key] = field; - } else if (cfield instanceof ComputedField) { - copy[key] = cfield[Copy](); // ComputedField.MakeFunction(cfield.script.originalScript); - } else if (field instanceof ObjectField) { - copy[key] = - doc[key] instanceof Doc - ? key.includes('layout[') - ? undefined - : doc[key] // reference documents except remove documents that are expanded teplate fields - : ObjectField.MakeCopy(field); - } else if (field instanceof Promise) { - debugger; //This shouldn't happend... + const exclude = [...StrListCast(doc.cloneFieldFilter), 'dragFactory_count', 'cloneFieldFilter']; + Object.keys(doc) + .filter(key => !exclude.includes(key)) + .forEach(key => { + if (key === 'proto' && copyProto) { + if (doc.proto instanceof Doc) { + copy[key] = Doc.MakeCopy(doc.proto, false); + } } else { - copy[key] = field; + const cfield = ComputedField.WithoutComputed(() => FieldValue(doc[key])); + const field = key === 'author' ? Doc.CurrentUserEmail : ProxyField.WithoutProxy(() => doc[key]); + if (field instanceof RefField) { + copy[key] = field; + } else if (cfield instanceof ComputedField) { + copy[key] = cfield[Copy](); // ComputedField.MakeFunction(cfield.script.originalScript); + } else if (field instanceof ObjectField) { + copy[key] = + doc[key] instanceof Doc && key.includes('layout[') + ? undefined // remove expanded template field documents + : ObjectField.MakeCopy(field); + } else if (field instanceof Promise) { + debugger; //This shouldn't happend... + } else { + copy[key] = field; + } } - } - }); + }); if (copyProto) { - Doc.GetProto(copy).context = undefined; - Doc.GetProto(copy).aliases = new List<Doc>([copy]); + Doc.GetProto(copy).embedContainer = undefined; + Doc.GetProto(copy).proto_embeddings = new List<Doc>([copy]); } else { - Doc.AddDocToList(Doc.GetProto(copy)[DataSym], 'aliases', copy); + Doc.AddDocToList(Doc.GetProto(copy)[DocData], 'proto_embeddings', copy); } - copy.context = undefined; + copy.embedContainer = undefined; Doc.defaultAclPrivate && (copy['acl-Public'] = 'Not Shared'); if (retitle) { copy.title = incrementTitleCopy(StrCast(copy.title)); @@ -1031,7 +1059,7 @@ export namespace Doc { Object.keys(doc) .filter(key => key.startsWith('acl')) .forEach(key => (delegate[key] = doc[key])); - if (!Doc.IsSystem(doc)) Doc.AddDocToList(doc[DataSym], 'aliases', delegate); + if (!Doc.IsSystem(doc)) Doc.AddDocToList(doc[DocData], 'proto_embeddings', delegate); title && (delegate.title = title); delegate[Initializing] = false; Doc.AddFileOrphan(delegate); @@ -1049,13 +1077,13 @@ export namespace Doc { delegateProto[Initializing] = true; delegateProto.proto = doc; delegateProto.author = Doc.CurrentUserEmail; - delegateProto.isPrototype = true; + delegateProto.isDataDoc = true; title && (delegateProto.title = title); const delegate = new Doc(id, true); delegate[Initializing] = true; delegate.proto = delegateProto; delegate.author = Doc.CurrentUserEmail; - Doc.AddDocToList(delegateProto[DataSym], 'aliases', delegate); + Doc.AddDocToList(delegateProto[DocData], 'proto_embeddings', delegate); delegate[Initializing] = false; delegateProto[Initializing] = false; return delegate; @@ -1067,9 +1095,9 @@ export namespace Doc { const proto = new Doc(); proto.author = Doc.CurrentUserEmail; const target = Doc.MakeDelegate(proto); - const targetKey = StrCast(templateDoc.layoutKey, 'layout'); + const targetKey = StrCast(templateDoc.layout_fieldKey, 'layout'); const applied = ApplyTemplateTo(templateDoc, target, targetKey, templateDoc.title + '(...' + _applyCount++ + ')'); - target.layoutKey = targetKey; + target.layout_fieldKey = targetKey; applied && (Doc.GetProto(applied).type = templateDoc.type); Doc.defaultAclPrivate && (applied['acl-Public'] = 'Not Shared'); return applied; @@ -1159,7 +1187,7 @@ export namespace Doc { } export const brushManager = new DocBrush(); - export class DocData { + export class UserDocData { @observable _user_doc: Doc = undefined!; @observable _sharing_doc: Doc = undefined!; @observable _searchQuery: string = ''; @@ -1169,37 +1197,37 @@ export namespace Doc { // a layout field or 'layout' is given. export function Layout(doc: Doc, layout?: Doc): Doc { const overrideLayout = layout && Cast(doc[`${StrCast(layout.isTemplateForField, 'data')}-layout[` + layout[Id] + ']'], Doc, null); - return overrideLayout || doc[LayoutSym] || doc; + return overrideLayout || doc[DocLayout] || doc; } export function SetLayout(doc: Doc, layout: Doc | string) { - doc[StrCast(doc.layoutKey, 'layout')] = layout; + doc[StrCast(doc.layout_fieldKey, 'layout')] = layout; } export function LayoutField(doc: Doc) { - return doc[StrCast(doc.layoutKey, 'layout')]; + return doc[StrCast(doc.layout_fieldKey, 'layout')]; } export function LayoutFieldKey(doc: Doc): string { - return StrCast(Doc.Layout(doc).layout).split("'")[1]; // bcz: TODO check on this . used to always reference 'layout', now it uses the layout speicfied by the current layoutKey + return StrCast(Doc.Layout(doc).layout).split("'")[1]; // bcz: TODO check on this . used to always reference 'layout', now it uses the layout speicfied by the current layout_fieldKey } export function NativeAspect(doc: Doc, dataDoc?: Doc, useDim?: boolean) { return Doc.NativeWidth(doc, dataDoc, useDim) / (Doc.NativeHeight(doc, dataDoc, useDim) || 1); } export function NativeWidth(doc?: Doc, dataDoc?: Doc, useWidth?: boolean) { - return !doc ? 0 : NumCast(doc._nativeWidth, NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + '-nativeWidth'], useWidth ? doc[WidthSym]() : 0)); + return !doc ? 0 : NumCast(doc._nativeWidth, NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + '_nativeWidth'], useWidth ? doc[Width]() : 0)); } export function NativeHeight(doc?: Doc, dataDoc?: Doc, useHeight?: boolean) { if (!doc) return 0; - const nheight = (Doc.NativeWidth(doc, dataDoc, useHeight) * doc[HeightSym]()) / doc[WidthSym](); - const dheight = NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + '-nativeHeight'], useHeight ? doc[HeightSym]() : 0); + const nheight = (Doc.NativeWidth(doc, dataDoc, useHeight) * doc[Height]()) / doc[Width](); + const dheight = NumCast((dataDoc || doc)[Doc.LayoutFieldKey(doc) + '_nativeHeight'], useHeight ? doc[Height]() : 0); return NumCast(doc._nativeHeight, nheight || dheight); } export function SetNativeWidth(doc: Doc, width: number | undefined, fieldKey?: string) { - doc[(fieldKey ?? Doc.LayoutFieldKey(doc)) + '-nativeWidth'] = width; + doc[(fieldKey ?? Doc.LayoutFieldKey(doc)) + '_nativeWidth'] = width; } export function SetNativeHeight(doc: Doc, height: number | undefined, fieldKey?: string) { - doc[(fieldKey ?? Doc.LayoutFieldKey(doc)) + '-nativeHeight'] = height; + doc[(fieldKey ?? Doc.LayoutFieldKey(doc)) + '_nativeHeight'] = height; } - const manager = new DocData(); + const manager = new UserDocData(); export function SearchQuery(): string { return manager._searchQuery; } @@ -1269,8 +1297,8 @@ export namespace Doc { const lastBrushed = Array.from(brushManager.BrushedDoc.keys()).lastElement(); if (lastBrushed) { for (const link of LinkManager.Instance.getAllDirectLinks(lastBrushed)) { - const a1 = Cast(link.anchor1, Doc, null); - const a2 = Cast(link.anchor2, Doc, null); + const a1 = Cast(link.link_anchor_1, Doc, null); + const a2 = Cast(link.link_anchor_2, Doc, null); if (Doc.AreProtosEqual(a1, doc) || Doc.AreProtosEqual(a2, doc) || Doc.AreProtosEqual(Cast(a1.annotationOn, Doc, null), doc) || Doc.AreProtosEqual(Cast(a2.annotationOn, Doc, null), doc)) { return DocBrushStatus.linkHighlighted; } @@ -1302,8 +1330,8 @@ export namespace Doc { } export function LinkEndpoint(linkDoc: Doc, anchorDoc: Doc) { - if (linkDoc.anchor2 === anchorDoc || (linkDoc.anchor2 as Doc).annotationOn) return '2'; - return Doc.AreProtosEqual(anchorDoc, (linkDoc.anchor1 as Doc).annotationOn as Doc) || Doc.AreProtosEqual(anchorDoc, linkDoc.anchor1 as Doc) ? '1' : '2'; + if (linkDoc.link_anchor_2 === anchorDoc || (linkDoc.link_anchor_2 as Doc).annotationOn) return '2'; + return Doc.AreProtosEqual(anchorDoc, (linkDoc.link_anchor_1 as Doc).annotationOn as Doc) || Doc.AreProtosEqual(anchorDoc, linkDoc.link_anchor_1 as Doc) ? '1' : '2'; } export function linkFollowUnhighlight() { @@ -1336,16 +1364,16 @@ export namespace Doc { export var highlightedDocs = new ObservableSet<Doc>(); export function IsHighlighted(doc: Doc) { if (!doc || GetEffectiveAcl(doc) === AclPrivate || GetEffectiveAcl(Doc.GetProto(doc)) === AclPrivate || doc.opacity === 0) return false; - return doc[HighlightSym] || Doc.GetProto(doc)[HighlightSym]; + return doc[Highlight] || Doc.GetProto(doc)[Highlight]; } export function HighlightDoc(doc: Doc, dataAndDisplayDocs = true, presEffect?: Doc) { runInAction(() => { highlightedDocs.add(doc); - doc[HighlightSym] = true; - doc[AnimationSym] = presEffect; + doc[Highlight] = true; + doc[Animation] = presEffect; if (dataAndDisplayDocs) { highlightedDocs.add(Doc.GetProto(doc)); - Doc.GetProto(doc)[HighlightSym] = true; + Doc.GetProto(doc)[Highlight] = true; } }); } @@ -1355,8 +1383,8 @@ export namespace Doc { (doc ? [doc] : Array.from(highlightedDocs)).forEach(doc => { highlightedDocs.delete(doc); highlightedDocs.delete(Doc.GetProto(doc)); - doc[HighlightSym] = Doc.GetProto(doc)[HighlightSym] = false; - doc[AnimationSym] = undefined; + doc[Highlight] = Doc.GetProto(doc)[Highlight] = false; + doc[Animation] = undefined; }); }); } @@ -1398,29 +1426,29 @@ export namespace Doc { } export function deiconifyView(doc: Doc) { - StrCast(doc.layoutKey).split('_')[1] === 'icon' && setNativeView(doc); + StrCast(doc.layout_fieldKey).split('_')[1] === 'icon' && setNativeView(doc); } export function setNativeView(doc: any) { - const prevLayout = StrCast(doc.layoutKey).split('_')[1]; + const prevLayout = StrCast(doc.layout_fieldKey).split('_')[1]; const deiconify = prevLayout === 'icon' && StrCast(doc.deiconifyLayout) ? 'layout_' + StrCast(doc.deiconifyLayout) : ''; prevLayout === 'icon' && (doc.deiconifyLayout = undefined); - doc.layoutKey = deiconify || 'layout'; + doc.layout_fieldKey = deiconify || 'layout'; } export function setDocRangeFilter(container: Opt<Doc>, key: string, range?: number[]) { if (!container) return; - const docRangeFilters = Cast(container._docRangeFilters, listSpec('string'), []); - for (let i = 0; i < docRangeFilters.length; i += 3) { - if (docRangeFilters[i] === key) { - docRangeFilters.splice(i, 3); + const childFiltersByRanges = Cast(container._childFiltersByRanges, listSpec('string'), []); + for (let i = 0; i < childFiltersByRanges.length; i += 3) { + if (childFiltersByRanges[i] === key) { + childFiltersByRanges.splice(i, 3); break; } } if (range !== undefined) { - docRangeFilters.push(key); - docRangeFilters.push(range[0].toString()); - docRangeFilters.push(range[1].toString()); - container._docRangeFilters = new List<string>(docRangeFilters); + childFiltersByRanges.push(key); + childFiltersByRanges.push(range[0].toString()); + childFiltersByRanges.push(range[1].toString()); + container._childFiltersByRanges = new List<string>(childFiltersByRanges); } } @@ -1429,35 +1457,35 @@ export namespace Doc { // based on the modifiers :"check", "x", undefined export function setDocFilter(container: Opt<Doc>, key: string, value: any, modifiers: 'remove' | 'match' | 'check' | 'x' | 'exists' | 'unset', toggle?: boolean, fieldPrefix?: string, append: boolean = true) { if (!container) return; - const filterField = '_' + (fieldPrefix ? fieldPrefix + '-' : '') + 'docFilters'; - const docFilters = Cast(container[filterField], listSpec('string'), []); + const filterField = '_' + (fieldPrefix ? fieldPrefix + '_' : '') + 'childFilters'; + const childFilters = StrListCast(container[filterField]); runInAction(() => { - for (let i = 0; i < docFilters.length; i++) { - const fields = docFilters[i].split(':'); // split key:value:modifier + for (let i = 0; i < childFilters.length; i++) { + const fields = childFilters[i].split(':'); // split key:value:modifier if (fields[0] === key && (fields[1] === value || modifiers === 'match' || (fields[2] === 'match' && modifiers === 'remove'))) { if (fields[2] === modifiers && modifiers && fields[1] === value) { if (toggle) modifiers = 'remove'; else return; } - docFilters.splice(i, 1); - container[filterField] = new List<string>(docFilters); + childFilters.splice(i, 1); + container[filterField] = new List<string>(childFilters); break; } } - if (!docFilters.length && modifiers === 'match' && value === undefined) { + if (!childFilters.length && modifiers === 'match' && value === undefined) { container[filterField] = undefined; } else if (modifiers !== 'remove') { - !append && (docFilters.length = 0); - docFilters.push(key + ':' + value + ':' + modifiers); - container[filterField] = new List<string>(docFilters); + !append && (childFilters.length = 0); + childFilters.push(key + ':' + value + ':' + modifiers); + container[filterField] = new List<string>(childFilters); } }); } export function readDocRangeFilter(doc: Doc, key: string) { - const docRangeFilters = Cast(doc._docRangeFilters, listSpec('string'), []); - for (let i = 0; i < docRangeFilters.length; i += 3) { - if (docRangeFilters[i] === key) { - return [Number(docRangeFilters[i + 1]), Number(docRangeFilters[i + 2])]; + const childFiltersByRanges = Cast(doc._childFiltersByRanges, listSpec('string'), []); + for (let i = 0; i < childFiltersByRanges.length; i += 3) { + if (childFiltersByRanges[i] === key) { + return [Number(childFiltersByRanges[i + 1]), Number(childFiltersByRanges[i + 2])]; } } } @@ -1469,18 +1497,18 @@ export namespace Doc { export function toggleNativeDimensions(layoutDoc: Doc, contentScale: number, panelWidth: number, panelHeight: number) { runInAction(() => { if (Doc.NativeWidth(layoutDoc) || Doc.NativeHeight(layoutDoc)) { - layoutDoc._viewScale = NumCast(layoutDoc._viewScale, 1) * contentScale; + layoutDoc._freeform_scale = NumCast(layoutDoc._freeform_scale, 1) * contentScale; layoutDoc._nativeWidth = undefined; layoutDoc._nativeHeight = undefined; - layoutDoc._forceReflow = undefined; + layoutDoc._layout_forceReflow = undefined; layoutDoc._nativeHeightUnfrozen = undefined; layoutDoc._nativeDimModifiable = undefined; } else { - layoutDoc._autoHeight = false; + layoutDoc._layout_autoHeight = false; if (!Doc.NativeWidth(layoutDoc)) { layoutDoc._nativeWidth = NumCast(layoutDoc._width, panelWidth); layoutDoc._nativeHeight = NumCast(layoutDoc._height, panelHeight); - layoutDoc._forceReflow = true; + layoutDoc._layout_forceReflow = true; layoutDoc._nativeHeightUnfrozen = true; layoutDoc._nativeDimModifiable = true; } @@ -1508,15 +1536,33 @@ export namespace Doc { return style; } + export function Paste(docids: string[], clone: boolean, addDocument: (doc: Doc | Doc[]) => boolean, ptx?: number, pty?: number, newPoint?: number[]) { + DocServer.GetRefFields(docids).then(async fieldlist => { + const list = Array.from(Object.values(fieldlist)) + .map(d => DocCast(d)) + .filter(d => d); + const docs = clone ? (await Promise.all(Doc.MakeClones(list, false))).map(res => res.clone) : list; + if (ptx !== undefined && pty !== undefined && newPoint !== undefined) { + const firstx = list.length ? NumCast(list[0].x) + ptx - newPoint[0] : 0; + const firsty = list.length ? NumCast(list[0].y) + pty - newPoint[1] : 0; + docs.map(doc => { + doc.x = NumCast(doc.x) - firstx; + doc.y = NumCast(doc.y) - firsty; + }); + } + undoable(addDocument, 'Paste Doc')(docs); // embedContainer gets set in addDocument + }); + } + // prettier-ignore export function toIcon(doc?: Doc, isOpen?: boolean) { - switch (StrCast(doc?.type)) { + switch (isOpen !== undefined ? DocumentType.COL: StrCast(doc?.type)) { case DocumentType.IMG: return 'image'; case DocumentType.COMPARISON: return 'columns'; case DocumentType.RTF: return 'sticky-note'; case DocumentType.COL: - const folder: IconProp = isOpen ? 'folder-open' : 'folder'; - const chevron: IconProp = isOpen ? 'chevron-down' : 'chevron-right'; + const folder: IconProp = isOpen === true ? 'folder-open' : isOpen === false ? 'folder' : 'question'; + const chevron: IconProp = isOpen === true ? 'chevron-down' : isOpen === false ? 'chevron-right' : 'question'; return !doc?.isFolder ? folder : chevron; case DocumentType.WEB: return 'globe-asia'; case DocumentType.SCREENSHOT: return 'photo-video'; @@ -1708,8 +1754,8 @@ export function IdToDoc(id: string) { ScriptingGlobals.add(function idToDoc(id: string): any { return IdToDoc(id); }); -ScriptingGlobals.add(function renameAlias(doc: any) { - return StrCast(Doc.GetProto(doc).title).replace(/\([0-9]*\)/, '') + `(${doc.aliasNumber})`; +ScriptingGlobals.add(function renameEmbedding(doc: any) { + return StrCast(Doc.GetProto(doc).title).replace(/\([0-9]*\)/, '') + `(${doc.proto_embeddingId})`; }); ScriptingGlobals.add(function getProto(doc: any) { return Doc.GetProto(doc); @@ -1717,8 +1763,8 @@ ScriptingGlobals.add(function getProto(doc: any) { ScriptingGlobals.add(function getDocTemplate(doc?: any) { return Doc.getDocTemplate(doc); }); -ScriptingGlobals.add(function getAlias(doc: any) { - return Doc.MakeAlias(doc); +ScriptingGlobals.add(function getEmbedding(doc: any) { + return Doc.MakeEmbedding(doc); }); ScriptingGlobals.add(function getCopy(doc: any, copyProto: any) { return doc.isTemplateDoc ? Doc.ApplyTemplate(doc) : Doc.MakeCopy(doc, copyProto); @@ -1742,6 +1788,16 @@ ScriptingGlobals.add(function undo() { SelectionManager.DeselectAll(); return UndoManager.Undo(); }); + +export function ShowUndoStack() { + SelectionManager.DeselectAll(); + var buffer = ''; + UndoManager.undoStack.forEach((batch, i) => { + buffer += 'Batch => ' + UndoManager.undoStackNames[i] + '\n'; + ///batch.forEach(undo => (buffer += ' ' + undo.prop + '\n')); + }); + alert(buffer); +} ScriptingGlobals.add(function redo() { SelectionManager.DeselectAll(); return UndoManager.Redo(); diff --git a/src/fields/DocSymbols.ts b/src/fields/DocSymbols.ts new file mode 100644 index 000000000..65decc147 --- /dev/null +++ b/src/fields/DocSymbols.ts @@ -0,0 +1,26 @@ +export const Update = Symbol('DocUpdate'); +export const Self = Symbol('DocSelf'); +export const SelfProxy = Symbol('DocSelfProxy'); +export const FieldKeys = Symbol('DocFieldKeys'); +export const FieldTuples = Symbol('DocFieldTuples'); +export const Width = Symbol('DocWidth'); +export const Height = Symbol('DocHeight'); +export const Animation = Symbol('DocAnimation'); +export const Highlight = Symbol('DocHighlight'); +export const DocData = Symbol('DocData'); +export const DocLayout = Symbol('DocLayout'); +export const DocFields = Symbol('DocFields'); +export const DocCss = Symbol('DocCss'); +export const DocAcl = Symbol('DocAcl'); +export const DirectLinks = Symbol('DocDirectLinks'); +export const AclUnset = Symbol('DocAclUnset'); +export const AclPrivate = Symbol('DocAclOwnerOnly'); +export const AclReadonly = Symbol('DocAclReadOnly'); +export const AclAugment = Symbol('DocAclAugment'); +export const AclSelfEdit = Symbol('DocAclSelfEdit'); +export const AclEdit = Symbol('DocAclEdit'); +export const AclAdmin = Symbol('DocAclAdmin'); +export const UpdatingFromServer = Symbol('DocUpdatingFromServer'); +export const Initializing = Symbol('DocInitializing'); +export const ForceServerWrite = Symbol('DocForceServerWrite'); +export const CachedUpdates = Symbol('DocCachedUpdates'); diff --git a/src/fields/FieldSymbols.ts b/src/fields/FieldSymbols.ts index e50c2856f..c381f14f5 100644 --- a/src/fields/FieldSymbols.ts +++ b/src/fields/FieldSymbols.ts @@ -1,12 +1,9 @@ -export const Update = Symbol('Update'); -export const Self = Symbol('Self'); -export const SelfProxy = Symbol('SelfProxy'); -export const HandleUpdate = Symbol('HandleUpdate'); -export const Id = Symbol('Id'); -export const OnUpdate = Symbol('OnUpdate'); -export const Parent = Symbol('Parent'); -export const Copy = Symbol('Copy'); -export const ToValue = Symbol('ToValue'); -export const ToScriptString = Symbol('ToScriptString'); -export const ToPlainText = Symbol('ToPlainText'); -export const ToString = Symbol('ToString'); +export const HandleUpdate = Symbol('FieldHandleUpdate'); +export const Id = Symbol('FieldId'); +export const OnUpdate = Symbol('FieldOnUpdate'); +export const Parent = Symbol('FieldParent'); +export const Copy = Symbol('FieldCopy'); +export const ToValue = Symbol('FieldToValue'); +export const ToScriptString = Symbol('FieldToScriptString'); +export const ToPlainText = Symbol('FieldToPlainText'); +export const ToString = Symbol('FieldToString'); diff --git a/src/fields/List.ts b/src/fields/List.ts index dfd24cf7a..033fa569b 100644 --- a/src/fields/List.ts +++ b/src/fields/List.ts @@ -3,8 +3,9 @@ import { alias, list, serializable } from 'serializr'; import { DocServer } from '../client/DocServer'; import { ScriptingGlobals } from '../client/util/ScriptingGlobals'; import { afterDocDeserialize, autoObject, Deserializable } from '../client/util/SerializationHelper'; +import { FieldTuples, Self, SelfProxy, Update } from './DocSymbols'; import { Field } from './Doc'; -import { Copy, OnUpdate, Parent, Self, SelfProxy, ToScriptString, ToString, Update } from './FieldSymbols'; +import { Copy, OnUpdate, Parent, ToScriptString, ToString } from './FieldSymbols'; import { ObjectField } from './ObjectField'; import { ProxyField } from './Proxy'; import { RefField } from './RefField'; @@ -21,12 +22,12 @@ const listHandlers: any = { if (value instanceof RefField) { throw new Error('fill with RefFields not supported yet'); } - const res = this[Self].__fields.fill(value, start, end); + const res = this[Self].__fieldTuples.fill(value, start, end); this[Update](); return res; }, pop(): any { - const field = toRealField(this[Self].__fields.pop()); + const field = toRealField(this[Self].__fieldTuples.pop()); this[Update](); return field; }, @@ -34,7 +35,7 @@ const listHandlers: any = { items = items.map(toObjectField); const list = this[Self]; - const length = list.__fields.length; + const length = list.__fieldTuples.length; for (let i = 0; i < items.length; i++) { const item = items[i]; //TODO Error checking to make sure parent doesn't already exist @@ -43,23 +44,23 @@ const listHandlers: any = { item[OnUpdate] = updateFunction(list, i + length, item, this); } } - const res = list.__fields.push(...items); + const res = list.__fieldTuples.push(...items); this[Update]({ op: '$addToSet', items, length: length + items.length }); return res; }), reverse() { - const res = this[Self].__fields.reverse(); + const res = this[Self].__fieldTuples.reverse(); this[Update](); return res; }, shift() { - const res = toRealField(this[Self].__fields.shift()); + const res = toRealField(this[Self].__fieldTuples.shift()); this[Update](); return res; }, sort(cmpFunc: any) { this[Self].__realFields(); // coerce retrieving entire array - const res = this[Self].__fields.sort(cmpFunc ? (first: any, second: any) => cmpFunc(toRealField(first), toRealField(second)) : undefined); + const res = this[Self].__fieldTuples.sort(cmpFunc ? (first: any, second: any) => cmpFunc(toRealField(first), toRealField(second)) : undefined); this[Update](); return res; }, @@ -67,7 +68,7 @@ const listHandlers: any = { this[Self].__realFields(); // coerce retrieving entire array items = items.map(toObjectField); const list = this[Self]; - const removed = list.__fields.filter((item: any, i: number) => i >= start && i < start + deleteCount); + const removed = list.__fieldTuples.filter((item: any, i: number) => i >= start && i < start + deleteCount); for (let i = 0; i < items.length; i++) { const item = items[i]; //TODO Error checking to make sure parent doesn't already exist @@ -77,18 +78,18 @@ const listHandlers: any = { item[OnUpdate] = updateFunction(list, i + start, item, this); } } - let hintArray: {val : any, index : number}[] = []; - for(let i = start; i < start + deleteCount; i++) { - hintArray.push({val : list.__fields[i], index : i}); + let hintArray: { val: any; index: number }[] = []; + for (let i = start; i < start + deleteCount; i++) { + hintArray.push({ val: list.__fieldTuples[i], index: i }); } - const res = list.__fields.splice(start, deleteCount, ...items); + const res = list.__fieldTuples.splice(start, deleteCount, ...items); // the hint object sends the starting index of the slice and the number - // of elements to delete. + // of elements to delete. this[Update]( items.length === 0 && deleteCount - ? { op: '$remFromSet', items: removed, hint : { start : start, deleteCount : deleteCount }, length: list.__fields.length } - : items.length && !deleteCount && start === list.__fields.length - ? { op: '$addToSet', items, length: list.__fields.length } + ? { op: '$remFromSet', items: removed, hint: { start: start, deleteCount: deleteCount }, length: list.__fieldTuples.length } + : items.length && !deleteCount && start === list.__fieldTuples.length + ? { op: '$addToSet', items, length: list.__fieldTuples.length } : undefined ); return res.map(toRealField); @@ -96,7 +97,7 @@ const listHandlers: any = { unshift(...items: any[]) { items = items.map(toObjectField); const list = this[Self]; - const length = list.__fields.length; + const length = list.__fieldTuples.length; for (let i = 0; i < items.length; i++) { const item = items[i]; //TODO Error checking to make sure parent doesn't already exist @@ -106,32 +107,32 @@ const listHandlers: any = { item[OnUpdate] = updateFunction(list, i, item, this); } } - const res = this[Self].__fields.unshift(...items); + const res = this[Self].__fieldTuples.unshift(...items); this[Update](); return res; }, /// Accessor methods concat: action(function (this: any, ...items: any[]) { this[Self].__realFields(); - return this[Self].__fields.map(toRealField).concat(...items); + return this[Self].__fieldTuples.map(toRealField).concat(...items); }), includes(valueToFind: any, fromIndex: number) { if (valueToFind instanceof RefField) { return this[Self].__realFields().includes(valueToFind, fromIndex); } else { - return this[Self].__fields.includes(valueToFind, fromIndex); + return this[Self].__fieldTuples.includes(valueToFind, fromIndex); } }, indexOf(valueToFind: any, fromIndex: number) { if (valueToFind instanceof RefField) { return this[Self].__realFields().indexOf(valueToFind, fromIndex); } else { - return this[Self].__fields.indexOf(valueToFind, fromIndex); + return this[Self].__fieldTuples.indexOf(valueToFind, fromIndex); } }, join(separator: any) { this[Self].__realFields(); - return this[Self].__fields.map(toRealField).join(separator); + return this[Self].__fieldTuples.map(toRealField).join(separator); }, lastElement() { return this[Self].__realFields().lastElement(); @@ -140,12 +141,12 @@ const listHandlers: any = { if (valueToFind instanceof RefField) { return this[Self].__realFields().lastIndexOf(valueToFind, fromIndex); } else { - return this[Self].__fields.lastIndexOf(valueToFind, fromIndex); + return this[Self].__fieldTuples.lastIndexOf(valueToFind, fromIndex); } }, slice(begin: number, end: number) { this[Self].__realFields(); - return this[Self].__fields.slice(begin, end).map(toRealField); + return this[Self].__fieldTuples.slice(begin, end).map(toRealField); }, /// Iteration methods @@ -156,55 +157,55 @@ const listHandlers: any = { return this[Self].__realFields().every(callback, thisArg); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead - // return this[Self].__fields.every((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); + // return this[Self].__fieldTuples.every((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); }, filter(callback: any, thisArg: any) { return this[Self].__realFields().filter(callback, thisArg); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead - // return this[Self].__fields.filter((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); + // return this[Self].__fieldTuples.filter((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); }, find(callback: any, thisArg: any) { return this[Self].__realFields().find(callback, thisArg); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead - // return this[Self].__fields.find((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); + // return this[Self].__fieldTuples.find((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); }, findIndex(callback: any, thisArg: any) { return this[Self].__realFields().findIndex(callback, thisArg); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead - // return this[Self].__fields.findIndex((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); + // return this[Self].__fieldTuples.findIndex((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); }, forEach(callback: any, thisArg: any) { return this[Self].__realFields().forEach(callback, thisArg); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead - // return this[Self].__fields.forEach((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); + // return this[Self].__fieldTuples.forEach((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); }, map(callback: any, thisArg: any) { return this[Self].__realFields().map(callback, thisArg); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead - // return this[Self].__fields.map((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); + // return this[Self].__fieldTuples.map((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); }, reduce(callback: any, initialValue: any) { return this[Self].__realFields().reduce(callback, initialValue); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead - // return this[Self].__fields.reduce((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue); + // return this[Self].__fieldTuples.reduce((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue); }, reduceRight(callback: any, initialValue: any) { return this[Self].__realFields().reduceRight(callback, initialValue); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead - // return this[Self].__fields.reduceRight((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue); + // return this[Self].__fieldTuples.reduceRight((acc:any, element:any, index:number, array:any) => callback(acc, toRealField(element), index, array), initialValue); }, some(callback: any, thisArg: any) { return this[Self].__realFields().some(callback, thisArg); // TODO This is probably more efficient, but technically the callback can take the array, which would mean we would have to map the actual array anyway. // If we don't want to support the array parameter, we should use this version instead - // return this[Self].__fields.some((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); + // return this[Self].__fieldTuples.some((element:any, index:number, array:any) => callback(toRealField(element), index, array), thisArg); }, values() { return this[Self].__realFields().values(); @@ -246,7 +247,7 @@ type ListUpdate<T> = ListSpliceUpdate<T> | ListIndexUpdate<T>; type StoredType<T extends Field> = T extends RefField ? ProxyField<T> : T; -export const ListFieldName="fields"; +export const ListFieldName = 'fields'; @Deserializable('list') class ListImpl<T extends Field> extends ObjectField { constructor(fields?: T[]) { @@ -254,9 +255,9 @@ class ListImpl<T extends Field> extends ObjectField { const list = new Proxy<this>(this, { set: setter, get: listGetter, - ownKeys: target => Object.keys(target.__fields), + ownKeys: target => Object.keys(target.__fieldTuples), getOwnPropertyDescriptor: (target, prop) => { - if (prop in target.__fields) { + if (prop in target[FieldTuples]) { return { configurable: true, //TODO Should configurable be true? enumerable: true, @@ -281,7 +282,7 @@ class ListImpl<T extends Field> extends ObjectField { // this requests all ProxyFields at the same time to avoid the overhead // of separate network requests and separate updates to the React dom. private __realFields() { - const unrequested = this.__fields.filter(f => f instanceof ProxyField && f.needsRequesting).map(f => f as ProxyField<RefField>); + const unrequested = this[FieldTuples].filter(f => f instanceof ProxyField && f.needsRequesting).map(f => f as ProxyField<RefField>); // if we find any ProxyFields that don't have a current value, then // start the server request for all of them if (unrequested.length) { @@ -293,17 +294,16 @@ class ListImpl<T extends Field> extends ObjectField { // will await the batch request and return the requested field value. unrequested.forEach(p => p.setExternalValuePromise(allSetPromise)); } - return this.__fields.map(toRealField); + return this[FieldTuples].map(toRealField); } - public static FieldDataName = 'fields'; @serializable(alias(ListFieldName, list(autoObject(), { afterDeserialize: afterDocDeserialize }))) - private get __fields() { - return this.___fields; + private get __fieldTuples() { + return this[FieldTuples]; } - private set __fields(value) { - this.___fields = value; + private set __fieldTuples(value) { + this[FieldTuples] = value; for (const key in value) { const field = value[key]; if (field instanceof ObjectField) { @@ -314,14 +314,14 @@ class ListImpl<T extends Field> extends ObjectField { } [Copy]() { - const copiedData = this[Self].__fields.map(f => (f instanceof ObjectField ? f[Copy]() : f)); + const copiedData = this[Self].__fieldTuples.map(f => (f instanceof ObjectField ? f[Copy]() : f)); const deepCopy = new ListImpl<T>(copiedData as any); return deepCopy; } // @serializable(alias("fields", list(autoObject()))) @observable - private ___fields: StoredType<T>[] = []; + private [FieldTuples]: StoredType<T>[] = []; private [Update] = (diff: any) => { // console.log(diff); diff --git a/src/fields/RichTextUtils.ts b/src/fields/RichTextUtils.ts index 239b59e83..24cd078f2 100644 --- a/src/fields/RichTextUtils.ts +++ b/src/fields/RichTextUtils.ts @@ -395,11 +395,11 @@ export namespace RichTextUtils { if (new RegExp(window.location.origin + delimiter).test(url) && !url.endsWith(alreadyShared)) { const linkDoc = await DocServer.GetRefField(url.split(delimiter)[1]); if (linkDoc instanceof Doc) { - let exported = (await Cast(linkDoc.anchor2, Doc))!; + let exported = (await Cast(linkDoc.link_anchor_2, Doc))!; if (!exported.customLayout) { - exported = Doc.MakeAlias(exported); + exported = Doc.MakeEmbedding(exported); DocUtils.makeCustomViewClicked(exported, Docs.Create.FreeformDocument); - linkDoc.anchor2 = exported; + linkDoc.link_anchor_2 = exported; } url = Utils.shareUrl(exported[Id]); } diff --git a/src/fields/Schema.ts b/src/fields/Schema.ts index 7ad376a28..f035eeb0d 100644 --- a/src/fields/Schema.ts +++ b/src/fields/Schema.ts @@ -1,13 +1,13 @@ -import { Interface, ToInterface, Cast, ToConstructor, HasTail, Head, Tail, ListSpec, ToType, DefaultFieldConstructor } from "./Types"; -import { Doc, Field } from "./Doc"; -import { ObjectField } from "./ObjectField"; -import { RefField } from "./RefField"; -import { SelfProxy } from "./FieldSymbols"; -import { List } from "./List"; +import { Interface, ToInterface, Cast, ToConstructor, HasTail, Head, Tail, ListSpec, ToType, DefaultFieldConstructor } from './Types'; +import { Doc, Field } from './Doc'; +import { ObjectField } from './ObjectField'; +import { RefField } from './RefField'; +import { SelfProxy } from './DocSymbols'; +import { List } from './List'; type AllToInterface<T extends Interface[]> = { - 1: ToInterface<Head<T>> & AllToInterface<Tail<T>>, - 0: ToInterface<Head<T>> + 1: ToInterface<Head<T>> & AllToInterface<Tail<T>>; + 0: ToInterface<Head<T>>; }[HasTail<T> extends true ? 1 : 0]; export const emptySchema = createSchema({}); @@ -28,36 +28,40 @@ export function makeInterface<T extends Interface[]>(...schemas: T): InterfaceFu schema[key] = s[key]; } } - const proto = new Proxy({}, { - get(target: any, prop, receiver) { - const field = receiver.doc?.[prop]; - if (prop in schema) { - const desc = prop === "proto" ? Doc : (schema as any)[prop]; // bcz: proto doesn't appear in schemas ... maybe it should? - if (typeof desc === "object" && "defaultVal" in desc && "type" in desc) {//defaultSpec - return Cast(field, desc.type, desc.defaultVal); - } - if (typeof desc === "function" && !ObjectField.isPrototypeOf(desc) && !RefField.isPrototypeOf(desc)) { - const doc = Cast(field, Doc); - if (doc === undefined) { - return undefined; + const proto = new Proxy( + {}, + { + get(target: any, prop, receiver) { + const field = receiver.doc?.[prop]; + if (prop in schema) { + const desc = prop === 'proto' ? Doc : (schema as any)[prop]; // bcz: proto doesn't appear in schemas ... maybe it should? + if (typeof desc === 'object' && 'defaultVal' in desc && 'type' in desc) { + //defaultSpec + return Cast(field, desc.type, desc.defaultVal); } - if (doc instanceof Doc) { - return desc(doc); + if (typeof desc === 'function' && !ObjectField.isPrototypeOf(desc) && !RefField.isPrototypeOf(desc)) { + const doc = Cast(field, Doc); + if (doc === undefined) { + return undefined; + } + if (doc instanceof Doc) { + return desc(doc); + } + return doc.then(doc => doc && desc(doc)); } - return doc.then(doc => doc && desc(doc)); + return Cast(field, desc); } - return Cast(field, desc); - } - return field; - }, - set(target: any, prop, value, receiver) { - receiver.doc && (receiver.doc[prop] = value); // receiver.doc may be undefined as the result of a change in acls - return true; + return field; + }, + set(target: any, prop, value, receiver) { + receiver.doc && (receiver.doc[prop] = value); // receiver.doc may be undefined as the result of a change in acls + return true; + }, } - }); + ); // !(doc instanceof Doc) && (throw new Error("Currently wrapping a schema in another schema isn't supported")); const fn = (doc: Doc) => Object.create(proto, { doc: { value: doc[SelfProxy], writable: false } }); - return ((doc?: Doc | Doc[]) => (doc instanceof List ? doc : undefined)?.map?.(fn) ?? fn((doc as Doc) ?? new Doc)) as InterfaceFunc<T>; + return ((doc?: Doc | Doc[]) => (doc instanceof List ? doc : undefined)?.map?.(fn) ?? fn((doc as Doc) ?? new Doc())) as InterfaceFunc<T>; } export type makeStrictInterface<T extends Interface> = Partial<ToInterface<T>>; @@ -75,8 +79,8 @@ export function makeStrictInterface<T extends Interface>(schema: T): (doc: Doc) this.__doc[key] = value; return; } - throw new TypeError("Expected type " + type); - } + throw new TypeError('Expected type ' + type); + }, }); } return function (doc: any) { @@ -95,12 +99,12 @@ export function createSchema<T extends Interface>(schema: T): T & { proto: ToCon } export function listSpec<U extends ToConstructor<Field>>(type: U): ListSpec<ToType<U>> { - return { List: type as any };//TODO Types + return { List: type as any }; //TODO Types } export function defaultSpec<T extends ToConstructor<Field>>(type: T, defaultVal: ToType<T>): DefaultFieldConstructor<ToType<T>> { return { type: type as any, - defaultVal + defaultVal, }; -}
\ No newline at end of file +} diff --git a/src/fields/Types.ts b/src/fields/Types.ts index 4cf286a32..251b1149d 100644 --- a/src/fields/Types.ts +++ b/src/fields/Types.ts @@ -3,7 +3,7 @@ import { List } from './List'; import { RefField } from './RefField'; import { DateField } from './DateField'; import { ScriptField } from './ScriptField'; -import { URLField, WebField, ImageField } from './URLField'; +import { URLField, WebField, ImageField, CsvField } from './URLField'; import { TextField } from '@material-ui/core'; import { RichTextField } from './RichTextField'; @@ -104,6 +104,9 @@ export function RTFCast(field: FieldResult) { export function ScriptCast(field: FieldResult, defaultVal: ScriptField | null = null) { return Cast(field, ScriptField, defaultVal); } +export function CsvCast(field: FieldResult, defaultVal: CsvField | null = null) { + return Cast(field, CsvField, defaultVal); +} export function WebCast(field: FieldResult, defaultVal: WebField | null = null) { return Cast(field, WebField, defaultVal); } diff --git a/src/fields/documentSchemas.ts b/src/fields/documentSchemas.ts index b7fd06973..757d507be 100644 --- a/src/fields/documentSchemas.ts +++ b/src/fields/documentSchemas.ts @@ -9,15 +9,15 @@ export const documentSchema = createSchema({ type: 'string', // enumerated type of document -- should be template-specific (ie, start with an '_') title: 'string', // document title (can be on either data document or layout) isTemplateForField: 'string', // if specified, it indicates the document is a template that renders the specified field - creationDate: DateField, // when the document was created + author_date: DateField, // when the document was created links: listSpec(Doc), // computed (readonly) list of links associated with this document // "Location" properties in a very general sense - _curPage: 'number', // current page of a page based document + _layout_curPage: 'number', // current page of a page based document _currentFrame: 'number', // current frame of a frame based collection (e.g., a progressive slide) lastFrame: 'number', // last frame of a frame based collection (e.g., a progressive slide) activeFrame: 'number', // the active frame of a frame based animated document - _currentTimecode: 'number', // current play back time of a temporal document (video / audio) + _layout_currentTimecode: 'number', // current play back time of a temporal document (video / audio) _timecodeToShow: 'number', // the time that a document should be displayed (e.g., time an annotation should be displayed on a video) _timecodeToHIde: 'number', // the time that a document should be hidden markers: listSpec(Doc), // list of markers for audio / video @@ -25,52 +25,52 @@ export const documentSchema = createSchema({ y: 'number', // y coordinate when in a freeform view z: 'number', // z "coordinate" - non-zero specifies the overlay layer of a freeformview zIndex: 'number', // zIndex of a document in a freeform view - _scrollTop: 'number', // scroll position of a scrollable document (pdf, text, web) + _layout_scrollTop: 'number', // scroll position of a scrollable document (pdf, text, web) lat: 'number', lng: 'number', // appearance properties on the layout '_backgroundGrid-spacing': 'number', // the size of the grid for collection views - _autoHeight: 'boolean', // whether the height of the document should be computed automatically based on its contents + _layout_autoHeight: 'boolean', // whether the height of the document should be computed automatically based on its contents _nativeWidth: 'number', // native width of document which determines how much document contents are scaled when the document's width is set _nativeHeight: 'number', // " _width: 'number', // width of document in its container's coordinate system _height: 'number', // " - _xPadding: 'number', // pixels of padding on left/right of collectionfreeformview contents when fitContentsToBox is set - _yPadding: 'number', // pixels of padding on top/bottom of collectionfreeformview contents when fitContentsToBox is set + _xPadding: 'number', // pixels of padding on left/right of collectionfreeformview contents when freeform_fitContentsToBox is set + _yPadding: 'number', // pixels of padding on top/bottom of collectionfreeformview contents when freeform_fitContentsToBox is set _xMargin: 'number', // margin added on left/right of most documents to add separation from their container _yMargin: 'number', // margin added on top/bottom of most documents to add separation from their container _overflow: 'string', // sets overflow behvavior for CollectionFreeForm views - _showCaption: 'string', // whether editable caption text is overlayed at the bottom of the document - _showTitle: 'string', // the fieldkey(s) whose contents should be displayed at the top of the document. separate multiple keys with ";". Use :hover suffix to indicate title should be shown on hover + _layout_showCaption: 'string', // whether editable caption text is overlayed at the bottom of the document + _layout_showTitle: 'string', // the fieldkey(s) whose contents should be displayed at the top of the document. separate multiple keys with ";". Use :hover suffix to indicate title should be shown on hover _pivotField: 'string', // specifies which field key should be used as the timeline/pivot axis _columnsFill: 'boolean', // whether documents in a stacking view column should be sized to fill the column _columnsSort: 'string', // how a document should be sorted "ascending", "descending", undefined (none) _columnsHideIfEmpty: 'boolean', // whether empty stacking view column headings should be hidden - _columnHeaders: listSpec(SchemaHeaderField), // header descriptions for stacking/masonry - _schemaHeaders: listSpec(SchemaHeaderField), // header descriptions for schema views - _fontSize: 'string', - _fontFamily: 'string', - _sidebarWidthPercent: 'string', // percent of text window width taken up by sidebar + // _columnHeaders: listSpec(SchemaHeaderField), // header descriptions for stacking/masonry + // _schemaHeaders: listSpec(SchemaHeaderField), // header descriptions for schema views + _text_fontSize: 'string', + _text_fontFamily: 'string', + _layout_sidebarWidthPercent: 'string', // percent of text window width taken up by sidebar // appearance properties on the data document backgroundColor: 'string', // background color of document - borderRounding: 'string', // border radius rounding of document - boxShadow: 'string', // the amount of shadow around the perimeter of a document + layout_borderRounding: 'string', // border radius rounding of document + layout_boxShadow: 'string', // the amount of shadow around the perimeter of a document color: 'string', // foreground color of document - fitContentsToBox: 'boolean', // whether freeform view contents should be zoomed/panned to fill the area of the document view box + freeform_fitContentsToBox: 'boolean', // whether freeform view contents should be zoomed/panned to fill the area of the document view box fontSize: 'string', hidden: 'boolean', // whether a document should not be displayed - isInkMask: 'boolean', // is the document a mask (ie, sits on top of other documents, has an unbounded width/height that is dark, and content uses 'hard-light' mix-blend-mode to let other documents pop through) - layout: 'string', // this is the native layout string for the document. templates can be added using other fields and setting layoutKey below - layoutKey: 'string', // holds the field key for the field that actually holds the current lyoat + stroke_isInkMask: 'boolean', // is the document a mask (ie, sits on top of other documents, has an unbounded width/height that is dark, and content uses 'hard-light' mix-blend-mode to let other documents pop through) + layout: 'string', // this is the native layout string for the document. templates can be added using other fields and setting layout_fieldKey below + layout_fieldKey: 'string', // holds the field key for the field that actually holds the current lyoat letterSpacing: 'string', opacity: 'number', // opacity of document - strokeWidth: 'number', - strokeBezier: 'number', - strokeStartMarker: 'string', - strokeEndMarker: 'string', - strokeDash: 'string', + stroke_width: 'number', + stroke_bezier: 'number', + stroke_startMarker: 'string', + stroke_endMarker: 'string', + stroke_dash: 'string', textTransform: 'string', treeViewOpen: 'boolean', // flag denoting whether the documents sub-tree (contents) is visible or hidden treeViewExpandedView: 'string', // name of field whose contents are being displayed as the document's subtree @@ -86,21 +86,21 @@ export const documentSchema = createSchema({ onDragStart: ScriptField, // script to run when document is dragged (without being selected). the script should return the Doc to be dropped. followLinkLocation: 'string', // flag for where to place content when following a click interaction (e.g., add:right, lightbox, default, ) hideLinkButton: 'boolean', // whether the blue link counter button should be hidden - hideAllLinks: 'boolean', // whether all individual blue anchor dots should be hidden - linkDisplay: 'boolean', // whether a link connection should be shown between link anchor endpoints. + layout_hideAllLinks: 'boolean', // whether all individual blue anchor dots should be hidden + link_displayLine: 'boolean', // whether a link connection should be shown between link anchor endpoints. isLightbox: 'boolean', // whether the marked object will display addDocTab() calls that target "lightbox" destinations layers: listSpec('string'), // which layers the document is part of _lockedPosition: 'boolean', // whether the document can be moved (dragged) _lockedTransform: 'boolean', // whether a freeformview can pan/zoom - linkDisplayArrow: 'boolean', // toggles directed arrows + link_displayArrow: 'boolean', // toggles directed arrows // drag drop properties _stayInCollection: 'boolean', // whether document can be dropped into a different collection dragFactory: Doc, // the document that serves as the "template" for the onDragStart script. ie, to drag out copies of the dragFactory document. - dropAction: 'string', // override specifying what should happen when this document is dropped (can be "alias", "copy", "move") - targetDropAction: 'string', // allows the target of a drop event to specify the dropAction ("alias", "copy", "move") NOTE: if the document is dropped within the same collection, the dropAction is coerced to 'move' - childDropAction: 'string', // specify the override for what should happen when the child of a collection is dragged from it and dropped (can be "alias" or "copy") - removeDropProperties: listSpec('string'), // properties that should be removed from the alias/copy/etc of this document when it is dropped + dropAction: 'string', // override specifying what should happen when this document is dropped (can be "embed", "copy", "move") + targetDropAction: 'string', // allows the target of a drop event to specify the dropAction ("embed", "copy", "move") NOTE: if the document is dropped within the same collection, the dropAction is coerced to 'move' + childDropAction: 'string', // specify the override for what should happen when the child of a collection is dragged from it and dropped (can be "embed" or "copy") + removeDropProperties: listSpec('string'), // properties that should be removed from the embed/copy/etc of this document when it is dropped }); export const collectionSchema = createSchema({ diff --git a/src/fields/util.ts b/src/fields/util.ts index 5e925cd7e..6058a5a5f 100644 --- a/src/fields/util.ts +++ b/src/fields/util.ts @@ -9,11 +9,10 @@ import { returnZero } from '../Utils'; import CursorField from './CursorField'; import { AclAdmin, - AclAugment, AclEdit, aclLevel, AclPrivate, - AclReadonly, + AclSelfEdit, AclSym, DataSym, Doc, @@ -35,7 +34,7 @@ import { ObjectField } from './ObjectField'; import { PrefetchProxy, ProxyField } from './Proxy'; import { RefField } from './RefField'; import { SchemaHeaderField } from './SchemaHeaderField'; -import { ComputedField } from './ScriptField'; +import { ComputedField, ScriptField } from './ScriptField'; import { ScriptCast, StrCast } from './Types'; import { convertCompilerOptionsFromJson } from 'typescript'; @@ -57,7 +56,7 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number if (value !== undefined) { value = value[SelfProxy] || value; } - const curValue = target.__fields[prop]; + const curValue = target.__fieldTuples[prop]; if (curValue === value || (curValue instanceof ProxyField && value instanceof RefField && curValue.fieldId === value[Id])) { // TODO This kind of checks correctly in the case that curValue is a ProxyField and value is a RefField, but technically // curValue should get filled in with value if it isn't already filled in, in case we fetched the referenced field some other way @@ -66,6 +65,7 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number if (value instanceof RefField) { value = new ProxyField(value); } + if (value instanceof ObjectField) { if (value[Parent] && value[Parent] !== receiver && !(value instanceof PrefetchProxy)) { throw new Error("Can't put the same object in multiple documents at the same time"); @@ -88,11 +88,13 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number if (writeToDoc) { if (value === undefined) { - target.__fieldKeys && delete target.__fieldKeys[prop]; - delete target.__fields[prop]; + target[FieldKeys] && delete target[FieldKeys][prop]; // Lists don't have a FieldKeys field + delete target.__fieldTuples[prop]; } else { - target.__fieldKeys && (target.__fieldKeys[prop] = true); - target.__fields[prop] = value; + // bcz: uncomment to see if server is being updated + // console.log(prop + ' = ' + value + '(' + curValue + ')'); + target[FieldKeys] && (target[FieldKeys][prop] = true); // Lists don't have a FieldKeys field + target.__fieldTuples[prop] = value; } if (writeToServer) { @@ -103,20 +105,24 @@ const _setterImpl = action(function (target: any, prop: string | symbol | number DocServer.registerDocWithCachedUpdate(receiver, prop as string, curValue); } !receiver[Initializing] && + !receiver.dontUndo && (!receiver[UpdatingFromServer] || receiver[ForceServerWrite]) && - UndoManager.AddEvent({ - redo: () => (receiver[prop] = value), - undo: () => { - const wasUpdate = receiver[UpdatingFromServer]; - const wasForce = receiver[ForceServerWrite]; - receiver[ForceServerWrite] = true; // needed since writes aren't propagated to server if UpdatingFromServerIsSet - receiver[UpdatingFromServer] = true; // needed if the event caused ACL's to change such that the doc is otherwise no longer editable. - receiver[prop] = curValue; - receiver[ForceServerWrite] = wasForce; - receiver[UpdatingFromServer] = wasUpdate; + UndoManager.AddEvent( + { + redo: () => (receiver[prop] = value), + undo: () => { + const wasUpdate = receiver[UpdatingFromServer]; + const wasForce = receiver[ForceServerWrite]; + receiver[ForceServerWrite] = true; // needed since writes aren't propagated to server if UpdatingFromServerIsSet + receiver[UpdatingFromServer] = true; // needed if the event caused ACL's to change such that the doc is otherwise no longer editable. + receiver[prop] = curValue; + receiver[ForceServerWrite] = wasForce; + receiver[UpdatingFromServer] = wasUpdate; + }, + prop: prop?.toString(), }, - prop: prop?.toString(), - }); + value + ); return true; } return false; @@ -143,13 +149,13 @@ export function denormalizeEmail(email: string) { * Copies parent's acl fields to the child */ export function inheritParentAcls(parent: Doc, child: Doc) { + return; const dataDoc = parent[DataSym]; for (const key of Object.keys(dataDoc)) { // if the default acl mode is private, then don't inherit the acl-Public permission, but set it to private. const permission = key === 'acl-Public' && Doc.defaultAclPrivate ? AclPrivate : dataDoc[key]; key.startsWith('acl') && distributeAcls(key, permission, child); } - return; } /** @@ -209,24 +215,8 @@ export function SetCachedGroups(groups: string[]) { } function getEffectiveAcl(target: any, user?: string): symbol { const targetAcls = target[AclSym]; - if (targetAcls?.['acl-Me'] === AclAdmin || GetCachedGroupByName(SharingPermissions.Admin)) return AclAdmin; - if (target['acl-' + normalizeEmail(Doc.CurrentUserEmail)]) { - if (target['acl-' + normalizeEmail(Doc.CurrentUserEmail)] == SharingPermissions.Admin) { - return AclAdmin; - } - if (target['acl-' + normalizeEmail(Doc.CurrentUserEmail)] == SharingPermissions.Edit) { - return AclEdit; - } - if (target['acl-' + normalizeEmail(Doc.CurrentUserEmail)] == SharingPermissions.Augment) { - return AclAugment; - } - if (target['acl-' + normalizeEmail(Doc.CurrentUserEmail)] == SharingPermissions.View) { - return AclReadonly; - } - if (target['acl-' + normalizeEmail(Doc.CurrentUserEmail)] == SharingPermissions.None) { - return AclPrivate; - } - } + if (targetAcls?.['acl-Me'] === AclAdmin || GetCachedGroupByName('Admin')) return AclAdmin; + const userChecked = user || Doc.CurrentUserEmail; // if the current user is the author of the document / the current user is a member of the admin group if (targetAcls && Object.keys(targetAcls).length) { let effectiveAcl = AclPrivate; @@ -248,7 +238,7 @@ function getEffectiveAcl(target: any, user?: string): symbol { return DocServer?.Control?.isReadOnly?.() && HierarchyMapping.get(effectiveAcl)!.level < aclLevel.editable ? AclEdit : effectiveAcl; } // authored documents are private until an ACL is set. - const targetAuthor = target.__fields?.author || target.author; // target may be a Doc of Proxy, so check __fields.author and .author + const targetAuthor = target.__fieldTuples?.author || target.author; // target may be a Doc of Proxy, so check __fieldTuples.author and .author if (targetAuthor && targetAuthor !== userChecked) return AclPrivate; return AclAdmin; let acl = AclPrivate; @@ -272,7 +262,7 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc if (!visited) visited = [] as Doc[]; if (!target || visited.includes(target)) return; - if ((target._viewType === CollectionViewType.Docking && visited.length > 1) || Doc.GetProto(visited[0]) !== Doc.GetProto(target)) { + if ((target._type_collection === CollectionViewType.Docking && visited.length > 1) || Doc.GetProto(visited[0]) !== Doc.GetProto(target)) { target[key] = acl; Doc.GetProto(target)[key] = acl; if (target !== Doc.GetProto(target)) { @@ -296,7 +286,7 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc } let dataDocChanged = false; - const dataDoc = target[DataSym]; + const dataDoc = target[DocData]; if (dataDoc && (!inheritingFromCollection || !dataDoc[key] || ReverseHierarchyMap.get(StrCast(dataDoc[key]))! > ReverseHierarchyMap.get(acl)!)) { if (GetEffectiveAcl(dataDoc) === AclAdmin) { dataDoc[key] = acl; @@ -309,13 +299,13 @@ export function distributeAcls(key: string, acl: SharingPermissions, target: Doc // maps over the children of the document DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc)]).forEach(d => { distributeAcls(key, acl, d, inheritingFromCollection, visited); - distributeAcls(key, acl, d[DataSym], inheritingFromCollection, visited); + distributeAcls(key, acl, d[DocData], inheritingFromCollection, visited); }); // maps over the annotations of the document - DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + '-annotations']).forEach(d => { + DocListCast(dataDoc[Doc.LayoutFieldKey(dataDoc) + '_annotations']).forEach(d => { distributeAcls(key, acl, d, inheritingFromCollection, visited); - distributeAcls(key, acl, d[DataSym], inheritingFromCollection, visited); + distributeAcls(key, acl, d[DocData], inheritingFromCollection, visited); }); } @@ -330,16 +320,16 @@ export function setter(target: any, in_prop: string | symbol | number, value: an // if you're trying to change an acl but don't have Admin access / you're trying to change it to something that isn't an acceptable acl, you can't if (typeof prop === 'string' && prop.startsWith('acl') && (effectiveAcl !== AclAdmin || ![...Object.values(SharingPermissions), undefined].includes(value))) return true; - if (typeof prop === 'string' && prop !== '__id' && prop !== '__fields' && prop.startsWith('_')) { + if (typeof prop === 'string' && prop !== '__id' && prop !== '__fieldTuples' && prop.startsWith('_')) { if (!prop.startsWith('__')) prop = prop.substring(1); if (target.__LAYOUT__) { target.__LAYOUT__[prop] = value; return true; } } - if (target.__fields[prop] instanceof ComputedField) { - if (target.__fields[prop].setterscript && value !== undefined && !(value instanceof ComputedField)) { - return ScriptCast(target.__fields[prop])?.setterscript?.run({ self: target[SelfProxy], this: target[SelfProxy], value }).success ? true : false; + if (target.__fieldTuples[prop] instanceof ComputedField) { + if (target.__fieldTuples[prop].setterscript && value !== undefined && !(value instanceof ComputedField)) { + return ScriptCast(target.__fieldTuples[prop])?.setterscript?.run({ self: target[SelfProxy], this: target[SelfProxy], value }).success ? true : false; } } return _setter(target, prop, value, receiver); @@ -349,14 +339,13 @@ export function getter(target: any, prop: string | symbol, proxy: any): any { // prettier-ignore switch (prop) { case 'then' : return undefined; - case '__fields' : case '__id': - case 'constructor': case 'toString': case 'valueOf': - case 'factory': case 'serializeInfo': + case 'constructor': case 'toString': case 'valueOf': + case 'serializeInfo': case 'factory': return target[prop]; - case AclSym : return target[AclSym]; - case $mobx: return target.__fields[prop]; - case LayoutSym: return target.__LAYOUT__; - case HeightSym: case WidthSym: if (GetEffectiveAcl(target) === AclPrivate) return returnZero; + case DocAcl : return target[DocAcl]; + case $mobx: return target.__fieldTuples[prop]; + case DocLayout: return target.__LAYOUT__; + case Height: case Width: if (GetEffectiveAcl(target) === AclPrivate) return returnZero; default : if (typeof prop === 'symbol') return target[prop]; if (prop.startsWith('isMobX')) return target[prop]; @@ -370,7 +359,7 @@ export function getter(target: any, prop: string | symbol, proxy: any): any { } function getFieldImpl(target: any, prop: string | number, proxy: any, ignoreProto: boolean = false): any { - const field = target.__fields[prop]; + const field = target.__fieldTuples[prop]; const value = field?.[ToValue]?.(proxy); // converts ComputedFields to values, or unpacks ProxyFields into Proxys if (value) return value.value; if (field === undefined && !ignoreProto && prop !== 'proto') { @@ -414,6 +403,7 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any diff?.op === '$addToSet' ? { redo: () => { + console.log('redo $add: ' + prop, diff.items); // bcz: uncomment to log undo receiver[prop].push(...diff.items.map((item: any) => item.value ?? item)); lastValue = ObjectField.MakeCopy(receiver[prop]); }, @@ -430,11 +420,12 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any }); lastValue = ObjectField.MakeCopy(receiver[prop]); }), - prop: '', + prop: 'add ' + diff.items.length + ' items to list', } : diff?.op === '$remFromSet' ? { redo: action(() => { + console.log('redo $rem: ' + prop, diff.items); // bcz: uncomment to log undo diff.items.forEach((item: any) => { const ind = item instanceof SchemaHeaderField ? receiver[prop].findIndex((ele: any) => ele instanceof SchemaHeaderField && ele.heading === item.heading) : receiver[prop].indexOf(item.value ?? item); ind !== -1 && receiver[prop].splice(ind, 1); @@ -454,10 +445,11 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any }); lastValue = ObjectField.MakeCopy(receiver[prop]); }, - prop: '', + prop: 'remove ' + diff.items.length + ' items from list', } : { redo: () => { + console.log('redo list: ' + prop, receiver[prop]); // bcz: uncomment to log undo receiver[prop] = ObjectField.MakeCopy(newValue as List<any>); lastValue = ObjectField.MakeCopy(receiver[prop]); }, @@ -466,7 +458,7 @@ export function updateFunction(target: any, prop: any, value: any, receiver: any receiver[prop] = ObjectField.MakeCopy(prevValue as List<any>); lastValue = ObjectField.MakeCopy(receiver[prop]); }, - prop: '', + prop: 'assign list', } ); } diff --git a/src/mobile/AudioUpload.tsx b/src/mobile/AudioUpload.tsx index a0b437354..7a1dde9fb 100644 --- a/src/mobile/AudioUpload.tsx +++ b/src/mobile/AudioUpload.tsx @@ -21,7 +21,13 @@ import React = require('react'); export class AudioUpload extends React.Component { @observable public _audioCol: Doc = FieldValue( Cast( - Docs.Create.FreeformDocument([Cast(Docs.Create.AudioDocument(nullAudio, { title: 'mobile audio', _width: 500, _height: 100 }), Doc) as Doc], { title: 'mobile audio', _width: 300, _height: 300, _fitContentsToBox: true, boxShadow: '0 0' }), + Docs.Create.FreeformDocument([Cast(Docs.Create.AudioDocument(nullAudio, { title: 'mobile audio', _width: 500, _height: 100 }), Doc) as Doc], { + title: 'mobile audio', + _width: 300, + _height: 300, + _layout_fitContentsToBox: true, + layout_boxShadow: '0 0', + }), Doc ) ) as Doc; @@ -48,7 +54,7 @@ export class AudioUpload extends React.Component { Doc ) as Doc, ], - { title: 'mobile audio', _width: 300, _height: 300, _fitContentsToBox: true, boxShadow: '0 0' } + { title: 'mobile audio', _width: 300, _height: 300, _layout_fitContentsToBox: true, layout_boxShadow: '0 0' } ), Doc ) @@ -104,8 +110,8 @@ export class AudioUpload extends React.Component { pinToPres={emptyFunction} rootSelected={returnTrue} removeDocument={undefined} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} ScreenToLocalTransform={Transform.Identity} PanelWidth={() => 600} diff --git a/src/mobile/ImageUpload.tsx b/src/mobile/ImageUpload.tsx index f910d765e..da38fcaee 100644 --- a/src/mobile/ImageUpload.tsx +++ b/src/mobile/ImageUpload.tsx @@ -42,7 +42,7 @@ export class Uploader extends React.Component<ImageUploadProps> { this.process = "Uploading Files"; for (let index = 0; index < files.length; ++index) { const file = files[index]; - const res = await Networking.UploadFilesToServer(file); + const res = await Networking.UploadFilesToServer({file}); this.setOpacity(3, "1"); // Slab 3 // For each item that the user has selected res.map(async ({ result }) => { diff --git a/src/mobile/MobileInterface.tsx b/src/mobile/MobileInterface.tsx index a543c2f70..584a7b432 100644 --- a/src/mobile/MobileInterface.tsx +++ b/src/mobile/MobileInterface.tsx @@ -260,7 +260,7 @@ export class MobileInterface extends React.Component { @action componentDidMount = () => { // if the home menu is in list view -> adjust the menu toggle appropriately - this._menuListView = this._homeDoc._viewType === 'stacking' ? true : false; + this._menuListView = this._homeDoc._type_collection === 'stacking' ? true : false; Doc.ActiveTool = InkTool.None; // ink should intially be set to none Doc.UserDoc().activeMobile = this._homeDoc; // active mobile set to home AudioBox.Enabled = true; @@ -402,8 +402,8 @@ export class MobileInterface extends React.Component { docViewPath={returnEmptyDoclist} whenChildContentsActiveChanged={emptyFunction} bringToFront={emptyFunction} - docFilters={returnEmptyFilter} - docRangeFilters={returnEmptyFilter} + childFilters={returnEmptyFilter} + childFiltersByRanges={returnEmptyFilter} searchFilterDocs={returnEmptyDoclist} /> </div> @@ -614,7 +614,7 @@ export class MobileInterface extends React.Component { // The static ink menu that appears at the top @computed get inkMenu() { - return this._activeDoc._viewType !== CollectionViewType.Docking || !this._ink ? null : <div className="colorSelector">{/* <CollectionFreeFormViewChrome /> */}</div>; + return this._activeDoc._type_collection !== CollectionViewType.Docking || !this._ink ? null : <div className="colorSelector">{/* <CollectionFreeFormViewChrome /> */}</div>; } // DocButton that uses UndoManager and handles the opacity change if CanUndo is true @@ -657,7 +657,7 @@ export class MobileInterface extends React.Component { // DocButton for switching into ink mode @computed get drawInk() { - return !this.mainContainer || this._activeDoc._viewType !== CollectionViewType.Docking ? null : ( + return !this.mainContainer || this._activeDoc._type_collection !== CollectionViewType.Docking ? null : ( <div className="docButton" id="inkButton" title={Doc.isDocPinned(this._activeDoc) ? 'Pen on' : 'Pen off'} onClick={this.onSwitchInking}> <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="pen-nib" /> </div> @@ -666,7 +666,7 @@ export class MobileInterface extends React.Component { // DocButton: Button that appears on the bottom of the screen to initiate image upload @computed get uploadImageButton() { - if (this._activeDoc.type === DocumentType.COL && this._activeDoc !== this._homeDoc && this._activeDoc._viewType !== CollectionViewType.Docking && this._activeDoc.title !== 'WORKSPACES') { + if (this._activeDoc.type === DocumentType.COL && this._activeDoc !== this._homeDoc && this._activeDoc._type_collection !== CollectionViewType.Docking && this._activeDoc.title !== 'WORKSPACES') { return ( <div className="docButton" id="imageButton" title={Doc.isDocPinned(this._activeDoc) ? 'Pen on' : 'Pen off'} onClick={this.toggleUpload}> <FontAwesomeIcon className="documentdecorations-icon" size="sm" icon="upload" /> @@ -722,9 +722,9 @@ export class MobileInterface extends React.Component { // Logic for switching the menu into the icons @action changeToIconView = () => { - if ((this._homeDoc._viewType = 'stacking')) { + if ((this._homeDoc._type_collection = 'stacking')) { this._menuListView = false; - this._homeDoc._viewType = 'masonry'; + this._homeDoc._type_collection = 'masonry'; this._homeDoc.columnWidth = 300; this._homeDoc._columnWidth = 300; const menuButtons = DocListCast(this._homeDoc.data); @@ -742,8 +742,8 @@ export class MobileInterface extends React.Component { // Logic for switching the menu into the stacking view @action changeToListView = () => { - if ((this._homeDoc._viewType = 'masonry')) { - this._homeDoc._viewType = 'stacking'; + if ((this._homeDoc._type_collection = 'masonry')) { + this._homeDoc._type_collection = 'stacking'; this._menuListView = true; const menuButtons = DocListCast(this._homeDoc.data); menuButtons.map(doc => { @@ -800,7 +800,7 @@ export class MobileInterface extends React.Component { // Radial menu can only be used if it is a colleciton and it is not a homeDoc // (and cannot be used on Dashboard to avoid pin to presentation opening on right) @computed get displayRadialMenu() { - return this._activeDoc.type === 'collection' && this._activeDoc !== this._homeDoc && this._activeDoc._viewType !== CollectionViewType.Docking ? <RadialMenu /> : null; + return this._activeDoc.type === 'collection' && this._activeDoc !== this._homeDoc && this._activeDoc._type_collection !== CollectionViewType.Docking ? <RadialMenu /> : null; } onDragOver = (e: React.DragEvent) => { diff --git a/src/server/ApiManagers/SearchManager.ts b/src/server/ApiManagers/SearchManager.ts index a74e13a62..186f0bcd3 100644 --- a/src/server/ApiManagers/SearchManager.ts +++ b/src/server/ApiManagers/SearchManager.ts @@ -1,94 +1,89 @@ -import { exec } from "child_process"; -import { cyan, green, red, yellow } from "colors"; +import { exec } from 'child_process'; +import { cyan, green, red, yellow } from 'colors'; import * as path from 'path'; -import { log_execution } from "../ActionUtilities"; -import { Database } from "../database"; -import { Method } from "../RouteManager"; -import RouteSubscriber from "../RouteSubscriber"; -import { Search } from "../Search"; -import ApiManager, { Registration } from "./ApiManager"; -import { Directory, pathToDirectory } from "./UploadManager"; +import { log_execution } from '../ActionUtilities'; +import { Database } from '../database'; +import { Method } from '../RouteManager'; +import RouteSubscriber from '../RouteSubscriber'; +import { Search } from '../Search'; +import ApiManager, { Registration } from './ApiManager'; +import { Directory, pathToDirectory } from './UploadManager'; const findInFiles = require('find-in-files'); export class SearchManager extends ApiManager { - protected initialize(register: Registration): void { - register({ method: Method.GET, - subscription: new RouteSubscriber("solr").add("action"), + subscription: new RouteSubscriber('solr').add('action'), secureHandler: async ({ req, res }) => { const { action } = req.params; switch (action) { - case "start": - case "stop": - const status = req.params.action === "start"; + case 'start': + case 'stop': + const status = req.params.action === 'start'; SolrManager.SetRunning(status); break; - case "update": + case 'update': await SolrManager.update(); break; default: console.log(yellow(`${action} is an unknown solr operation.`)); } - res.redirect("/home"); - } + res.redirect('/home'); + }, }); register({ method: Method.GET, - subscription: "/textsearch", + subscription: '/textsearch', secureHandler: async ({ req, res }) => { const q = req.query.q; if (q === undefined) { res.send([]); return; } - const resObj: { ids: string[], numFound: number, lines: string[] } = { ids: [], numFound: 0, lines: [] }; + const resObj: { ids: string[]; numFound: number; lines: string[] } = { ids: [], numFound: 0, lines: [] }; let results: any; const dir = pathToDirectory(Directory.text); try { const regex = new RegExp(q.toString()); - results = await findInFiles.find({ 'term': q, 'flags': 'ig' }, dir, ".txt$"); + results = await findInFiles.find({ term: q, flags: 'ig' }, dir, '.txt$'); for (const result in results) { - resObj.ids.push(path.basename(result, ".txt").replace(/upload_/, "")); + resObj.ids.push(path.basename(result, '.txt').replace(/upload_/, '')); resObj.lines.push(results[result].line); resObj.numFound++; } res.send(resObj); } catch (e) { - console.log(red("textsearch:bad RegExp" + q.toString())); + console.log(red('textsearch:bad RegExp' + q.toString())); res.send([]); return; } - } + }, }); register({ method: Method.GET, - subscription: "/dashsearch", + subscription: '/dashsearch', secureHandler: async ({ req, res }) => { const solrQuery: any = {}; - ["q", "fq", "start", "rows", "sort", "hl.maxAnalyzedChars", "hl", "hl.fl"].forEach(key => solrQuery[key] = req.query[key]); + ['q', 'fq', 'start', 'rows', 'sort', 'hl.maxAnalyzedChars', 'hl', 'hl.fl'].forEach(key => (solrQuery[key] = req.query[key])); if (solrQuery.q === undefined) { res.send([]); return; } const results = await Search.search(solrQuery); res.send(results); - } + }, }); - } - } export namespace SolrManager { - export function SetRunning(status: boolean) { - const args = status ? "start" : "stop -p 8983"; + const args = status ? 'start' : 'stop -p 8983'; console.log(`solr management: trying to ${args}`); - exec(`solr ${args}`, { cwd: "./solr-8.3.1/bin" }, (error, stdout, stderr) => { + exec(`solr ${args}`, { cwd: './solr-8.3.1/bin' }, (error, stdout, stderr) => { if (error) { console.log(red(`solr management error: unable to ${args} server`)); console.log(red(error.message)); @@ -97,39 +92,39 @@ export namespace SolrManager { console.log(yellow(stderr)); }); if (status) { - console.log(cyan("Start script is executing: please allow 15 seconds for solr to start on port 8983.")); + console.log(cyan('Start script is executing: please allow 15 seconds for solr to start on port 8983.')); } } export async function update() { - console.log(green("Beginning update...")); + console.log(green('Beginning update...')); await log_execution<void>({ - startMessage: "Clearing existing Solr information...", - endMessage: "Solr information successfully cleared", + startMessage: 'Clearing existing Solr information...', + endMessage: 'Solr information successfully cleared', action: Search.clear, - color: cyan + color: cyan, }); const cursor = await log_execution({ - startMessage: "Connecting to and querying for all documents from database...", + startMessage: 'Connecting to and querying for all documents from database...', endMessage: ({ result, error }) => { const success = error === null && result !== undefined; if (!success) { - console.log(red("Unable to connect to the database.")); + console.log(red('Unable to connect to the database.')); process.exit(0); } - return "Connection successful and query complete"; + return 'Connection successful and query complete'; }, action: () => Database.Instance.query({}), - color: yellow + color: yellow, }); const updates: any[] = []; let numDocs = 0; function updateDoc(doc: any) { numDocs++; - if ((numDocs % 50) === 0) { + if (numDocs % 50 === 0) { console.log(`Batch of 50 complete, total of ${numDocs}`); } - if (doc.__type !== "Doc") { + if (doc.__type !== 'Doc') { return; } const fields = doc.fields; @@ -143,8 +138,8 @@ export namespace SolrManager { const term = ToSearchTerm(value); if (term !== undefined) { const { suffix, value } = term; - if (key.endsWith('lastModified')) { - update["lastModified" + suffix] = value; + if (key.endsWith('modificationDate')) { + update['modificationDate' + suffix] = value; } update[key + suffix] = value; dynfield = true; @@ -157,51 +152,54 @@ export namespace SolrManager { await cursor?.forEach(updateDoc); const result = await log_execution({ startMessage: `Dispatching updates for ${updates.length} documents`, - endMessage: "Dispatched updates complete", + endMessage: 'Dispatched updates complete', action: () => Search.updateDocuments(updates), - color: cyan + color: cyan, }); try { if (result) { const { status } = JSON.parse(result).responseHeader; - console.log(status ? red(`Failed with status code (${status})`) : green("Success!")); + console.log(status ? red(`Failed with status code (${status})`) : green('Success!')); } else { - console.log(red("Solr is likely not running!")); + console.log(red('Solr is likely not running!')); } } catch (e) { - console.log(red("Error:")); + console.log(red('Error:')); console.log(e); - console.log("\n"); + console.log('\n'); } await cursor?.close(); } - const suffixMap: { [type: string]: (string | [string, string | ((json: any) => any)]) } = { - "number": "_n", - "string": "_t", - "boolean": "_b", - "image": ["_t", "url"], - "video": ["_t", "url"], - "pdf": ["_t", "url"], - "audio": ["_t", "url"], - "web": ["_t", "url"], - "map": ["_t", "url"], - "date": ["_d", value => new Date(value.date).toISOString()], - "proxy": ["_i", "fieldId"], - "prefetch_proxy": ["_i", "fieldId"], - "list": ["_l", list => { - const results = []; - for (const value of list.fields) { - const term = ToSearchTerm(value); - if (term) { - results.push(term.value); + const suffixMap: { [type: string]: string | [string, string | ((json: any) => any)] } = { + number: '_n', + string: '_t', + boolean: '_b', + image: ['_t', 'url'], + video: ['_t', 'url'], + pdf: ['_t', 'url'], + audio: ['_t', 'url'], + web: ['_t', 'url'], + map: ['_t', 'url'], + date: ['_d', value => new Date(value.date).toISOString()], + proxy: ['_i', 'fieldId'], + prefetch_proxy: ['_i', 'fieldId'], + list: [ + '_l', + list => { + const results = []; + for (const value of list.fields) { + const term = ToSearchTerm(value); + if (term) { + results.push(term.value); + } } - } - return results.length ? results : null; - }] + return results.length ? results : null; + }, + ], }; - function ToSearchTerm(val: any): { suffix: string, value: any } | undefined { + function ToSearchTerm(val: any): { suffix: string; value: any } | undefined { if (val === null || val === undefined) { return; } @@ -213,7 +211,7 @@ export namespace SolrManager { if (Array.isArray(suffix)) { const accessor = suffix[1]; - if (typeof accessor === "function") { + if (typeof accessor === 'function') { val = accessor(val); } else { val = val[accessor]; @@ -223,5 +221,4 @@ export namespace SolrManager { return { suffix, value: val }; } - -}
\ No newline at end of file +} diff --git a/src/server/ApiManagers/UploadManager.ts b/src/server/ApiManagers/UploadManager.ts index 74c06b4a6..ba6d7acfe 100644 --- a/src/server/ApiManagers/UploadManager.ts +++ b/src/server/ApiManagers/UploadManager.ts @@ -75,7 +75,7 @@ export default class UploadManager extends ApiManager { for (const key in files) { const f = files[key]; if (!Array.isArray(f)) { - const result = await DashUploadUtils.upload(f); + const result = await DashUploadUtils.upload(f, key); // key is the guid used by the client to track upload progress. result && !(result.result instanceof Error) && results.push(result); } } diff --git a/src/server/DashUploadUtils.ts b/src/server/DashUploadUtils.ts index 070d49ec3..eaaac4e6d 100644 --- a/src/server/DashUploadUtils.ts +++ b/src/server/DashUploadUtils.ts @@ -181,9 +181,10 @@ export namespace DashUploadUtils { }); } - export async function upload(file: File): Promise<Upload.FileResponse> { + export async function upload(file: File, overwriteGuid?: string): Promise<Upload.FileResponse> { const { type, path, name } = file; const types = type?.split('/') ?? []; + uploadProgress.set(overwriteGuid ?? name, 'uploading'); // If the client sent a guid it uses to track upload progress, use that guid. Otherwise, use the file's name. const category = types[0]; let format = `.${types[1]}`; @@ -205,6 +206,7 @@ export namespace DashUploadUtils { .videoCodec('copy') // this will copy the data instead of reencode it .save(file.path.replace('.mkv', '.mp4')) .on('end', res) + .on('error', (e: any) => console.log(e)) ); file.path = file.path.replace('.mkv', '.mp4'); format = '.mp4'; @@ -234,7 +236,7 @@ export namespace DashUploadUtils { return { source: file, result: { name: 'Unsupported video format', message: `Could not upload unsupported file (${name}). Please convert to an .mp4` } }; } } - if (videoFormats.includes(format)) { + if (videoFormats.includes(format) || format.includes('.webm')) { return MoveParsedFile(file, Directory.videos); } fs.unlink(path, () => {}); diff --git a/src/server/updateProtos.ts b/src/server/updateProtos.ts index c5552f6bf..2f3772a77 100644 --- a/src/server/updateProtos.ts +++ b/src/server/updateProtos.ts @@ -1,14 +1,22 @@ -import { Database } from "./database"; +import { Database } from './database'; -const protos = - ["text", "image", "web", "collection", "kvp", "video", "audio", "pdf", "icon", "import", "linkdoc", "map"]; +const protos = ['text', 'image', 'web', 'collection', 'kvp', 'video', 'audio', 'pdf', 'icon', 'import', 'linkdoc', 'map']; (async function () { await Promise.all( - protos.map(protoId => new Promise(res => Database.Instance.update(protoId, { - $set: { "fields.baseProto": true } - }, res))) + protos.map( + protoId => + new Promise(res => + Database.Instance.update( + protoId, + { + $set: { 'fields.isBaseProto': true }, + }, + res + ) + ) + ) ); - console.log("done"); -})();
\ No newline at end of file + console.log('done'); +})(); diff --git a/src/server/websocket.ts b/src/server/websocket.ts index f131927f7..be5cdb202 100644 --- a/src/server/websocket.ts +++ b/src/server/websocket.ts @@ -476,8 +476,8 @@ export namespace WebSocket { if (term !== undefined) { const { suffix, value } = term; update[key + suffix] = { set: value }; - if (key.endsWith('lastModified')) { - update['lastModified' + suffix] = value; + if (key.endsWith('modificationDate')) { + update['modificationDate' + suffix] = value; } } } diff --git a/tsconfig.json b/tsconfig.json index bff9255db..f1b275954 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,8 @@ "outDir": "dist", "lib": ["dom", "es2015"], "typeRoots": ["node_modules/@types", "./src/typings"], - "types": ["youtube", "node"] + "types": ["youtube", "node"], + "resolveJsonModule": true }, // "exclude": [ // "node_modules", |