diff --git a/src/package-lock.json b/src/package-lock.json index 2036c63d4..add13463b 100644 --- a/src/package-lock.json +++ b/src/package-lock.json @@ -1285,6 +1285,73 @@ "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" }, + "aws-sdk": { + "version": "2.1081.0", + "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1081.0.tgz", + "integrity": "sha512-204Aqi3NmSRZDAvyzmi1usje6oCM+Q4g6PgA+vc/XQQPe1oxO95AgOXZvrpjX2QlLbA0JDItL1ufUh3nszjaqA==", + "requires": { + "buffer": "4.9.2", + "events": "1.1.1", + "ieee754": "1.1.13", + "jmespath": "0.16.0", + "querystring": "0.2.0", + "sax": "1.2.1", + "url": "0.10.3", + "uuid": "3.3.2", + "xml2js": "0.4.19" + }, + "dependencies": { + "buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "requires": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "events": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", + "integrity": "sha1-nr23Y1rQmccNzEwqH1AEKI6L2SQ=" + }, + "ieee754": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", + "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==" + }, + "isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + }, + "sax": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", + "integrity": "sha1-e45lYZCyKOgaZq6nSEgNgozS03o=" + }, + "uuid": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.2.tgz", + "integrity": "sha512-yXJmeNaw3DnnKAOKJE51sL/ZaYfWJRl1pK9dr19YFCu0ObS231AB1/LbqTKRAQ5kw8A90rA6fr4riOUpTZvQZA==" + }, + "xml2js": { + "version": "0.4.19", + "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.4.19.tgz", + "integrity": "sha512-esZnJZJOiJR9wWKMyuvSE1y6Dq5LCuJanqhxslH2bxM6duahNZ+HMpCLhBQGZkbX6xRf8x1Y2eJlgt2q3qo49Q==", + "requires": { + "sax": ">=0.6.0", + "xmlbuilder": "~9.0.1" + } + }, + "xmlbuilder": { + "version": "9.0.7", + "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-9.0.7.tgz", + "integrity": "sha1-Ey7mPS7FVlxVfiD0wi35rKaGsQ0=" + } + } + }, "aws-sign2": { "version": "0.7.0", "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", @@ -1450,6 +1517,11 @@ } } }, + "boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=" + }, "brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -1509,6 +1581,14 @@ "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==" }, + "busboy": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.4.0.tgz", + "integrity": "sha512-TytIELfX6IPn1OClqcBz0NFE6+JT9e3iW0ZpgnEl7ffsfDxvRZGHfPaSHGbrI443nSV3GutCDWuqLB6yHY92Ew==", + "requires": { + "streamsearch": "^1.1.0" + } + }, "bytes": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", @@ -1580,6 +1660,32 @@ "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==" }, + "cheerio": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.10.tgz", + "integrity": "sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw==", + "requires": { + "cheerio-select": "^1.5.0", + "dom-serializer": "^1.3.2", + "domhandler": "^4.2.0", + "htmlparser2": "^6.1.0", + "parse5": "^6.0.1", + "parse5-htmlparser2-tree-adapter": "^6.0.1", + "tslib": "^2.2.0" + } + }, + "cheerio-select": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-1.5.0.tgz", + "integrity": "sha512-qocaHPv5ypefh6YNxvnbABM07KMxExbtbfuJoIie3iZXX1ERwYmJcIiRrr9H05ucQP1k28dav8rpdDgjQd8drg==", + "requires": { + "css-select": "^4.1.3", + "css-what": "^5.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0", + "domutils": "^2.7.0" + } + }, "chokidar": { "version": "3.5.3", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", @@ -1787,6 +1893,23 @@ "which": "^2.0.1" } }, + "css-select": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.2.1.tgz", + "integrity": "sha512-/aUslKhzkTNCQUB2qTX84lVmfia9NyjP3WpDGtj/WxhwBzWBYUV3DgUpurHTme8UTPcPlAD1DJ+b0nN/t50zDQ==", + "requires": { + "boolbase": "^1.0.0", + "css-what": "^5.1.0", + "domhandler": "^4.3.0", + "domutils": "^2.8.0", + "nth-check": "^2.0.1" + } + }, + "css-what": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-5.1.0.tgz", + "integrity": "sha512-arSMRWIIFY0hV8pIxZMEfmMI47Wj3R/aWpZDDxWYCPEiOMv6tfOrnpDtgxBYPEQD4V0Y/958+1TdC3iWTFcUPw==" + }, "cssom": { "version": "0.5.0", "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.5.0.tgz", @@ -1948,6 +2071,21 @@ "esutils": "^2.0.2" } }, + "dom-serializer": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", + "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.2.0", + "entities": "^2.0.0" + } + }, + "domelementtype": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", + "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==" + }, "domexception": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/domexception/-/domexception-4.0.0.tgz", @@ -1956,6 +2094,24 @@ "webidl-conversions": "^7.0.0" } }, + "domhandler": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.0.tgz", + "integrity": "sha512-fC0aXNQXqKSFTr2wDNZDhsEYjCiYsDWl3D01kwt25hm1YIPyDGHvvi3rw+PLqHAl/m71MaiF7d5zvBr0p5UB2g==", + "requires": { + "domelementtype": "^2.2.0" + } + }, + "domutils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", + "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", + "requires": { + "dom-serializer": "^1.0.1", + "domelementtype": "^2.2.0", + "domhandler": "^4.2.0" + } + }, "ecc-jsbn": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", @@ -2117,6 +2273,11 @@ "has-binary2": "~1.0.2" } }, + "entities": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", + "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==" + }, "env-paths": { "version": "2.2.1", "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", @@ -3038,6 +3199,23 @@ "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", "optional": true }, + "fs-extra": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-10.0.1.tgz", + "integrity": "sha512-NbdoVMZso2Lsrn/QwLXOy6rm0ufY2zEOKCDzJR/0kBsb0E6qed0P3iYK+Ath3BfvXEeu4JhEtXLgILx5psUfag==", + "requires": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "dependencies": { + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" + } + } + }, "fs-minipass": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", @@ -3174,8 +3352,7 @@ "graceful-fs": { "version": "4.2.9", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.9.tgz", - "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==", - "optional": true + "integrity": "sha512-NtNxqUcXgpW2iMrfqSfR73Glt39K+BLwWsPs94yR63v45T0Wbej7eRmL5cWfwEgqXnmjQp3zaJTshdRW/qC2ZQ==" }, "growl": { "version": "1.10.5", @@ -3380,6 +3557,17 @@ "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-2.0.1.tgz", "integrity": "sha512-0quDb7s97CfemeJAnW9wC0hw78MtW7NU3hqtCD75g2vFlDLt36llsYD7uB7SUzojLMP24N5IatXf7ylGXiGG9A==" }, + "htmlparser2": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", + "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "requires": { + "domelementtype": "^2.0.1", + "domhandler": "^4.0.0", + "domutils": "^2.5.2", + "entities": "^2.0.0" + } + }, "http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -3800,6 +3988,11 @@ } } }, + "jmespath": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", + "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==" + }, "js-cookie": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.1.tgz", @@ -3887,6 +4080,22 @@ "minimist": "^1.2.0" } }, + "jsonfile": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.1.0.tgz", + "integrity": "sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ==", + "requires": { + "graceful-fs": "^4.1.6", + "universalify": "^2.0.0" + }, + "dependencies": { + "universalify": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.0.tgz", + "integrity": "sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ==" + } + } + }, "jsonminify": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/jsonminify/-/jsonminify-0.4.1.tgz", @@ -7890,6 +8099,14 @@ "set-blocking": "~2.0.0" } }, + "nth-check": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.1.tgz", + "integrity": "sha512-it1vE95zF6dTT9lBsYbxvqh0Soy4SPowchj0UBGj/V6cTPnXXtQOPUbhZ6CmGzAD/rW22LQK6E96pcdJXk4A4w==", + "requires": { + "boolbase": "^1.0.0" + } + }, "number-is-nan": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", @@ -8109,6 +8326,14 @@ "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==" }, + "parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "requires": { + "parse5": "^6.0.1" + } + }, "parseqs": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/parseqs/-/parseqs-0.0.6.tgz", @@ -8333,6 +8558,11 @@ "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" }, + "querystring": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", + "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=" + }, "queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -9082,6 +9312,30 @@ "resolved": "https://registry.npmjs.org/stoppable/-/stoppable-1.1.0.tgz", "integrity": "sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==" }, + "stream_upload": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/stream_upload/-/stream_upload-0.1.5.tgz", + "integrity": "sha512-96Yiijdz7zIQnxe0SvuQ9r/IfkWNVxayw45rRCRKHrqgkc8tWmRHKAPNV1M6oNIlW/7bMskXHAdpaIdyasInqg==", + "requires": { + "aws-sdk": "^2.1033.0", + "fs-extra": "^10.0.0", + "mime-types": "^2.1.34", + "underscore": "^1.8.3", + "uuid": "^8.3.2" + }, + "dependencies": { + "uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + } + } + }, + "streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==" + }, "string-width": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", @@ -9820,6 +10074,22 @@ "punycode": "^2.1.0" } }, + "url": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", + "integrity": "sha1-Ah5NnHcF8hu/N9A861h2dAJ3TGQ=", + "requires": { + "punycode": "1.3.2", + "querystring": "0.2.0" + }, + "dependencies": { + "punycode": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", + "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=" + } + } + }, "util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", diff --git a/src/package.json b/src/package.json index 8ff696300..8edf6fcf0 100644 --- a/src/package.json +++ b/src/package.json @@ -31,6 +31,8 @@ ], "dependencies": { "async": "^3.2.2", + "busboy": "^1.4.0", + "cheerio": "^1.0.0-rc.10", "clean-css": "^5.2.4", "cookie-parser": "^1.4.6", "cross-spawn": "^7.0.3", @@ -63,6 +65,7 @@ "security": "1.0.0", "semver": "^7.3.5", "socket.io": "^2.4.1", + "stream_upload": "^0.1.5", "superagent": "^6.1.0", "terser": "^5.10.0", "threads": "^1.7.0", diff --git a/src/plugins/md_align/README.md b/src/plugins/md_align/README.md new file mode 100644 index 000000000..f1b1609ed --- /dev/null +++ b/src/plugins/md_align/README.md @@ -0,0 +1 @@ +Align Content in Mudoc \ No newline at end of file diff --git a/src/plugins/md_align/ep.json b/src/plugins/md_align/ep.json new file mode 100644 index 000000000..f222a306a --- /dev/null +++ b/src/plugins/md_align/ep.json @@ -0,0 +1,24 @@ +{ + "parts": [ + { + "name": "main", + "client_hooks": { + "aceEditEvent": "ep_align/static/js/index", + "postToolbarInit": "ep_align/static/js/index", + "aceDomLineProcessLineAttributes": "ep_align/static/js/index", + "postAceInit": "ep_align/static/js/index", + "aceInitialized": "ep_align/static/js/index", + "aceAttribsToClasses": "ep_align/static/js/index", + "collectContentPre": "ep_align/static/js/shared", + "aceRegisterBlockElements": "ep_align/static/js/index" + }, + "hooks": { + "eejsBlock_editbarMenuLeft": "ep_align/index", + "collectContentPre": "ep_align/static/js/shared", + "collectContentPost": "ep_align/static/js/shared", + "padInitToolbar": "ep_align/index", + "getLineHTMLForExport": "ep_align/index" + } + } + ] +} diff --git a/src/plugins/md_align/index.js b/src/plugins/md_align/index.js new file mode 100644 index 000000000..bac20ce1a --- /dev/null +++ b/src/plugins/md_align/index.js @@ -0,0 +1,88 @@ +'use strict'; + +const eejs = require('ep_etherpad-lite/node/eejs/'); +const Changeset = require('ep_etherpad-lite/static/js/Changeset'); +const settings = require('ep_etherpad-lite/node/utils/Settings'); + +exports.eejsBlock_editbarMenuLeft = (hookName, args, cb) => { + if (args.renderContext.isReadOnly) return cb(); + + for (const button of ['alignLeft', 'alignJustify', 'alignCenter', 'alignRight']) { + if (JSON.stringify(settings.toolbar).indexOf(button) > -1) { + return cb(); + } + } + + args.content += eejs.require('ep_align/templates/editbarButtons.ejs'); + return cb(); +}; + +const _analyzeLine = (alineAttrs, apool) => { + let alignment = null; + if (alineAttrs) { + const opIter = Changeset.opIterator(alineAttrs); + if (opIter.hasNext()) { + const op = opIter.next(); + alignment = Changeset.opAttributeValue(op, 'align', apool); + } + } + return alignment; +}; + +// line, apool,attribLine,text +exports.getLineHTMLForExport = async (hookName, context) => { + const align = _analyzeLine(context.attribLine, context.apool); + if (align) { + if (context.text.indexOf('*') === 0) { + context.lineContent = context.lineContent.replace('*', ''); + } + const heading = context.lineContent.match(/]+)?>/); + + if (heading) { + if (heading.indexOf('style=') === -1) { + context.lineContent = context.lineContent.replace('>', ` style='text-align:${align}'>`); + } else { + context.lineContent = context.lineContent.replace('style=', `style='text-align:${align} `); + } + } else { + context.lineContent = + `

${context.lineContent}

`; + } + return context.lineContent; + } +}; + +exports.padInitToolbar = (hookName, args, cb) => { + const toolbar = args.toolbar; + + const alignLeftButton = toolbar.button({ + command: 'alignLeft', + localizationId: 'ep_align.toolbar.left.title', + class: 'buttonicon buttonicon-align-left ep_align ep_align_left', + }); + + const alignCenterButton = toolbar.button({ + command: 'alignCenter', + localizationId: 'ep_align.toolbar.middle.title', + class: 'buttonicon buttonicon-align-center ep_align ep_align_center', + }); + + const alignJustifyButton = toolbar.button({ + command: 'alignJustify', + localizationId: 'ep_align.toolbar.justify.title', + class: 'buttonicon buttonicon-align-justify ep_align ep_align_justify', + }); + + const alignRightButton = toolbar.button({ + command: 'alignRight', + localizationId: 'ep_align.toolbar.right.title', + class: 'buttonicon buttonicon-align-right ep_align ep_align_right', + }); + + toolbar.registerButton('alignLeft', alignLeftButton); + toolbar.registerButton('alignCenter', alignCenterButton); + toolbar.registerButton('alignJustify', alignJustifyButton); + toolbar.registerButton('alignRight', alignRightButton); + + return cb(); +}; diff --git a/src/plugins/md_align/locales/ca.json b/src/plugins/md_align/locales/ca.json new file mode 100644 index 000000000..4947c6173 --- /dev/null +++ b/src/plugins/md_align/locales/ca.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "Mguix" + ] + }, + "ep_align.align": "Alinear", + "ep_align.toolbar.left.title": "Esquerra", + "ep_align.toolbar.center.title": "Centre", + "ep_align.toolbar.right.title": "Dreta", + "ep_align.toolbar.justify.title": "Justifica" +} diff --git a/src/plugins/md_align/locales/cs.json b/src/plugins/md_align/locales/cs.json new file mode 100644 index 000000000..85a2c6b5b --- /dev/null +++ b/src/plugins/md_align/locales/cs.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "Spotter" + ] + }, + "ep_align.align": "Zarovnání", + "ep_align.toolbar.left.title": "Vlevo", + "ep_align.toolbar.center.title": "Střed", + "ep_align.toolbar.right.title": "Vpravo", + "ep_align.toolbar.justify.title": "Do bloku" +} diff --git a/src/plugins/md_align/locales/cy.json b/src/plugins/md_align/locales/cy.json new file mode 100644 index 000000000..3d61916e3 --- /dev/null +++ b/src/plugins/md_align/locales/cy.json @@ -0,0 +1,11 @@ +{ + "@metadata": { + "authors": [ + "Robin Owain" + ] + }, + "ep_align.align": "Alinio", + "ep_align.toolbar.left.title": "Chwith", + "ep_align.toolbar.right.title": "De", + "ep_align.toolbar.justify.title": "Unioni" +} diff --git a/src/plugins/md_align/locales/da.json b/src/plugins/md_align/locales/da.json new file mode 100644 index 000000000..efab41584 --- /dev/null +++ b/src/plugins/md_align/locales/da.json @@ -0,0 +1,9 @@ +{ + "@metadata": { + "authors": [ + "Saederup92" + ] + }, + "ep_align.toolbar.left.title": "Venstre", + "ep_align.toolbar.right.title": "Højre" +} diff --git a/src/plugins/md_align/locales/de.json b/src/plugins/md_align/locales/de.json new file mode 100644 index 000000000..df7ae727f --- /dev/null +++ b/src/plugins/md_align/locales/de.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "Lorisobi" + ] + }, + "ep_align.align": "Ausrichtung", + "ep_align.toolbar.left.title": "Linksbündig", + "ep_align.toolbar.center.title": "Zentrieren", + "ep_align.toolbar.right.title": "Rechtsbündig", + "ep_align.toolbar.justify.title": "Blocksatz" +} diff --git a/src/plugins/md_align/locales/diq.json b/src/plugins/md_align/locales/diq.json new file mode 100644 index 000000000..aff1443db --- /dev/null +++ b/src/plugins/md_align/locales/diq.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "1917 Ekim Devrimi" + ] + }, + "ep_align.align": "Ratnayış", + "ep_align.toolbar.left.title": "Çep", + "ep_align.toolbar.center.title": "Merkez", + "ep_align.toolbar.right.title": "Raşt", + "ep_align.toolbar.justify.title": "Heq" +} diff --git a/src/plugins/md_align/locales/en.json b/src/plugins/md_align/locales/en.json new file mode 100644 index 000000000..549cd82ed --- /dev/null +++ b/src/plugins/md_align/locales/en.json @@ -0,0 +1,7 @@ +{ + "ep_align.align" : "Align", + "ep_align.toolbar.left.title" : "Left", + "ep_align.toolbar.center.title" : "Center", + "ep_align.toolbar.right.title" : "Right", + "ep_align.toolbar.justify.title" : "Justify" +} diff --git a/src/plugins/md_align/locales/eu.json b/src/plugins/md_align/locales/eu.json new file mode 100644 index 000000000..9b9fa1229 --- /dev/null +++ b/src/plugins/md_align/locales/eu.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "Izendegi" + ] + }, + "ep_align.align": "Lerrokatu", + "ep_align.toolbar.left.title": "Ezkerrera", + "ep_align.toolbar.center.title": "Erdian", + "ep_align.toolbar.right.title": "Eskuinera", + "ep_align.toolbar.justify.title": "Justifikatu" +} diff --git a/src/plugins/md_align/locales/fi.json b/src/plugins/md_align/locales/fi.json new file mode 100644 index 000000000..6cbff8768 --- /dev/null +++ b/src/plugins/md_align/locales/fi.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "Maantietäjä" + ] + }, + "ep_align.align": "Kohdista", + "ep_align.toolbar.left.title": "Vasen", + "ep_align.toolbar.center.title": "Keskellä", + "ep_align.toolbar.right.title": "Oikea", + "ep_align.toolbar.justify.title": "Perustele" +} diff --git a/src/plugins/md_align/locales/fr.json b/src/plugins/md_align/locales/fr.json new file mode 100644 index 000000000..f988d2fe5 --- /dev/null +++ b/src/plugins/md_align/locales/fr.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "Florian COLLIN", + "Gomoko", + "Verdy p" + ] + }, + "ep_align.align": "Alignement", + "ep_align.toolbar.left.title": "À gauche", + "ep_align.toolbar.center.title": "Centré", + "ep_align.toolbar.right.title": "À droite", + "ep_align.toolbar.justify.title": "Justifié" +} diff --git a/src/plugins/md_align/locales/gl.json b/src/plugins/md_align/locales/gl.json new file mode 100644 index 000000000..8af87928c --- /dev/null +++ b/src/plugins/md_align/locales/gl.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "Ghose" + ] + }, + "ep_align.align": "Aliñar", + "ep_align.toolbar.left.title": "Esquerda", + "ep_align.toolbar.center.title": "Centro", + "ep_align.toolbar.right.title": "Dereita", + "ep_align.toolbar.justify.title": "Xustificar" +} diff --git a/src/plugins/md_align/locales/he.json b/src/plugins/md_align/locales/he.json new file mode 100644 index 000000000..7ad67faaa --- /dev/null +++ b/src/plugins/md_align/locales/he.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "YaronSh" + ] + }, + "ep_align.align": "יישור", + "ep_align.toolbar.left.title": "שמאל", + "ep_align.toolbar.center.title": "מרכז", + "ep_align.toolbar.right.title": "ימין", + "ep_align.toolbar.justify.title": "פיזור שוויוני" +} diff --git a/src/plugins/md_align/locales/hu.json b/src/plugins/md_align/locales/hu.json new file mode 100644 index 000000000..17dce989d --- /dev/null +++ b/src/plugins/md_align/locales/hu.json @@ -0,0 +1,11 @@ +{ + "@metadata": { + "authors": [ + "Hanna Tardos" + ] + }, + "ep_align.align": "Illesztés", + "ep_align.toolbar.left.title": "Balra igazítás", + "ep_align.toolbar.right.title": "Jobbra igazítás", + "ep_align.toolbar.justify.title": "Sorkizárt" +} diff --git a/src/plugins/md_align/locales/it.json b/src/plugins/md_align/locales/it.json new file mode 100644 index 000000000..ddd30a6a7 --- /dev/null +++ b/src/plugins/md_align/locales/it.json @@ -0,0 +1,13 @@ +{ + "@metadata": { + "authors": [ + "Beta16", + "VamosErik88" + ] + }, + "ep_align.align": "Allinea", + "ep_align.toolbar.left.title": "Sinistra", + "ep_align.toolbar.center.title": "Centro", + "ep_align.toolbar.right.title": "Destra", + "ep_align.toolbar.justify.title": "Giustificato" +} diff --git a/src/plugins/md_align/locales/ko.json b/src/plugins/md_align/locales/ko.json new file mode 100644 index 000000000..3ac58ce02 --- /dev/null +++ b/src/plugins/md_align/locales/ko.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "Ykhwong" + ] + }, + "ep_align.align": "정렬", + "ep_align.toolbar.left.title": "왼쪽", + "ep_align.toolbar.center.title": "가운데", + "ep_align.toolbar.right.title": "오른쪽", + "ep_align.toolbar.justify.title": "균등" +} diff --git a/src/plugins/md_align/locales/mk.json b/src/plugins/md_align/locales/mk.json new file mode 100644 index 000000000..3abd4d3fb --- /dev/null +++ b/src/plugins/md_align/locales/mk.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "Bjankuloski06" + ] + }, + "ep_align.align": "Порамни", + "ep_align.toolbar.left.title": "Лево", + "ep_align.toolbar.center.title": "По средина", + "ep_align.toolbar.right.title": "Десно", + "ep_align.toolbar.justify.title": "Порамни пасус" +} diff --git a/src/plugins/md_align/locales/my.json b/src/plugins/md_align/locales/my.json new file mode 100644 index 000000000..90240cdaa --- /dev/null +++ b/src/plugins/md_align/locales/my.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "Andibecker" + ] + }, + "ep_align.align": "ညှိပါ", + "ep_align.toolbar.left.title": "ဘယ်ဘက်", + "ep_align.toolbar.center.title": "စင်တာ", + "ep_align.toolbar.right.title": "မှန်တယ်", + "ep_align.toolbar.justify.title": "အကြောင်းပြပါ" +} diff --git a/src/plugins/md_align/locales/nl.json b/src/plugins/md_align/locales/nl.json new file mode 100644 index 000000000..b3c5768a1 --- /dev/null +++ b/src/plugins/md_align/locales/nl.json @@ -0,0 +1,11 @@ +{ + "@metadata": { + "authors": [ + "woeterman_94" + ] + }, + "ep_align.align": "Uitlijnen", + "ep_align.toolbar.left.title": "Links", + "ep_align.toolbar.right.title": "Rechts", + "ep_align.toolbar.justify.title": "Uitvullen" +} diff --git a/src/plugins/md_align/locales/oc.json b/src/plugins/md_align/locales/oc.json new file mode 100644 index 000000000..4380bd023 --- /dev/null +++ b/src/plugins/md_align/locales/oc.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "Quentí" + ] + }, + "ep_align.align": "Alinhament", + "ep_align.toolbar.left.title": "Esquèrra", + "ep_align.toolbar.center.title": "Centre", + "ep_align.toolbar.right.title": "Drecha", + "ep_align.toolbar.justify.title": "Justifica" +} diff --git a/src/plugins/md_align/locales/pl.json b/src/plugins/md_align/locales/pl.json new file mode 100644 index 000000000..dddad2bc3 --- /dev/null +++ b/src/plugins/md_align/locales/pl.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "Rail" + ] + }, + "ep_align.align": "Wyrównanie", + "ep_align.toolbar.left.title": "Do lewej", + "ep_align.toolbar.center.title": "Wyśrodkowanie", + "ep_align.toolbar.right.title": "Do prawej", + "ep_align.toolbar.justify.title": "Justowanie" +} diff --git a/src/plugins/md_align/locales/pms.json b/src/plugins/md_align/locales/pms.json new file mode 100644 index 000000000..3492cdb9d --- /dev/null +++ b/src/plugins/md_align/locales/pms.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "Borichèt" + ] + }, + "ep_align.align": "Aliniament", + "ep_align.toolbar.left.title": "Snistra", + "ep_align.toolbar.center.title": "Sènter", + "ep_align.toolbar.right.title": "Drita", + "ep_align.toolbar.justify.title": "Giustifiché" +} diff --git a/src/plugins/md_align/locales/pt-br.json b/src/plugins/md_align/locales/pt-br.json new file mode 100644 index 000000000..d5dd91309 --- /dev/null +++ b/src/plugins/md_align/locales/pt-br.json @@ -0,0 +1,13 @@ +{ + "@metadata": { + "authors": [ + "Eduardo Addad de Oliveira", + "Eduardoaddad" + ] + }, + "ep_align.align": "Alinhar", + "ep_align.toolbar.left.title": "Esquerda", + "ep_align.toolbar.center.title": "Centro", + "ep_align.toolbar.right.title": "Direita", + "ep_align.toolbar.justify.title": "Justificar" +} diff --git a/src/plugins/md_align/locales/pt.json b/src/plugins/md_align/locales/pt.json new file mode 100644 index 000000000..29cdee1b7 --- /dev/null +++ b/src/plugins/md_align/locales/pt.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "Guilha" + ] + }, + "ep_align.align": "Alinhar", + "ep_align.toolbar.left.title": "Esquerda", + "ep_align.toolbar.center.title": "Centro", + "ep_align.toolbar.right.title": "Direita", + "ep_align.toolbar.justify.title": "Justificado" +} diff --git a/src/plugins/md_align/locales/roa-tara.json b/src/plugins/md_align/locales/roa-tara.json new file mode 100644 index 000000000..31ec9c3a1 --- /dev/null +++ b/src/plugins/md_align/locales/roa-tara.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "Joetaras" + ] + }, + "ep_align.align": "Allineamende", + "ep_align.toolbar.left.title": "Sinistre", + "ep_align.toolbar.center.title": "Cendre", + "ep_align.toolbar.right.title": "Destre", + "ep_align.toolbar.justify.title": "Giustificate" +} diff --git a/src/plugins/md_align/locales/ru.json b/src/plugins/md_align/locales/ru.json new file mode 100644 index 000000000..78855f70d --- /dev/null +++ b/src/plugins/md_align/locales/ru.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "Okras" + ] + }, + "ep_align.align": "Выравнивание", + "ep_align.toolbar.left.title": "Слева", + "ep_align.toolbar.center.title": "По центру", + "ep_align.toolbar.right.title": "Справа", + "ep_align.toolbar.justify.title": "Выровнять" +} diff --git a/src/plugins/md_align/locales/sk.json b/src/plugins/md_align/locales/sk.json new file mode 100644 index 000000000..3c00bfac4 --- /dev/null +++ b/src/plugins/md_align/locales/sk.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "Yardom78" + ] + }, + "ep_align.align": "Zarovnať", + "ep_align.toolbar.left.title": "Vľavo", + "ep_align.toolbar.center.title": "Na stred", + "ep_align.toolbar.right.title": "Vpravo", + "ep_align.toolbar.justify.title": "Zarovnať" +} diff --git a/src/plugins/md_align/locales/sl.json b/src/plugins/md_align/locales/sl.json new file mode 100644 index 000000000..cadc6e347 --- /dev/null +++ b/src/plugins/md_align/locales/sl.json @@ -0,0 +1,9 @@ +{ + "@metadata": { + "authors": [ + "HairyFotr" + ] + }, + "ep_align.toolbar.left.title": "Levo", + "ep_align.toolbar.right.title": "Desno" +} diff --git a/src/plugins/md_align/locales/sq.json b/src/plugins/md_align/locales/sq.json new file mode 100644 index 000000000..5b48f8898 --- /dev/null +++ b/src/plugins/md_align/locales/sq.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "Besnik b" + ] + }, + "ep_align.align": "Drejtim", + "ep_align.toolbar.left.title": "Majtas", + "ep_align.toolbar.center.title": "Në qendër", + "ep_align.toolbar.right.title": "Djathtas", + "ep_align.toolbar.justify.title": "Përligje" +} diff --git a/src/plugins/md_align/locales/sv.json b/src/plugins/md_align/locales/sv.json new file mode 100644 index 000000000..0ddfb644f --- /dev/null +++ b/src/plugins/md_align/locales/sv.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "WikiPhoenix" + ] + }, + "ep_align.align": "Textjustering", + "ep_align.toolbar.left.title": "Vänsterjustera", + "ep_align.toolbar.center.title": "Centrera", + "ep_align.toolbar.right.title": "Högerjustera", + "ep_align.toolbar.justify.title": "Marginaljustera" +} diff --git a/src/plugins/md_align/locales/sw.json b/src/plugins/md_align/locales/sw.json new file mode 100644 index 000000000..ac685cc11 --- /dev/null +++ b/src/plugins/md_align/locales/sw.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "Andibecker" + ] + }, + "ep_align.align": "Panga", + "ep_align.toolbar.left.title": "Kushoto", + "ep_align.toolbar.center.title": "Kituo", + "ep_align.toolbar.right.title": "Haki", + "ep_align.toolbar.justify.title": "Thibitisha" +} diff --git a/src/plugins/md_align/locales/th.json b/src/plugins/md_align/locales/th.json new file mode 100644 index 000000000..55c2c79f2 --- /dev/null +++ b/src/plugins/md_align/locales/th.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "Andibecker" + ] + }, + "ep_align.align": "จัดตำแหน่ง", + "ep_align.toolbar.left.title": "ซ้าย", + "ep_align.toolbar.center.title": "ศูนย์กลาง", + "ep_align.toolbar.right.title": "ขวา", + "ep_align.toolbar.justify.title": "เหตุผล" +} diff --git a/src/plugins/md_align/locales/tl.json b/src/plugins/md_align/locales/tl.json new file mode 100644 index 000000000..018f07774 --- /dev/null +++ b/src/plugins/md_align/locales/tl.json @@ -0,0 +1,10 @@ +{ + "@metadata": { + "authors": [ + "Mrkczr" + ] + }, + "ep_align.align": "Ihanay", + "ep_align.toolbar.left.title": "Kaliwa", + "ep_align.toolbar.right.title": "Kanan" +} diff --git a/src/plugins/md_align/locales/tr.json b/src/plugins/md_align/locales/tr.json new file mode 100644 index 000000000..be776015c --- /dev/null +++ b/src/plugins/md_align/locales/tr.json @@ -0,0 +1,13 @@ +{ + "@metadata": { + "authors": [ + "Hedda", + "MuratTheTurkish" + ] + }, + "ep_align.align": "Hizalama", + "ep_align.toolbar.left.title": "Sol", + "ep_align.toolbar.center.title": "Merkez", + "ep_align.toolbar.right.title": "Sağ", + "ep_align.toolbar.justify.title": "Yasla" +} diff --git a/src/plugins/md_align/locales/uk.json b/src/plugins/md_align/locales/uk.json new file mode 100644 index 000000000..93f55ceae --- /dev/null +++ b/src/plugins/md_align/locales/uk.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "DDPAT" + ] + }, + "ep_align.align": "Вирівняти", + "ep_align.toolbar.left.title": "Ліворуч", + "ep_align.toolbar.center.title": "По центру", + "ep_align.toolbar.right.title": "Праворуч", + "ep_align.toolbar.justify.title": "Обґрунтуйте" +} diff --git a/src/plugins/md_align/locales/zh-hans.json b/src/plugins/md_align/locales/zh-hans.json new file mode 100644 index 000000000..8ae2e10e2 --- /dev/null +++ b/src/plugins/md_align/locales/zh-hans.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "列维劳德" + ] + }, + "ep_align.align": "对齐", + "ep_align.toolbar.left.title": "左对齐", + "ep_align.toolbar.center.title": "居中", + "ep_align.toolbar.right.title": "右对齐", + "ep_align.toolbar.justify.title": "左右对齐" +} diff --git a/src/plugins/md_align/locales/zh-hant.json b/src/plugins/md_align/locales/zh-hant.json new file mode 100644 index 000000000..9d7f6c6b4 --- /dev/null +++ b/src/plugins/md_align/locales/zh-hant.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "Kly" + ] + }, + "ep_align.align": "校準", + "ep_align.toolbar.left.title": "左", + "ep_align.toolbar.center.title": "中", + "ep_align.toolbar.right.title": "右", + "ep_align.toolbar.justify.title": "左右對齊" +} diff --git a/src/plugins/md_align/package.json b/src/plugins/md_align/package.json new file mode 100644 index 000000000..0b4baa0c7 --- /dev/null +++ b/src/plugins/md_align/package.json @@ -0,0 +1,71 @@ +{ + "_from": "ep_align", + "_id": "ep_align@0.0.1", + "_inBundle": false, + "_integrity": "sha512-QedMwuwHqgV24AERpNPXazZpGgnKn61zIo6rZHo9TyVIhNDRCZ9i9gtsZDNHNBPM+2XrcWjLIB45/LmAUMWuEw==", + "_location": "/ep_align", + "_phantomChildren": {}, + "_requested": { + "type": "tag", + "registry": true, + "raw": "ep_align", + "name": "ep_align", + "escapedName": "ep_align", + "rawSpec": "", + "saveSpec": null, + "fetchSpec": "latest" + }, + "_requiredBy": [ + "#USER" + ], + "_resolved": "https://registry.npmjs.org/ep_align/-/ep_align-0.0.1.tgz", + "_shasum": "8988e41f258aa6b0f5390da4efc035eb976c5e72", + "_spec": "ep_align", + "_where": "/Users/dev/Github/Edumatica/MuDocs/src", + "author": { + "name": "Akhil Naidu", + "email": "kaparapu.akhilnaidu@gmail.com" + }, + "bugs": { + "url": "https://github.com/akhil-naidu/ep_align/issues" + }, + "bundleDependencies": false, + "deprecated": false, + "description": "Mu Doc plugin to set left, center, right, or full justification for a paragraph", + "devDependencies": { + "eslint": "^7.32.0", + "eslint-config-etherpad": "^2.0.2", + "eslint-plugin-cypress": "^2.12.1", + "eslint-plugin-eslint-comments": "^3.2.0", + "eslint-plugin-mocha": "^9.0.0", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prefer-arrow": "^1.2.3", + "eslint-plugin-promise": "^5.1.1", + "eslint-plugin-you-dont-need-lodash-underscore": "^6.12.0" + }, + "engines": { + "node": ">=12.13.0" + }, + "eslintConfig": { + "root": true, + "extends": "etherpad/plugin" + }, + "homepage": "https://github.com/akhil-naidu/ep_align#readme", + "keywords": [ + "mudoc", + "plugin" + ], + "name": "ep_align", + "peerDependencies": { + "ep_etherpad-lite": ">=0.0.2" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/akhil-naidu/ep_align.git" + }, + "scripts": { + "lint": "eslint .", + "lint:fix": "eslint --fix ." + }, + "version": "0.0.1" +} diff --git a/src/plugins/md_align/static/js/index.js b/src/plugins/md_align/static/js/index.js new file mode 100644 index 000000000..8c3034e77 --- /dev/null +++ b/src/plugins/md_align/static/js/index.js @@ -0,0 +1,153 @@ +'use strict'; + +// All our tags are block elements, so we just return them. +const tags = ['left', 'center', 'justify', 'right']; + +const range = (start, end) => Array.from( + Array(Math.abs(end - start) + 1), + (_, i) => start + i +); + +exports.aceRegisterBlockElements = () => tags; + +// Bind the event handler to the toolbar buttons +exports.postAceInit = (hookName, context) => { + $('body').on('click', '.ep_align', function () { + const value = $(this).data('align'); + const intValue = parseInt(value, 10); + if (!isNaN(intValue)) { + context.ace.callWithAce((ace) => { + ace.ace_doInsertAlign(intValue); + }, 'insertalign', true); + } + }); + + return; +}; + +// On caret position change show the current align +exports.aceEditEvent = (hook, call) => { + // If it's not a click or a key event and the text hasn't changed then do nothing + const cs = call.callstack; + if (!(cs.type === 'handleClick') && !(cs.type === 'handleKeyEvent') && !(cs.docTextChanged)) { + return false; + } + // If it's an initial setup event then do nothing.. + if (cs.type === 'setBaseText' || cs.type === 'setup') return false; + + // It looks like we should check to see if this section has this attribute + return setTimeout(() => { // avoid race condition.. + const attributeManager = call.documentAttributeManager; + const rep = call.rep; + const activeAttributes = {}; + // $("#align-selection").val(-2); // TODO commented this out + + const firstLine = rep.selStart[0]; + const lastLine = Math.max(firstLine, rep.selEnd[0] - ((rep.selEnd[1] === 0) ? 1 : 0)); + let totalNumberOfLines = 0; + + range(firstLine, lastLine + 1).forEach((line) => { + totalNumberOfLines++; + const attr = attributeManager.getAttributeOnLine(line, 'align'); + if (!activeAttributes[attr]) { + activeAttributes[attr] = {}; + activeAttributes[attr].count = 1; + } else { + activeAttributes[attr].count++; + } + }); + + $.each(activeAttributes, (k, attr) => { + if (attr.count === totalNumberOfLines) { + // show as active class + // const ind = tags.indexOf(k); + // $("#align-selection").val(ind); // TODO commnented this out + } + }); + + return; + }, 250); +}; + +// Our align attribute will result in a heaading:left.... :left class +exports.aceAttribsToClasses = (hook, context) => { + if (context.key === 'align') { + return [`align:${context.value}`]; + } +}; + +// Here we convert the class align:left into a tag +exports.aceDomLineProcessLineAttributes = (name, context) => { + const cls = context.cls; + const alignType = /(?:^| )align:([A-Za-z0-9]*)/.exec(cls); + let tagIndex; + if (alignType) tagIndex = tags.indexOf(alignType[1]); + if (tagIndex !== undefined && tagIndex >= 0) { + const tag = tags[tagIndex]; + const styles = + `width:100%;margin:0 auto;list-style-position:inside;display:block;text-align:${tag}`; + const modifier = { + preHtml: `<${tag} style="${styles}">`, + postHtml: ``, + processedMarker: true, + }; + return [modifier]; + } + return []; +}; + + +// Once ace is initialized, we set ace_doInsertAlign and bind it to the context +exports.aceInitialized = (hook, context) => { + // Passing a level >= 0 will set a alignment on the selected lines, level < 0 + // will remove it + function doInsertAlign(level) { + const rep = this.rep; + const documentAttributeManager = this.documentAttributeManager; + if (!(rep.selStart && rep.selEnd) || (level >= 0 && tags[level] === undefined)) { + return; + } + + const firstLine = rep.selStart[0]; + const lastLine = Math.max(firstLine, rep.selEnd[0] - ((rep.selEnd[1] === 0) ? 1 : 0)); + range(firstLine, lastLine).forEach((i) => { + if (level >= 0) { + documentAttributeManager.setAttributeOnLine(i, 'align', tags[level]); + } else { + documentAttributeManager.removeAttributeOnLine(i, 'align'); + } + }); + } + + const editorInfo = context.editorInfo; + editorInfo.ace_doInsertAlign = doInsertAlign.bind(context); + return; +}; + +const align = (context, alignment) => { + context.ace.callWithAce((ace) => { + ace.ace_doInsertAlign(alignment); + ace.ace_focus(); + }, 'insertalign', true); +}; + +exports.postToolbarInit = (hookName, context) => { + const editbar = context.toolbar; // toolbar is actually editbar + editbar.registerCommand('alignLeft', () => { + align(context, 0); + }); + + editbar.registerCommand('alignCenter', () => { + align(context, 1); + }); + + editbar.registerCommand('alignJustify', () => { + align(context, 2); + }); + + editbar.registerCommand('alignRight', () => { + align(context, 3); + }); + + return true; +}; diff --git a/src/plugins/md_align/static/js/shared.js b/src/plugins/md_align/static/js/shared.js new file mode 100644 index 000000000..119dd3ff1 --- /dev/null +++ b/src/plugins/md_align/static/js/shared.js @@ -0,0 +1,29 @@ +'use strict'; + +const tags = ['left', 'center', 'justify', 'right']; + +exports.collectContentPre = (hookName, context, cb) => { + const tname = context.tname; + const state = context.state; + const lineAttributes = state.lineAttributes; + const tagIndex = tags.indexOf(tname); + if (tname === 'div' || tname === 'p') { + delete lineAttributes.align; + } + if (tagIndex >= 0) { + lineAttributes.align = tags[tagIndex]; + } + return cb(); +}; + +// I don't even know when this is run.. +exports.collectContentPost = (hookName, context, cb) => { + const tname = context.tname; + const state = context.state; + const lineAttributes = state.lineAttributes; + const tagIndex = tags.indexOf(tname); + if (tagIndex >= 0) { + delete lineAttributes.align; + } + return cb(); +}; diff --git a/src/plugins/md_align/templates/editbarButtons.ejs b/src/plugins/md_align/templates/editbarButtons.ejs new file mode 100644 index 000000000..aaca1222c --- /dev/null +++ b/src/plugins/md_align/templates/editbarButtons.ejs @@ -0,0 +1,25 @@ +
  • + +
  • + + + +
  • + +
  • + + + +
  • + +
  • + + + +
  • + +
  • + + + +
  • diff --git a/src/plugins/md_all_headings/README.md b/src/plugins/md_all_headings/README.md new file mode 100644 index 000000000..b26fd5601 --- /dev/null +++ b/src/plugins/md_all_headings/README.md @@ -0,0 +1 @@ +Headings in MuDoc \ No newline at end of file diff --git a/src/plugins/md_all_headings/ep.json b/src/plugins/md_all_headings/ep.json new file mode 100644 index 000000000..778d287ee --- /dev/null +++ b/src/plugins/md_all_headings/ep.json @@ -0,0 +1,24 @@ +{ + "parts": [ + { + "name": "main", + "client_hooks": { + "aceEditorCSS": "ep_all_headings/static/js/index", + "aceEditEvent": "ep_all_headings/static/js/index", + "aceDomLineProcessLineAttributes": "ep_all_headings/static/js/index", + "postAceInit": "ep_all_headings/static/js/index", + "aceInitialized": "ep_all_headings/static/js/index", + "aceAttribsToClasses": "ep_all_headings/static/js/index", + "collectContentPre": "ep_all_headings/static/js/shared", + "aceRegisterBlockElements": "ep_all_headings/static/js/index" + }, + "hooks": { + "eejsBlock_editbarMenuLeft": "ep_all_headings/index", + "collectContentPre": "ep_all_headings/static/js/shared", + "collectContentPost": "ep_all_headings/static/js/shared", + "getLineHTMLForExport": "ep_all_headings/index", + "stylesForExport" : "ep_all_headings/index" + } + } + ] +} diff --git a/src/plugins/md_all_headings/index.js b/src/plugins/md_all_headings/index.js new file mode 100644 index 000000000..3a9e65f6f --- /dev/null +++ b/src/plugins/md_all_headings/index.js @@ -0,0 +1,48 @@ +'use strict'; + +const eejs = require('ep_etherpad-lite/node/eejs/'); +const Changeset = require('ep_etherpad-lite/static/js/Changeset'); + +exports.eejsBlock_editbarMenuLeft = (hookName, args, cb) => { + args.content += eejs.require('ep_all_headings/templates/editbarButtons.ejs'); + return cb(); +}; + +// Include CSS for HTML export +exports.stylesForExport = () => ( + // These should be consistent with client CSS. + 'h1{font-size: 2.5em;}\n' + + 'h2{font-size: 1.8em;}\n' + + 'h3{font-size: 1.5em;}\n' + + 'h4{font-size: 1.2em;}\n' + + 'code{font-family: RobotoMono;}\n'); + +const _analyzeLine = (alineAttrs, apool) => { + let header = null; + if (alineAttrs) { + const opIter = Changeset.opIterator(alineAttrs); + if (opIter.hasNext()) { + const op = opIter.next(); + header = Changeset.opAttributeValue(op, 'heading', apool); + } + } + return header; +}; + +// line, apool,attribLine,text +exports.getLineHTMLForExport = async (hookName, context) => { + const header = _analyzeLine(context.attribLine, context.apool); + if (header) { + if (context.text.indexOf('*') === 0) { + context.lineContent = context.lineContent.replace('*', ''); + } + const paragraph = context.lineContent.match(/]+)?>/); + if (paragraph) { + context.lineContent = context.lineContent.replace('', ``); + } else { + context.lineContent = `<${header}>${context.lineContent}`; + } + return context.lineContent; + } +}; diff --git a/src/plugins/md_all_headings/locales/ca.json b/src/plugins/md_all_headings/locales/ca.json new file mode 100644 index 000000000..e1bf5591a --- /dev/null +++ b/src/plugins/md_all_headings/locales/ca.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "Mguix" + ] + }, + "ep_headings.style": "Estil", + "ep_headings.normal": "Normal", + "ep_headings.h1": "Encapçalament 1", + "ep_headings.h2": "Encapçalament 2", + "ep_headings.h3": "Encapçalament 3", + "ep_headings.h4": "Encapçalament 4", + "ep_headings.code": "Codi" +} diff --git a/src/plugins/md_all_headings/locales/cs.json b/src/plugins/md_all_headings/locales/cs.json new file mode 100644 index 000000000..b0f2faf04 --- /dev/null +++ b/src/plugins/md_all_headings/locales/cs.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "Spotter" + ] + }, + "ep_headings.style": "Styl", + "ep_headings.normal": "Normální", + "ep_headings.h1": "Nadpis 1", + "ep_headings.h2": "Nadpis 2", + "ep_headings.h3": "Nadpis 3", + "ep_headings.h4": "Nadpis 4", + "ep_headings.code": "Kód" +} diff --git a/src/plugins/md_all_headings/locales/cy.json b/src/plugins/md_all_headings/locales/cy.json new file mode 100644 index 000000000..affc9e574 --- /dev/null +++ b/src/plugins/md_all_headings/locales/cy.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "Robin Owain" + ] + }, + "ep_headings.style": "Math", + "ep_headings.normal": "Normal", + "ep_headings.h1": "Pennawd 1", + "ep_headings.h2": "Pennawd 2", + "ep_headings.h3": "Pennawd 3", + "ep_headings.h4": "Pennawd 4", + "ep_headings.code": "Cod" +} diff --git a/src/plugins/md_all_headings/locales/da.json b/src/plugins/md_all_headings/locales/da.json new file mode 100644 index 000000000..c294b1ea7 --- /dev/null +++ b/src/plugins/md_all_headings/locales/da.json @@ -0,0 +1,9 @@ +{ + "@metadata": { + "authors": [ + "Saederup92" + ] + }, + "ep_headings.style": "Stil", + "ep_headings.code": "Kode" +} diff --git a/src/plugins/md_all_headings/locales/de.json b/src/plugins/md_all_headings/locales/de.json new file mode 100644 index 000000000..1209f5f99 --- /dev/null +++ b/src/plugins/md_all_headings/locales/de.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [] + }, + "ep_headings.style": "Stil", + "ep_headings.normal": "Normal", + "ep_headings.h1": "Überschrift 1", + "ep_headings.h2": "Überschrift 2", + "ep_headings.h3": "Überschrift 3", + "ep_headings.h4": "Überschrift 4", + "ep_headings.code": "Code" +} diff --git a/src/plugins/md_all_headings/locales/diq.json b/src/plugins/md_all_headings/locales/diq.json new file mode 100644 index 000000000..064e13e1a --- /dev/null +++ b/src/plugins/md_all_headings/locales/diq.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "1917 Ekim Devrimi" + ] + }, + "ep_headings.style": "Style", + "ep_headings.normal": "Normal", + "ep_headings.h1": "Sername 1", + "ep_headings.h2": "Sername 2", + "ep_headings.h3": "Sername 3", + "ep_headings.h4": "Sername 4", + "ep_headings.code": "Kod" +} diff --git a/src/plugins/md_all_headings/locales/en.json b/src/plugins/md_all_headings/locales/en.json new file mode 100644 index 000000000..551d9beac --- /dev/null +++ b/src/plugins/md_all_headings/locales/en.json @@ -0,0 +1,9 @@ +{ + "ep_headings.style" : "Style", + "ep_headings.normal" : "Normal", + "ep_headings.h1" : "Heading 1", + "ep_headings.h2" : "Heading 2", + "ep_headings.h3" : "Heading 3", + "ep_headings.h4" : "Heading 4", + "ep_headings.code" : "Code" +} diff --git a/src/plugins/md_all_headings/locales/et.json b/src/plugins/md_all_headings/locales/et.json new file mode 100644 index 000000000..9294c71ee --- /dev/null +++ b/src/plugins/md_all_headings/locales/et.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [] + }, + "ep_headings.style": "Stiilid", + "ep_headings.normal": "Tavaline tekst", + "ep_headings.h1": "Pealkiri 1", + "ep_headings.h2": "Pealkiri 2", + "ep_headings.h3": "Pealkiri 3", + "ep_headings.h4": "Pealkiri 4", + "ep_headings.code": "Kood" +} diff --git a/src/plugins/md_all_headings/locales/eu.json b/src/plugins/md_all_headings/locales/eu.json new file mode 100644 index 000000000..09e90037f --- /dev/null +++ b/src/plugins/md_all_headings/locales/eu.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "Izendegi" + ] + }, + "ep_headings.style": "Estiloa", + "ep_headings.normal": "Normala", + "ep_headings.h1": "Goiburua 1", + "ep_headings.h2": "Goiburua 2", + "ep_headings.h3": "Goiburua 3", + "ep_headings.h4": "Goiburua 4", + "ep_headings.code": "Kodea" +} diff --git a/src/plugins/md_all_headings/locales/fi.json b/src/plugins/md_all_headings/locales/fi.json new file mode 100644 index 000000000..dfe962100 --- /dev/null +++ b/src/plugins/md_all_headings/locales/fi.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "Maantietäjä" + ] + }, + "ep_headings.style": "Tyyli", + "ep_headings.normal": "Normaali", + "ep_headings.h1": "Otsikko 1", + "ep_headings.h2": "Otsikko 2", + "ep_headings.h3": "Otsikko 3", + "ep_headings.h4": "Otsikko 4", + "ep_headings.code": "Koodi" +} diff --git a/src/plugins/md_all_headings/locales/fr.json b/src/plugins/md_all_headings/locales/fr.json new file mode 100644 index 000000000..c782aa20d --- /dev/null +++ b/src/plugins/md_all_headings/locales/fr.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "Verdy p" + ] + }, + "ep_headings.style": "Style", + "ep_headings.normal": "Normal", + "ep_headings.h1": "Titre 1", + "ep_headings.h2": "Titre 2", + "ep_headings.h3": "Titre 3", + "ep_headings.h4": "Titre 4", + "ep_headings.code": "Code" +} diff --git a/src/plugins/md_all_headings/locales/gl.json b/src/plugins/md_all_headings/locales/gl.json new file mode 100644 index 000000000..bccf1bb99 --- /dev/null +++ b/src/plugins/md_all_headings/locales/gl.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "Ghose" + ] + }, + "ep_headings.style": "Estilo", + "ep_headings.normal": "Normal", + "ep_headings.h1": "Cabeceira 1", + "ep_headings.h2": "Cabeceira 2", + "ep_headings.h3": "Cabeceira 3", + "ep_headings.h4": "Cabeceira 4", + "ep_headings.code": "Código" +} diff --git a/src/plugins/md_all_headings/locales/he.json b/src/plugins/md_all_headings/locales/he.json new file mode 100644 index 000000000..bd843929e --- /dev/null +++ b/src/plugins/md_all_headings/locales/he.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "YaronSh" + ] + }, + "ep_headings.style": "סגנון", + "ep_headings.normal": "רגיל", + "ep_headings.h1": "כותרת רמה 1", + "ep_headings.h2": "כותרת רמה 2", + "ep_headings.h3": "כותרת רמה 3", + "ep_headings.h4": "כותרת רמה 4", + "ep_headings.code": "קוד" +} diff --git a/src/plugins/md_all_headings/locales/hu.json b/src/plugins/md_all_headings/locales/hu.json new file mode 100644 index 000000000..0675fae65 --- /dev/null +++ b/src/plugins/md_all_headings/locales/hu.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [] + }, + "ep_headings.style": "Stílus", + "ep_headings.normal": "Normál", + "ep_headings.h1": "Címsor 1", + "ep_headings.h2": "Címsor 2", + "ep_headings.h3": "Címsor 3", + "ep_headings.h4": "Címsor 4", + "ep_headings.code": "Kód" +} diff --git a/src/plugins/md_all_headings/locales/it.json b/src/plugins/md_all_headings/locales/it.json new file mode 100644 index 000000000..dfdb6a4eb --- /dev/null +++ b/src/plugins/md_all_headings/locales/it.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "Beta16" + ] + }, + "ep_headings.style": "Stile", + "ep_headings.normal": "Normale", + "ep_headings.h1": "Titolo 1", + "ep_headings.h2": "Titolo 2", + "ep_headings.h3": "Titolo 3", + "ep_headings.h4": "Titolo 4", + "ep_headings.code": "Codice" +} diff --git a/src/plugins/md_all_headings/locales/ko.json b/src/plugins/md_all_headings/locales/ko.json new file mode 100644 index 000000000..e3532ef7f --- /dev/null +++ b/src/plugins/md_all_headings/locales/ko.json @@ -0,0 +1,15 @@ +{ + "@metadata": { + "authors": [ + "Ykhwong", + "그냥기여자" + ] + }, + "ep_headings.style": "스타일", + "ep_headings.normal": "표준", + "ep_headings.h1": "제목 1", + "ep_headings.h2": "제목 2", + "ep_headings.h3": "제목 3", + "ep_headings.h4": "제목 4", + "ep_headings.code": "코드" +} diff --git a/src/plugins/md_all_headings/locales/mk.json b/src/plugins/md_all_headings/locales/mk.json new file mode 100644 index 000000000..85fe8e06f --- /dev/null +++ b/src/plugins/md_all_headings/locales/mk.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "Bjankuloski06" + ] + }, + "ep_headings.style": "Стил", + "ep_headings.normal": "Нормален", + "ep_headings.h1": "Наслов 1", + "ep_headings.h2": "Наслов 2", + "ep_headings.h3": "Наслов 3", + "ep_headings.h4": "Наслов 4", + "ep_headings.code": "Код" +} diff --git a/src/plugins/md_all_headings/locales/my.json b/src/plugins/md_all_headings/locales/my.json new file mode 100644 index 000000000..91247a5f1 --- /dev/null +++ b/src/plugins/md_all_headings/locales/my.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "Andibecker" + ] + }, + "ep_headings.style": "ပုံစံ", + "ep_headings.normal": "ပုံမှန်ပါပဲ", + "ep_headings.h1": "ခေါင်းစဉ် ၁", + "ep_headings.h2": "ခေါင်းစီး ၂", + "ep_headings.h3": "ခေါင်းစဉ် ၃", + "ep_headings.h4": "ခေါင်းစဉ် ၄", + "ep_headings.code": "ကုဒ်" +} diff --git a/src/plugins/md_all_headings/locales/nl.json b/src/plugins/md_all_headings/locales/nl.json new file mode 100644 index 000000000..7e01bacf2 --- /dev/null +++ b/src/plugins/md_all_headings/locales/nl.json @@ -0,0 +1,15 @@ +{ + "@metadata": { + "authors": [ + "Aranka", + "woeterman_94" + ] + }, + "ep_headings.style": "Stijl", + "ep_headings.normal": "Normaal", + "ep_headings.h1": "Kop 1", + "ep_headings.h2": "Kop 2", + "ep_headings.h3": "Kop 3", + "ep_headings.h4": "Kop 4", + "ep_headings.code": "Code" +} diff --git a/src/plugins/md_all_headings/locales/oc.json b/src/plugins/md_all_headings/locales/oc.json new file mode 100644 index 000000000..61a4bad64 --- /dev/null +++ b/src/plugins/md_all_headings/locales/oc.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "Quentí" + ] + }, + "ep_headings.style": "Estil", + "ep_headings.normal": "Normal", + "ep_headings.h1": "Títol 1", + "ep_headings.h2": "Títol 2", + "ep_headings.h3": "Títol 3", + "ep_headings.h4": "Títol 4", + "ep_headings.code": "Còdi" +} diff --git a/src/plugins/md_all_headings/locales/pl.json b/src/plugins/md_all_headings/locales/pl.json new file mode 100644 index 000000000..118a3c087 --- /dev/null +++ b/src/plugins/md_all_headings/locales/pl.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [] + }, + "ep_headings.style": "Styl", + "ep_headings.normal": "Normalny", + "ep_headings.h1": "Nagłówek 1", + "ep_headings.h2": "Nagłówek 2", + "ep_headings.h3": "Nagłówek 3", + "ep_headings.h4": "Nagłówek 4", + "ep_headings.code": "Kod" +} diff --git a/src/plugins/md_all_headings/locales/pms.json b/src/plugins/md_all_headings/locales/pms.json new file mode 100644 index 000000000..414a1dbbb --- /dev/null +++ b/src/plugins/md_all_headings/locales/pms.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "Borichèt" + ] + }, + "ep_headings.style": "Stil", + "ep_headings.normal": "Normal", + "ep_headings.h1": "Antestassion 1", + "ep_headings.h2": "Antestassion 2", + "ep_headings.h3": "Antestassion 3", + "ep_headings.h4": "Antestassion 4", + "ep_headings.code": "Còdes" +} diff --git a/src/plugins/md_all_headings/locales/pt-br.json b/src/plugins/md_all_headings/locales/pt-br.json new file mode 100644 index 000000000..f1b0e0e5f --- /dev/null +++ b/src/plugins/md_all_headings/locales/pt-br.json @@ -0,0 +1,15 @@ +{ + "@metadata": { + "authors": [ + "Eduardo Addad de Oliveira", + "Eduardoaddad" + ] + }, + "ep_headings.style": "Estilo", + "ep_headings.normal": "Normal", + "ep_headings.h1": "Nível 1", + "ep_headings.h2": "Nível 2", + "ep_headings.h3": "Nível 3", + "ep_headings.h4": "Nível 4", + "ep_headings.code": "Código" +} diff --git a/src/plugins/md_all_headings/locales/pt.json b/src/plugins/md_all_headings/locales/pt.json new file mode 100644 index 000000000..3fe78dfc8 --- /dev/null +++ b/src/plugins/md_all_headings/locales/pt.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "Guilha" + ] + }, + "ep_headings.style": "Estilo", + "ep_headings.normal": "Normal", + "ep_headings.h1": "Título 1", + "ep_headings.h2": "Título 2", + "ep_headings.h3": "Título 3", + "ep_headings.h4": "Título 4", + "ep_headings.code": "Código" +} diff --git a/src/plugins/md_all_headings/locales/qqq.json b/src/plugins/md_all_headings/locales/qqq.json new file mode 100644 index 000000000..3810c7518 --- /dev/null +++ b/src/plugins/md_all_headings/locales/qqq.json @@ -0,0 +1,8 @@ +{ + "@metadata": { + "authors": [ + "BryanDavis" + ] + }, + "ep_headings.style": "{{Identical|Style}}" +} diff --git a/src/plugins/md_all_headings/locales/roa-tara.json b/src/plugins/md_all_headings/locales/roa-tara.json new file mode 100644 index 000000000..316ff7f7c --- /dev/null +++ b/src/plugins/md_all_headings/locales/roa-tara.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "Joetaras" + ] + }, + "ep_headings.style": "Stile", + "ep_headings.normal": "Normale", + "ep_headings.h1": "Testate 1", + "ep_headings.h2": "Testate 2", + "ep_headings.h3": "Testate 3", + "ep_headings.h4": "Testate 4", + "ep_headings.code": "Codece" +} diff --git a/src/plugins/md_all_headings/locales/ru.json b/src/plugins/md_all_headings/locales/ru.json new file mode 100644 index 000000000..435bfa21c --- /dev/null +++ b/src/plugins/md_all_headings/locales/ru.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [] + }, + "ep_headings.style": "Стили", + "ep_headings.normal": "Обычный текст", + "ep_headings.h1": "Заголовок 1", + "ep_headings.h2": "Заголовок 2", + "ep_headings.h3": "Заголовок 3", + "ep_headings.h4": "Заголовок 4", + "ep_headings.code": "Код" +} diff --git a/src/plugins/md_all_headings/locales/scn.json b/src/plugins/md_all_headings/locales/scn.json new file mode 100644 index 000000000..c01d0d0eb --- /dev/null +++ b/src/plugins/md_all_headings/locales/scn.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "Ajeje Brazorf" + ] + }, + "ep_headings.style": "Stili", + "ep_headings.normal": "Nurmali", + "ep_headings.h1": "Tìtulu 1", + "ep_headings.h2": "Tìtulu 2", + "ep_headings.h3": "Tìtulu 3", + "ep_headings.h4": "Tìtulu 4", + "ep_headings.code": "Còdici" +} diff --git a/src/plugins/md_all_headings/locales/sk.json b/src/plugins/md_all_headings/locales/sk.json new file mode 100644 index 000000000..43bf09578 --- /dev/null +++ b/src/plugins/md_all_headings/locales/sk.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "Yardom78" + ] + }, + "ep_headings.style": "Štýl", + "ep_headings.normal": "Normálne", + "ep_headings.h1": "Nadpis 1", + "ep_headings.h2": "Nadpis 2", + "ep_headings.h3": "Nadpis 3", + "ep_headings.h4": "Nadpis 4", + "ep_headings.code": "Kód" +} diff --git a/src/plugins/md_all_headings/locales/sl.json b/src/plugins/md_all_headings/locales/sl.json new file mode 100644 index 000000000..529bd375a --- /dev/null +++ b/src/plugins/md_all_headings/locales/sl.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [ + "HairyFotr" + ] + }, + "ep_headings.style": "Slog", + "ep_headings.h1": "Naslov 1", + "ep_headings.h2": "Naslov 2", + "ep_headings.h3": "Naslov 3", + "ep_headings.h4": "Naslov 4" +} diff --git a/src/plugins/md_all_headings/locales/sq.json b/src/plugins/md_all_headings/locales/sq.json new file mode 100644 index 000000000..5670058f8 --- /dev/null +++ b/src/plugins/md_all_headings/locales/sq.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "Besnik b" + ] + }, + "ep_headings.style": "Stil", + "ep_headings.normal": "Normal", + "ep_headings.h1": "Krye 1", + "ep_headings.h2": "Krye 2", + "ep_headings.h3": "Krye 3", + "ep_headings.h4": "Krye 4", + "ep_headings.code": "Kod" +} diff --git a/src/plugins/md_all_headings/locales/sv.json b/src/plugins/md_all_headings/locales/sv.json new file mode 100644 index 000000000..71c57224f --- /dev/null +++ b/src/plugins/md_all_headings/locales/sv.json @@ -0,0 +1,12 @@ +{ + "@metadata": { + "authors": [] + }, + "ep_headings.style": "Stil", + "ep_headings.normal": "Normal", + "ep_headings.h1": "Rubrik 1", + "ep_headings.h2": "Rubrik 2", + "ep_headings.h3": "Rubrik 3", + "ep_headings.h4": "Rubrik 4", + "ep_headings.code": "Kod" +} diff --git a/src/plugins/md_all_headings/locales/sw.json b/src/plugins/md_all_headings/locales/sw.json new file mode 100644 index 000000000..b0ab17604 --- /dev/null +++ b/src/plugins/md_all_headings/locales/sw.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "Andibecker" + ] + }, + "ep_headings.style": "Mtindo", + "ep_headings.normal": "Kawaida", + "ep_headings.h1": "Kichwa 1", + "ep_headings.h2": "Kichwa 2", + "ep_headings.h3": "Kichwa 3", + "ep_headings.h4": "Kichwa 4", + "ep_headings.code": "Kanuni" +} diff --git a/src/plugins/md_all_headings/locales/th.json b/src/plugins/md_all_headings/locales/th.json new file mode 100644 index 000000000..fc3710c56 --- /dev/null +++ b/src/plugins/md_all_headings/locales/th.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "Andibecker" + ] + }, + "ep_headings.style": "สไตล์", + "ep_headings.normal": "ปกติ", + "ep_headings.h1": "หัวเรื่อง 1", + "ep_headings.h2": "หัวเรื่อง 2", + "ep_headings.h3": "หัวเรื่อง 3", + "ep_headings.h4": "หัวเรื่อง 4", + "ep_headings.code": "รหัส" +} diff --git a/src/plugins/md_all_headings/locales/tl.json b/src/plugins/md_all_headings/locales/tl.json new file mode 100644 index 000000000..3015d8e66 --- /dev/null +++ b/src/plugins/md_all_headings/locales/tl.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "Mrkczr" + ] + }, + "ep_headings.style": "Istilo", + "ep_headings.normal": "Karaniwan", + "ep_headings.h1": "Pamuhatan 1", + "ep_headings.h2": "Pamuhatan 2", + "ep_headings.h3": "Pamuhatan 3", + "ep_headings.h4": "Pamuhatan 4", + "ep_headings.code": "Kodigo" +} diff --git a/src/plugins/md_all_headings/locales/tr.json b/src/plugins/md_all_headings/locales/tr.json new file mode 100644 index 000000000..08a31771d --- /dev/null +++ b/src/plugins/md_all_headings/locales/tr.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "MuratTheTurkish" + ] + }, + "ep_headings.style": "Stil", + "ep_headings.normal": "Normal", + "ep_headings.h1": "Başlık 1", + "ep_headings.h2": "Başlık 2", + "ep_headings.h3": "Başlık 3", + "ep_headings.h4": "Başlık 4", + "ep_headings.code": "Kod" +} diff --git a/src/plugins/md_all_headings/locales/uk.json b/src/plugins/md_all_headings/locales/uk.json new file mode 100644 index 000000000..237fdaa97 --- /dev/null +++ b/src/plugins/md_all_headings/locales/uk.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "DDPAT" + ] + }, + "ep_headings.style": "Стиль", + "ep_headings.normal": "Нормальний", + "ep_headings.h1": "Заголовок 1", + "ep_headings.h2": "Заголовок 2", + "ep_headings.h3": "Заголовок 3", + "ep_headings.h4": "Заголовок 4", + "ep_headings.code": "Код" +} diff --git a/src/plugins/md_all_headings/locales/zh-cn.json b/src/plugins/md_all_headings/locales/zh-cn.json new file mode 100644 index 000000000..5b501c1fc --- /dev/null +++ b/src/plugins/md_all_headings/locales/zh-cn.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "Kly" + ] + }, + "ep_headings.style": "样式", + "ep_headings.normal": "正文", + "ep_headings.h1": "标题 1", + "ep_headings.h2": "标题 2", + "ep_headings.h3": "标题 3", + "ep_headings.h4": "标题 4", + "ep_headings.code": "代码" +} diff --git a/src/plugins/md_all_headings/locales/zh-hans.json b/src/plugins/md_all_headings/locales/zh-hans.json new file mode 100644 index 000000000..fb0aa3cd8 --- /dev/null +++ b/src/plugins/md_all_headings/locales/zh-hans.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "列维劳德" + ] + }, + "ep_headings.style": "样式", + "ep_headings.normal": "正常", + "ep_headings.h1": "标题 1", + "ep_headings.h2": "标题 2", + "ep_headings.h3": "标题 3", + "ep_headings.h4": "标题 4", + "ep_headings.code": "代码" +} diff --git a/src/plugins/md_all_headings/locales/zh-hant.json b/src/plugins/md_all_headings/locales/zh-hant.json new file mode 100644 index 000000000..1aa22d89a --- /dev/null +++ b/src/plugins/md_all_headings/locales/zh-hant.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "Kly" + ] + }, + "ep_headings.style": "樣式", + "ep_headings.normal": "一般", + "ep_headings.h1": "標題 1", + "ep_headings.h2": "標題 2", + "ep_headings.h3": "標題 3", + "ep_headings.h4": "標題 4", + "ep_headings.code": "代碼" +} diff --git a/src/plugins/md_all_headings/package.json b/src/plugins/md_all_headings/package.json new file mode 100644 index 000000000..d8b97f704 --- /dev/null +++ b/src/plugins/md_all_headings/package.json @@ -0,0 +1,71 @@ +{ + "_from": "ep_all_headings", + "_id": "ep_all_headings@0.0.1", + "_inBundle": false, + "_integrity": "sha512-wi4NepzLTh4NhafeMPvEZsH9D73UQ8S+I5cK7Lup4UDuRMW4KRSm5UMu+g03uXDEBSLUPFQOK3HX0gzf4oTbLw==", + "_location": "/ep_all_headings", + "_phantomChildren": {}, + "_requested": { + "type": "tag", + "registry": true, + "raw": "ep_all_headings", + "name": "ep_all_headings", + "escapedName": "ep_all_headings", + "rawSpec": "", + "saveSpec": null, + "fetchSpec": "latest" + }, + "_requiredBy": [ + "#USER" + ], + "_resolved": "https://registry.npmjs.org/ep_all_headings/-/ep_all_headings-0.0.1.tgz", + "_shasum": "3581983c07bfd2f74bc98b79dab53e4427a81237", + "_spec": "ep_all_headings", + "_where": "/Users/dev/Github/Edumatica/MuDocs/src", + "author": { + "name": "Akhil Naidu", + "email": "kaparapu.akhilnaidu@gmail.com" + }, + "bugs": { + "url": "https://github.com/ether/ep_all_headings/issues" + }, + "bundleDependencies": false, + "deprecated": false, + "description": "Adds heading support to Mu Doc Lite.", + "devDependencies": { + "eslint": "^7.32.0", + "eslint-config-etherpad": "^2.0.2", + "eslint-plugin-cypress": "^2.12.1", + "eslint-plugin-eslint-comments": "^3.2.0", + "eslint-plugin-mocha": "^9.0.0", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prefer-arrow": "^1.2.3", + "eslint-plugin-promise": "^5.1.1", + "eslint-plugin-you-dont-need-lodash-underscore": "^6.12.0" + }, + "engines": { + "node": ">=12.13.0" + }, + "eslintConfig": { + "root": true, + "extends": "etherpad/plugin" + }, + "homepage": "https://github.com/akhil-naidu/ep_all_headings#readme", + "keywords": [ + "mudoc", + "plugin" + ], + "name": "ep_all_headings", + "peerDependencies": { + "ep_etherpad-lite": ">=0.0.2" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/akhil-nadiu/ep_all_headings.git" + }, + "scripts": { + "lint": "eslint .", + "lint:fix": "eslint --fix ." + }, + "version": "0.0.1" +} diff --git a/src/plugins/md_all_headings/static/css/editor.css b/src/plugins/md_all_headings/static/css/editor.css new file mode 100644 index 000000000..3817683e4 --- /dev/null +++ b/src/plugins/md_all_headings/static/css/editor.css @@ -0,0 +1,11 @@ +#innerdocbody h1{font-size: 2.5em;} +#innerdocbody h2{font-size: 1.8em;} +#innerdocbody h3{font-size: 1.5em;} +#innerdocbody h4{font-size: 1.2em;} +#innerdocbody code{font-family: RobotoMono;} +#innerdocbody h1, +#innerdocbody h2, +#innerdocbody h3, +#innerdocbody h4 { + display: inline-block; +} diff --git a/src/plugins/md_all_headings/static/js/index.js b/src/plugins/md_all_headings/static/js/index.js new file mode 100644 index 000000000..b482d7a10 --- /dev/null +++ b/src/plugins/md_all_headings/static/js/index.js @@ -0,0 +1,121 @@ +'use strict'; + +const cssFiles = ['ep_all_headings/static/css/editor.css']; + +// All our tags are block elements, so we just return them. +const tags = ['h1', 'h2', 'h3', 'h4', 'code']; +exports.aceRegisterBlockElements = () => tags; + +// Bind the event handler to the toolbar buttons +exports.postAceInit = (hookName, context) => { + const hs = $('#heading-selection'); + hs.on('change', function () { + const value = $(this).val(); + const intValue = parseInt(value, 10); + if (!isNaN(intValue)) { + context.ace.callWithAce((ace) => { + ace.ace_doInsertHeading(intValue); + }, 'insertheading', true); + hs.val('dummy'); + } + }); +}; + +const range = (start, end) => Array.from( + Array(Math.abs(end - start) + 1), + (_, i) => start + i +); + +// On caret position change show the current heading +exports.aceEditEvent = (hookName, call) => { + // If it's not a click or a key event and the text hasn't changed then do nothing + const cs = call.callstack; + if (!(cs.type === 'handleClick') && !(cs.type === 'handleKeyEvent') && !(cs.docTextChanged)) { + return false; + } + // If it's an initial setup event then do nothing.. + if (cs.type === 'setBaseText' || cs.type === 'setup') return false; + + // It looks like we should check to see if this section has this attribute + setTimeout(() => { // avoid race condition.. + const attributeManager = call.documentAttributeManager; + const rep = call.rep; + const activeAttributes = {}; + $('#heading-selection').val('dummy').niceSelect('update'); + + const firstLine = rep.selStart[0]; + const lastLine = Math.max(firstLine, rep.selEnd[0] - ((rep.selEnd[1] === 0) ? 1 : 0)); + let totalNumberOfLines = 0; + + range(firstLine, lastLine).forEach((line) => { + totalNumberOfLines++; + const attr = attributeManager.getAttributeOnLine(line, 'heading'); + if (!activeAttributes[attr]) { + activeAttributes[attr] = {}; + activeAttributes[attr].count = 1; + } else { + activeAttributes[attr].count++; + } + }); + + $.each(activeAttributes, (k, attr) => { + if (attr.count === totalNumberOfLines) { + // show as active class + const ind = tags.indexOf(k); + $('#heading-selection').val(ind).niceSelect('update'); + } + }); + }, 250); +}; + +// Our heading attribute will result in a heaading:h1... :h6 class +exports.aceAttribsToClasses = (hookName, context) => { + if (context.key === 'heading') { + return [`heading:${context.value}`]; + } +}; + +// Here we convert the class heading:h1 into a tag +exports.aceDomLineProcessLineAttributes = (hookName, context) => { + const cls = context.cls; + const headingType = /(?:^| )heading:([A-Za-z0-9]*)/.exec(cls); + if (headingType) { + let tag = headingType[1]; + + // backward compatibility, we used propose h5 and h6, but not anymore + if (tag === 'h5' || tag === 'h6') tag = 'h4'; + + if (tags.indexOf(tag) >= 0) { + const modifier = { + preHtml: `<${tag}>`, + postHtml: ``, + processedMarker: true, + }; + return [modifier]; + } + } + return []; +}; + +// Once ace is initialized, we set ace_doInsertHeading and bind it to the context +exports.aceInitialized = (hookName, context) => { + const editorInfo = context.editorInfo; + // Passing a level >= 0 will set a heading on the selected lines, level < 0 will remove it. + editorInfo.ace_doInsertHeading = (level) => { + const {documentAttributeManager, rep} = context; + if (!(rep.selStart && rep.selEnd)) return; + if (level >= 0 && tags[level] === undefined) return; + const firstLine = rep.selStart[0]; + const lastLine = Math.max(firstLine, rep.selEnd[0] - ((rep.selEnd[1] === 0) ? 1 : 0)); + + range(firstLine, lastLine).forEach((line) => { + if (level >= 0) { + documentAttributeManager.setAttributeOnLine(line, 'heading', tags[level]); + } else { + documentAttributeManager.removeAttributeOnLine(line, 'heading'); + } + }); + }; +}; + +exports.aceEditorCSS = () => cssFiles; diff --git a/src/plugins/md_all_headings/static/js/shared.js b/src/plugins/md_all_headings/static/js/shared.js new file mode 100644 index 000000000..237f7f805 --- /dev/null +++ b/src/plugins/md_all_headings/static/js/shared.js @@ -0,0 +1,29 @@ +'use strict'; + +const tags = ['h1', 'h2', 'h3', 'h4', 'code']; + +exports.collectContentPre = (hookName, context, cb) => { + const tname = context.tname; + const state = context.state; + const lineAttributes = state.lineAttributes; + const tagIndex = tags.indexOf(tname); + if (tname === 'div' || tname === 'p') { + delete lineAttributes.heading; + } + if (tagIndex >= 0) { + lineAttributes.heading = tags[tagIndex]; + } + return cb(); +}; + +// I don't even know when this is run.. +exports.collectContentPost = (hookName, context, cb) => { + const tname = context.tname; + const state = context.state; + const lineAttributes = state.lineAttributes; + const tagIndex = tags.indexOf(tname); + if (tagIndex >= 0) { + delete lineAttributes.heading; + } + return cb(); +}; diff --git a/src/plugins/md_all_headings/templates/editbarButtons.ejs b/src/plugins/md_all_headings/templates/editbarButtons.ejs new file mode 100644 index 000000000..fc0647ee6 --- /dev/null +++ b/src/plugins/md_all_headings/templates/editbarButtons.ejs @@ -0,0 +1,12 @@ +
  • +
  • + +
  • diff --git a/src/plugins/md_author_colors/README.md b/src/plugins/md_author_colors/README.md new file mode 100644 index 000000000..488ed7697 --- /dev/null +++ b/src/plugins/md_author_colors/README.md @@ -0,0 +1 @@ +Visually pleasing looks in Mu Doc \ No newline at end of file diff --git a/src/plugins/md_author_colors/ep.json b/src/plugins/md_author_colors/ep.json new file mode 100644 index 000000000..cad7ba140 --- /dev/null +++ b/src/plugins/md_author_colors/ep.json @@ -0,0 +1,13 @@ +{ + "parts": [ + { + "name": "ep_author_colors", + "client_hooks": { + "postAceInit": "ep_author_colors/static/js/index", + "aceSetAuthorStyle": "ep_author_colors/static/js/index", + "aceEditEvent": "ep_author_colors/static/js/index", + "acePostWriteDomLineHTML": "ep_author_colors/static/js/index" + } + } + ] +} diff --git a/src/plugins/md_author_colors/package.json b/src/plugins/md_author_colors/package.json new file mode 100644 index 000000000..7f6966938 --- /dev/null +++ b/src/plugins/md_author_colors/package.json @@ -0,0 +1,72 @@ +{ + "_from": "ep_author_colors", + "_id": "ep_author_colors@0.0.1", + "_inBundle": false, + "_integrity": "sha512-MX9iInCMKENgvi9dLzhAO/AnShx1a2lmhkBLPz5Az0saA0veUIyEAkzpFrxeag33Uonytb4OOAuIe1sucE24zQ==", + "_location": "/ep_author_colors", + "_phantomChildren": {}, + "_requested": { + "type": "tag", + "registry": true, + "raw": "ep_author_colors", + "name": "ep_author_colors", + "escapedName": "ep_author_colors", + "rawSpec": "", + "saveSpec": null, + "fetchSpec": "latest" + }, + "_requiredBy": [ + "#USER" + ], + "_resolved": "https://registry.npmjs.org/ep_author_colors/-/ep_author_colors-0.0.1.tgz", + "_shasum": "a39958ae24104b269ce76db4ad2c379b737450fd", + "_spec": "ep_author_colors", + "_where": "/Users/dev/Github/Edumatica/MuDocs/src", + "bugs": { + "url": "https://github.com/ether/ep_author_colors/issues" + }, + "bundleDependencies": false, + "deprecated": false, + "description": "Mudoc plugin that uses colored underlines instead of colored backgrounds to indicate authorship.", + "devDependencies": { + "eslint": "^7.32.0", + "eslint-config-etherpad": "^2.0.2", + "eslint-plugin-cypress": "^2.12.1", + "eslint-plugin-eslint-comments": "^3.2.0", + "eslint-plugin-mocha": "^9.0.0", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prefer-arrow": "^1.2.3", + "eslint-plugin-promise": "^5.1.1", + "eslint-plugin-you-dont-need-lodash-underscore": "^6.12.0" + }, + "engines": { + "node": ">=12.13.0" + }, + "eslintConfig": { + "root": true, + "extends": "etherpad/plugin" + }, + "funding": { + "type": "individual", + "url": "https://edumatica.io/" + }, + "homepage": "https://github.com/akhil-naidu/ep_author_colors#readme", + "keywords": [ + "mudoc", + "plugin" + ], + "license": "MIT", + "name": "ep_author_colors", + "peerDependencies": { + "ep_etherpad-lite": ">=0.0.2" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/akhil-naidu/ep_author_colors.git" + }, + "scripts": { + "lint": "eslint .", + "lint:fix": "eslint --fix ." + }, + "version": "0.0.1" +} diff --git a/src/plugins/md_author_colors/static/js/index.js b/src/plugins/md_author_colors/static/js/index.js new file mode 100644 index 000000000..0f81d0d58 --- /dev/null +++ b/src/plugins/md_author_colors/static/js/index.js @@ -0,0 +1,222 @@ +'use strict'; + +let $sidedivinner; +let init; + +exports.postAceInit = (hookName, context) => { + $sidedivinner = $('iframe[name="ace_outer"]').contents().find('#sidedivinner'); + if (!$('#editorcontainerbox').hasClass('flex-layout')) { + return $.gritter.add({ + title: 'Error', + text: 'ep_author_colors: Please upgrade to Mudoc 0.0.2 for this plugin to work correctly', + sticky: true, + class_name: 'error', + }); + } +}; + +const derivePrimaryAuthor = ($node) => { + let mPA, authorClass; + const byAuthor = new Map(); + $node.find('span').each(function () { + const $this = $(this); + for (const spanclass of this.classList) { + if (spanclass.startsWith('author')) { + byAuthor.set(spanclass, (byAuthor.get(spanclass) || 0) + $this.text().length); + } + } + }); + mPA = 0; + authorClass = null; + for (const [author, value] of byAuthor) { + if (value <= mPA) continue; + mPA = value; + authorClass = author; + } + return authorClass; +}; + +const toggleAuthor = ($node, prefix, authorClass) => { + if ($node.length === 0) return true; + let hasClass = false; + const myClass = `${prefix}-${authorClass}`; + for (const c of $node[0].classList) { + if (c.indexOf(prefix) !== 0) continue; + if (c === myClass) { + hasClass = true; + } else { + $node.removeClass(c); + } + } + if (hasClass) { + return false; + } + $node.addClass(myClass); + return true; +}; + +const updateDomline = ($node) => { + const lineNumber = $node.index() + 1; + if (!lineNumber) { + return false; + } + const authorClass = $node.text().length > 0 ? derivePrimaryAuthor($node) : 'none'; + toggleAuthor($node, 'primary', authorClass); + return authorViewUpdate($node, lineNumber, null, authorClass); +}; + +const extractAuthor = ($node) => { + let ref$, ref1$; + return (ref$ = (() => { + const results$ = []; + for (const a of $node[0].classList) { + if (a.startsWith('primary-')) results$.push(a); + } + return results$; + })()) != null ? (ref1$ = ref$[0]) != null ? ref1$.replace(/^primary-/, '') : void 8 : void 8; +}; + +const authorViewUpdate = ($node, lineNumber, prevAuthor, authorClass) => { + let prev, ref$, authorChanged, logicalPrevAuthor; + if (!$sidedivinner) { + $sidedivinner = $('iframe[name="ace_outer"]').contents().find('#sidedivinner'); + } + const $authorContainer = $sidedivinner.find(`div:nth-child(${lineNumber})`); + if (authorClass == null) authorClass = extractAuthor($node); + if (!prevAuthor) { + prev = $authorContainer; + while ((prev = prev.prev()) && prev.length) { + prevAuthor = extractAuthor(prev); + if (prevAuthor !== 'none') { + break; + } + } + } + $authorContainer.toggleClass('concise', prevAuthor === authorClass); + const prevId = (ref$ = $authorContainer.attr('id')) != null ? ref$.replace(/^ref-/, '') : void 8; + if (prevId === $node.attr('id')) { + authorChanged = toggleAuthor($authorContainer, 'primary', authorClass); + if (!authorChanged) { + return; + } + } else { + $authorContainer.attr('id', `ref-${$node.attr('id')}`); + toggleAuthor($authorContainer, 'primary', authorClass); + } + const next = $node.next(); + if (next.length) { + logicalPrevAuthor = authorClass === 'none' ? prevAuthor : authorClass; + return authorViewUpdate(next, lineNumber + 1, logicalPrevAuthor); + } +}; + +const getAuthorClassName = (author) => `author-${author.replace(/[^a-y0-9]/g, (c) => { + if (c === '.') { + return '-'; + } else { + return `z${c.charCodeAt(0)}z`; + } +})}`; + +const outerInit = (outerDynamicCSS) => { + const x$ = outerDynamicCSS.selectorStyle('#sidedivinner.authorColors > div'); + x$.borderRight = 'solid 5px transparent'; + const y$ = outerDynamicCSS.selectorStyle('#sidedivinner.authorColors > div.concise::before'); + y$.content = "' '"; + const z$ = outerDynamicCSS.selectorStyle('#sidedivinner.authorColors > div::before'); + z$.fontSize = '11px'; + z$.textTransform = 'capitalize'; + z$.textOverflow = 'ellipsis'; + z$.overflow = 'hidden'; + return init = true; +}; + +exports.aceSetAuthorStyle = (hookName, context) => { + const {author, dynamicCSS, info, outerDynamicCSS, parentDynamicCSS} = context; + if (!init) { + outerInit(outerDynamicCSS); + } + const authorClass = getAuthorClassName(author); + const authorSelector = `.authorColors span.${authorClass}`; + if (info) { + const color = info.bgcolor; + if (!color) { + return 1; + } + const authorName = authorNameAndColorFromAuthorId(author).name; + const x$ = dynamicCSS.selectorStyle(`#innerdocbody.authorColors span.${authorClass}`); + x$.borderBottom = `2px solid ${color}`; + x$.paddingBottom = '1px'; + const y$ = parentDynamicCSS.selectorStyle(authorSelector); + y$.borderBottom = `2px solid ${color}`; + y$.paddingBottom = '1px'; + const z$ = dynamicCSS.selectorStyle( + `#innerdocbody.authorColors .primary-${authorClass} span.${authorClass}`); + z$.borderBottom = '0px'; + const z1$ = outerDynamicCSS.selectorStyle( + `#sidedivinner.authorColors > div.primary-${authorClass}`); + z1$.borderRight = `solid 5px ${color}`; + const z2$ = outerDynamicCSS.selectorStyle( + `#sidedivinner.authorColors > div.primary-${authorClass}::before`); + z2$.content = `'${authorName}'`; + z2$.paddingLeft = '5px'; + z2$.whiteSpace = 'nowrap'; + const z3$ = outerDynamicCSS.selectorStyle( + `.line-numbers-hidden #sidedivinner.authorColors > div.primary-${authorClass}::before`); + z3$.paddingRight = '12px'; + } else { + dynamicCSS.removeSelectorStyle(`#innerdocbody.authorColors span.${authorClass}`); + parentDynamicCSS.removeSelectorStyle(authorSelector); + } + return 1; +}; + +const authorNameAndColorFromAuthorId = (authorId) => { + let authorObj; + const myAuthorId = pad.myUserInfo.userId; + if (myAuthorId === authorId) { + return { + name: 'Me', + color: pad.myUserInfo.colorId, + }; + } + authorObj = {}; + $('#otheruserstable > tbody > tr').each(function () { + let x$; + if (authorId === $(this).data('authorid')) { + x$ = $(this); + x$.find('.usertdname').each(function () { + return authorObj.name = $(this).text() || 'Unknown Author'; + }); + x$.find('.usertdswatch > div').each(function () { + return authorObj.color = $(this).css('background-color'); + }); + return authorObj; + } + }); + if (!authorObj || !authorObj.name) { + authorObj = clientVars.collab_client_vars.historicalAuthorData[authorId]; + } + return authorObj || { + name: 'Unknown Author', + color: '#fff', + }; +}; + +exports.acePostWriteDomLineHTML = + (hookName, {node}) => setTimeout(() => updateDomline($(node)), 200); + +exports.aceEditEvent = (hookName, {callstack}) => { + if (callstack.type !== 'setWraps') { + return; + } + const x$ = $('iframe[name="ace_outer"]').contents(); + x$.find('#sidediv').css({ + 'padding-right': '0px', + }); + x$.find('#sidedivinner').css({ + 'max-width': '180px', + 'overflow': 'hidden', + }); + return x$; +}; diff --git a/src/plugins/md_checklist/README.md b/src/plugins/md_checklist/README.md new file mode 100644 index 000000000..67d0a9b08 --- /dev/null +++ b/src/plugins/md_checklist/README.md @@ -0,0 +1 @@ +Checklist plugin for MuDoc diff --git a/src/plugins/md_checklist/checklist.js b/src/plugins/md_checklist/checklist.js new file mode 100644 index 000000000..620d399b7 --- /dev/null +++ b/src/plugins/md_checklist/checklist.js @@ -0,0 +1,14 @@ +var eejs = require('ep_etherpad-lite/node/eejs/'); + +exports.eejsBlock_scripts = function (hook_name, args, cb) { + // args.content += eejs.require('ep_checklist/static/js/checklist.js'); + args.content = args.content + ''; + return cb(); +} + +exports.eejsBlock_styles = function (hook_name, args, cb) { + // args.content += eejs.require('ep_checklist/static/css/fontello.css'); + args.content = args.content + ''; + return cb(); +} + diff --git a/src/plugins/md_checklist/ep.json b/src/plugins/md_checklist/ep.json new file mode 100644 index 000000000..88036e8fe --- /dev/null +++ b/src/plugins/md_checklist/ep.json @@ -0,0 +1,21 @@ +{ + "parts":[ + { + "name": "checklist", + "client_hooks": { + "aceEditorCSS": "ep_checklist/static/js/checklist", + "aceDomLineProcessLineAttributes": "ep_checklist/static/js/checklist", + "postAceInit": "ep_checklist/static/js/checklist", + "aceInitialized": "ep_checklist/static/js/checklist", + "collectContentPre": "ep_checklist/static/js/shared", + "aceAttribsToClasses": "ep_checklist/static/js/checklist" + }, + "hooks": { + "eejsBlock_scripts": "ep_checklist/checklist", + "eejsBlock_styles": "ep_checklist/checklist", + "collectContentPre": "ep_checklist/static/js/shared", + "collectContentPost": "ep_checklist/static/js/shared" + } + } + ] +} diff --git a/src/plugins/md_checklist/package.json b/src/plugins/md_checklist/package.json new file mode 100644 index 000000000..39e037905 --- /dev/null +++ b/src/plugins/md_checklist/package.json @@ -0,0 +1,47 @@ +{ + "_from": "ep_checklist", + "_id": "ep_checklist@0.0.5", + "_inBundle": false, + "_integrity": "sha512-HBHNgbYwR9Sy+/RtQ8h/umW1J26aUunHSpMQGwfOq2otypDKWFoU9O7fy2kKEEpG7itX/PfiM8GRHZlRulPV/Q==", + "_location": "/ep_checklist", + "_phantomChildren": {}, + "_requested": { + "type": "tag", + "registry": true, + "raw": "ep_checklist", + "name": "ep_checklist", + "escapedName": "ep_checklist", + "rawSpec": "", + "saveSpec": null, + "fetchSpec": "latest" + }, + "_requiredBy": [ + "#USER" + ], + "_resolved": "https://registry.npmjs.org/ep_checklist/-/ep_checklist-0.0.5.tgz", + "_shasum": "3662e3284b7ca3231906fe69564910e35a1b66b3", + "_spec": "ep_checklist", + "_where": "/Users/dev/Github/Edumatica/MuDocs/src", + "author": { + "name": "Aman", + "email": "sherawataman05@gmail.com" + }, + "bugs": { + "url": "https://github.com/akhil-naidu/ep_checklist/issues" + }, + "bundleDependencies": false, + "contributors": [], + "dependencies": {}, + "deprecated": false, + "description": "Checklist Plugin for Mudoc", + "engines": { + "node": "*" + }, + "homepage": "https://github.com/akhil-naidu/ep_checklist#readme", + "name": "ep_checklist", + "repository": { + "type": "git", + "url": "git+https://github.com/akhil-naidu/ep_checklist.git" + }, + "version": "0.0.5" +} diff --git a/src/plugins/md_checklist/static/css/button.css b/src/plugins/md_checklist/static/css/button.css new file mode 100644 index 000000000..2c6f65662 --- /dev/null +++ b/src/plugins/md_checklist/static/css/button.css @@ -0,0 +1,14 @@ + + +.buttonicon-tasklist{ + height: 25px; + width: 25px; + /* background-image: url('../img/tasklist-icon_1.png'); + background-repeat:no-repeat; + background-size: 18px 18px; + background-position: 0px 4px; */ + + /* margin-top: 10px; */ + /* /static/plugins/ep_tasklist/static/img/tasklist-done.png */ + +} diff --git a/src/plugins/md_checklist/static/css/checklist.css b/src/plugins/md_checklist/static/css/checklist.css new file mode 100644 index 000000000..5a26e15cc --- /dev/null +++ b/src/plugins/md_checklist/static/css/checklist.css @@ -0,0 +1,20 @@ +li.checklist-not-done, li.checklist-done{ + list-style: none; + background-repeat: no-repeat; + padding-left: 1.5em; + cursor: pointer; +} + +.checklist-not-done{ + background-image: url('../img/checklist.png'); + background-position:2px 5px; + + +} + +.checklist-done{ + opacity: .6; + background-image: url('../img/checklist-done.png'); + background-position:2px 5px; + +} diff --git a/src/plugins/md_checklist/static/css/font/fontello.eot b/src/plugins/md_checklist/static/css/font/fontello.eot new file mode 100644 index 000000000..10d2d81e5 Binary files /dev/null and b/src/plugins/md_checklist/static/css/font/fontello.eot differ diff --git a/src/plugins/md_checklist/static/css/font/fontello.svg b/src/plugins/md_checklist/static/css/font/fontello.svg new file mode 100644 index 000000000..d087d4b6e --- /dev/null +++ b/src/plugins/md_checklist/static/css/font/fontello.svg @@ -0,0 +1,12 @@ + + + +Copyright (C) 2022 by original authors @ fontello.com + + + + + + + + diff --git a/src/plugins/md_checklist/static/css/font/fontello.ttf b/src/plugins/md_checklist/static/css/font/fontello.ttf new file mode 100644 index 000000000..21e9ea68a Binary files /dev/null and b/src/plugins/md_checklist/static/css/font/fontello.ttf differ diff --git a/src/plugins/md_checklist/static/css/font/fontello.woff b/src/plugins/md_checklist/static/css/font/fontello.woff new file mode 100644 index 000000000..6fbe4bc8c Binary files /dev/null and b/src/plugins/md_checklist/static/css/font/fontello.woff differ diff --git a/src/plugins/md_checklist/static/css/font/fontello.woff2 b/src/plugins/md_checklist/static/css/font/fontello.woff2 new file mode 100644 index 000000000..4a4586375 Binary files /dev/null and b/src/plugins/md_checklist/static/css/font/fontello.woff2 differ diff --git a/src/plugins/md_checklist/static/css/fontello.css b/src/plugins/md_checklist/static/css/fontello.css new file mode 100644 index 000000000..a6c8995c9 --- /dev/null +++ b/src/plugins/md_checklist/static/css/fontello.css @@ -0,0 +1,57 @@ +@font-face { + font-family: 'fontello'; + src: url('./font/fontello.eot?77898939'); + src: url('./font/fontello.eot?77898939#iefix') format('embedded-opentype'), + url('./font/fontello.woff2?77898939') format('woff2'), + url('./font/fontello.woff?77898939') format('woff'), + url('./font/fontello.ttf?77898939') format('truetype'), + url('./font/fontello.svg?77898939#fontello') format('svg'); + font-weight: normal; + font-style: normal; +} +/* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */ +/* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */ +/* +@media screen and (-webkit-min-device-pixel-ratio:0) { + @font-face { + font-family: 'fontello'; + src: url('../font/fontello.svg?77898939#fontello') format('svg'); + } +} +*/ +[class^="icon-"]:before, [class*=" icon-"]:before { + font-family: "fontello"; + font-style: normal; + font-weight: normal; + speak: never; + + display: inline-block; + text-decoration: inherit; + width: 1em; + margin-right: .2em; + text-align: center; + /* opacity: .8; */ + + /* For safety - reset parent styles, that can break glyph codes*/ + font-variant: normal; + text-transform: none; + + /* fix buttons height, for twitter bootstrap */ + line-height: 1em; + + /* Animation center compensation - margins should be symmetric */ + /* remove if not needed */ + margin-left: .2em; + + /* you can be more comfortable with increased icons size */ + font-size: 125%; + + /* Font smoothing. That was taken from TWBS */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + + /* Uncomment for 3D effect */ + /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ +} + +.icon-check:before { content: '\e801'; } /* '' */ diff --git a/src/plugins/md_checklist/static/img/checklist-done.png b/src/plugins/md_checklist/static/img/checklist-done.png new file mode 100644 index 000000000..98b7b0bdd Binary files /dev/null and b/src/plugins/md_checklist/static/img/checklist-done.png differ diff --git a/src/plugins/md_checklist/static/img/checklist-icon.png b/src/plugins/md_checklist/static/img/checklist-icon.png new file mode 100644 index 000000000..39cbcbc14 Binary files /dev/null and b/src/plugins/md_checklist/static/img/checklist-icon.png differ diff --git a/src/plugins/md_checklist/static/img/checklist-icon_1.png b/src/plugins/md_checklist/static/img/checklist-icon_1.png new file mode 100644 index 000000000..4c9d4f4e4 Binary files /dev/null and b/src/plugins/md_checklist/static/img/checklist-icon_1.png differ diff --git a/src/plugins/md_checklist/static/img/checklist-icon_2.png b/src/plugins/md_checklist/static/img/checklist-icon_2.png new file mode 100644 index 000000000..8e8d42b91 Binary files /dev/null and b/src/plugins/md_checklist/static/img/checklist-icon_2.png differ diff --git a/src/plugins/md_checklist/static/img/checklist-icon_3.png b/src/plugins/md_checklist/static/img/checklist-icon_3.png new file mode 100644 index 000000000..f4c08eca6 Binary files /dev/null and b/src/plugins/md_checklist/static/img/checklist-icon_3.png differ diff --git a/src/plugins/md_checklist/static/img/checklist-icon_4.png b/src/plugins/md_checklist/static/img/checklist-icon_4.png new file mode 100644 index 000000000..c412e73c6 Binary files /dev/null and b/src/plugins/md_checklist/static/img/checklist-icon_4.png differ diff --git a/src/plugins/md_checklist/static/img/checklist.png b/src/plugins/md_checklist/static/img/checklist.png new file mode 100644 index 000000000..8fd7316c9 Binary files /dev/null and b/src/plugins/md_checklist/static/img/checklist.png differ diff --git a/src/plugins/md_checklist/static/js/ace_inner.js b/src/plugins/md_checklist/static/js/ace_inner.js new file mode 100644 index 000000000..495ce65c8 --- /dev/null +++ b/src/plugins/md_checklist/static/js/ace_inner.js @@ -0,0 +1,10 @@ +// This is a hack to get around ACEs brain-dead limit on onClick on +// links inside the ACE domlines... +// Borrowed from: https://github.com/redhog/ep_sketchspace/blob/master/static/js/ace_inner.js + +$(document).ready(function () { + $("body").mousedown(function (event) { + parent.parent.exports.checklist.doUpdatechecklist(1); + }); +}); + diff --git a/src/plugins/md_checklist/static/js/checklist.js b/src/plugins/md_checklist/static/js/checklist.js new file mode 100644 index 000000000..9bed5a0ab --- /dev/null +++ b/src/plugins/md_checklist/static/js/checklist.js @@ -0,0 +1,169 @@ +if (typeof exports == 'undefined') { + var exports = (this['mymodule'] = {}); +} + +var underscore = require('ep_etherpad-lite/static/js/underscore'); +var padeditor = require('ep_etherpad-lite/static/js/pad_editor').padeditor; +var tags = ['checklist-not-done', 'checklist-done']; +var padEditor; + +exports.checklist = { + init: function (context) { + // Write the button to the dom + var buttonHTML = + '
  • '; + // $(buttonHTML).insertBefore($('.buttonicon-indent').parent().parent()); + $(buttonHTML).insertAfter($('.icon-shuffle').parent().parent()); + $('#checklist').click(function () { + // apply attribtes when we click the editbar button + + context.ace.callWithAce( + function (ace) { + // call the function to apply the attribute inside ACE + ace.ace_doInsertchecklist(); + }, + 'checklist', + true, + ); // TODO what's the second attribute do here? + padeditor.ace.focus(); + }); + context.ace.callWithAce( + function (ace) { + var doc = ace.ace_getDocument(); + $(doc) + .find('#innerdocbody') + .on('click', underscore(exports.checklist.doUpdatechecklist).bind(ace)); + }, + 'checklist', + true, + ); + }, + + doInsertchecklist: function () { + lineHasMarker = function (line) { + if ( + line.domInfo.node.className === 'ace-line primary-null' || + line.domInfo.node.className === 'ace-line primary-none' + ) { + $.gritter.add({ + text: 'There is no content present to add checklist!', + }); + return; + } else { + underscore(underscore.range(firstLine, lastLine + 1)).each(function (i) { + // For each line, either turn on or off task list + var ischecklist = documentAttributeManager.getAttributeOnLine(i, 'checklist-not-done'); + if (!ischecklist) { + // if its already a checklist item + documentAttributeManager.setAttributeOnLine( + i, + 'checklist-not-done', + 'checklist-not-done', + ); // make the line a task list + return; + } else { + documentAttributeManager.removeAttributeOnLine(i, 'checklist-not-done'); // remove the task list from the line + return; + } + }); + return; + } + return line.lineMarker === 1; + }; + + var rep = this.rep; + + var documentAttributeManager = this.documentAttributeManager; + if (!(rep.selStart && rep.selEnd)) { + return; + } // only continue if we have some caret position + var firstLine = rep.selStart[0]; // Get the first line + var lastLine = Math.max(firstLine, rep.selEnd[0] - (rep.selEnd[1] === 0 ? 1 : 0)); // Get the last line + console.log(lastLine); + const line = rep.lines.atIndex(lastLine); + lineHasMarker(line); + }, + + doTogglechecklistItem: function (lineNumber) { + var rep = this.rep; + var documentAttributeManager = this.documentAttributeManager; + var isDone = documentAttributeManager.getAttributeOnLine(lineNumber, 'checklist-done'); + if (isDone) { + documentAttributeManager.removeAttributeOnLine(lineNumber, 'checklist-done'); // remove the task list from the line + documentAttributeManager.setAttributeOnLine( + lineNumber, + 'checklist-not-done', + 'checklist-not-done', + ); // make it undone + } else { + documentAttributeManager.removeAttributeOnLine(lineNumber, 'checklist-not-done'); // remove the task list from the line + documentAttributeManager.setAttributeOnLine(lineNumber, 'checklist-done', 'checklist-done'); // make it done + } + }, + + doUpdatechecklist: function (event) { + // This is in the wrong context to access doc attr manager + var ace = this; + var target = event.target; + var ischecklist = + $(target).hasClass('checklist-not-done') || $(target).hasClass('checklist-done'); + var parent = $(target).parent(); + var lineNumber = parent.prevAll().length; + var targetRight = event.target.offsetLeft + 14; // The right hand side of the checklist -- remember the checklist can be indented + var isChecklist = event.pageX < targetRight; // was the click to the left of the checklist + if (!ischecklist || !isChecklist) { + return; + } // Dont continue if we're not clicking a checklist of a checklist + padEditor.callWithAce( + function (ace) { + // call the function to apply the attribute inside ACE + ace.ace_doTogglechecklistItem(lineNumber); + }, + 'checklist', + true, + ); // TODO what's the second attribute do here? + }, +}; + +function aceInitialized(hook, context) { + var editorInfo = context.editorInfo; + editorInfo.ace_doInsertchecklist = underscore(exports.checklist.doInsertchecklist).bind(context); // What does underscore do here? + editorInfo.ace_doTogglechecklistItem = underscore(exports.checklist.doTogglechecklistItem).bind( + context, + ); // TODO + padEditor = context.editorInfo.editor; +} + +var aceDomLineProcessLineAttributes = function (name, context) { + if (context.cls.indexOf('checklist-not-done') !== -1) { + var type = 'checklist-not-done'; + } + if (context.cls.indexOf('checklist-done') !== -1) { + var type = 'checklist-done'; + } + var tagIndex = context.cls.indexOf(type); + if (tagIndex !== undefined && type) { + var tag = tags[tagIndex]; + var modifier = { + preHtml: '
  • ', + postHtml: '
  • ', + processedMarker: true, + }; + return [modifier]; // return the modifier + } + return []; // or return nothing +}; + +exports.aceAttribsToClasses = function (hook, context) { + if (context.key == 'checklist-not-done' || context.key == 'checklist-done') { + return [context.value]; + } +}; +exports.aceInitialized = aceInitialized; +exports.aceDomLineProcessLineAttributes = aceDomLineProcessLineAttributes; +exports.aceEditorCSS = function (hook_name, cb) { + return ['/ep_checklist/static/css/checklist.css']; +}; // inner pad CSS +exports.postAceInit = function (hook, context) { + exports.checklist.init(context); +}; diff --git a/src/plugins/md_checklist/static/js/shared.js b/src/plugins/md_checklist/static/js/shared.js new file mode 100644 index 000000000..f9c31f321 --- /dev/null +++ b/src/plugins/md_checklist/static/js/shared.js @@ -0,0 +1,43 @@ +var collectContentPre = function(hook, context){ + var cls = context.cls; + var tname = context.tname; + var state = context.state; + var lineAttributes = state.lineAttributes + + if(cls !== null) { + var tagIndex = cls.indexOf("checklist-not-done"); + if(tagIndex === 0){ + lineAttributes['checklist-not-done'] = tags[tagIndex]; + } + + var tagIndex = cls.indexOf("checklist-done"); + if(tagIndex !== -1){ + lineAttributes['checklist-done'] = 'checklist-done'; + } + + if(tname === "div" || tname === "p"){ + delete lineAttributes['checklist-done']; + delete lineAttributes['checklist-not-done']; + } + } +}; + +var collectContentPost = function(hook, context){ + var cls = context.cls; + var tname = context.tname; + var state = context.state; + var lineAttributes = state.lineAttributes + + var tagIndex = cls.indexOf("checklist-not-done"); + if(tagIndex >= 0){ + delete lineAttributes['checklist-not-done']; + } + + var tagIndex = cls.indexOf("checklist-done"); + if(tagIndex >= 0){ + delete lineAttributes['checklist-done']; + } +}; + +exports.collectContentPre = collectContentPre; +exports.collectContentPost = collectContentPost; diff --git a/src/plugins/md_comments/README.md b/src/plugins/md_comments/README.md new file mode 100644 index 000000000..46018fee7 --- /dev/null +++ b/src/plugins/md_comments/README.md @@ -0,0 +1 @@ +Comments in Mu Doc \ No newline at end of file diff --git a/src/plugins/md_comments/apiUtils.js b/src/plugins/md_comments/apiUtils.js new file mode 100644 index 000000000..9ba92c5a1 --- /dev/null +++ b/src/plugins/md_comments/apiUtils.js @@ -0,0 +1,76 @@ +'use strict'; + +const absolutePaths = require('ep_etherpad-lite/node/utils/AbsolutePaths'); +const fs = require('fs'); +const padManager = require('ep_etherpad-lite/node/db/PadManager'); +const settings = require('ep_etherpad-lite/node/utils/Settings'); + +// ensure we have an apiKey +let apiKey = ''; +try { + apiKey = fs.readFileSync(absolutePaths.makeAbsolute('./APIKEY.txt'), 'utf8').trim(); +} catch (e) { + console.warn('Could not find APIKEY'); +} + +// Checks if api key is correct and prepare response if it is not. +// Returns true if valid, false otherwise. +const validateApiKey = (fields, res) => { + let valid = true; + + const apiKeyReceived = fields.apikey || fields.api_key; + if (apiKeyReceived !== apiKey) { + res.statusCode = 401; + res.json({code: 4, message: 'no or wrong API Key', data: null}); + valid = false; + } + + return valid; +}; + +const validateRequiredField = + (originalFields, fieldName) => typeof originalFields[fieldName] !== 'undefined'; + +// Checks if required fields are present, and prepare response if any of them +// is not. Returns true if valid, false otherwise. +const validateRequiredFields = (originalFields, requiredFields, res) => { + for (const requiredField of requiredFields) { + if (!validateRequiredField(originalFields, requiredField)) { + const errorMessage = `${requiredField} is required`; + res.json({code: 1, message: errorMessage, data: null}); + return false; + } + } + return true; +}; + +// Sanitizes pad id and returns it: +const sanitizePadId = (req) => { + let padIdReceived = req.params.pad; + padManager.sanitizePadId(padIdReceived, (padId) => { + padIdReceived = padId; + }); + + return padIdReceived; +}; + +// Builds url for message broadcasting, based on settings.json and on the +// given endPoint: +const broadcastUrlFor = (endPoint) => { + let url = ''; + if (settings.ssl) { + url += 'https://'; + } else { + url += 'http://'; + } + url += `${settings.ip}:${settings.port}${endPoint}`; + + return url; +}; + +/* ********** Available functions/values: ********** */ + +exports.validateApiKey = validateApiKey; +exports.validateRequiredFields = validateRequiredFields; +exports.sanitizePadId = sanitizePadId; +exports.broadcastUrlFor = broadcastUrlFor; diff --git a/src/plugins/md_comments/commentManager.js b/src/plugins/md_comments/commentManager.js new file mode 100644 index 000000000..6557eb1c5 --- /dev/null +++ b/src/plugins/md_comments/commentManager.js @@ -0,0 +1,209 @@ +'use strict'; + +const _ = require('underscore'); +const db = require('ep_etherpad-lite/node/db/DB'); +const log4js = require('ep_etherpad-lite/node_modules/log4js'); +const randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; +const shared = require('./static/js/shared'); + +const logger = log4js.getLogger('ep_comments'); + +exports.getComments = async (padId) => { + // Not sure if we will encouter race conditions here.. Be careful. + + // get the globalComments + let comments = await db.get(`comments:${padId}`); + if (comments == null) comments = {}; + return {comments}; +}; + +exports.deleteComment = async (padId, commentId, authorId) => { + const comments = await db.get(`comments:${padId}`); + if (comments == null || comments[commentId] == null) { + logger.debug(`ignoring attempt to delete non-existent comment ${commentId}`); + throw new Error('no_such_comment'); + } + if (comments[commentId].author !== authorId) { + logger.debug(`author ${authorId} attempted to delete comment ${commentId} ` + + `belonging to author ${comments[commentId].author}`); + throw new Error('unauth'); + } + delete comments[commentId]; + await db.set(`comments:${padId}`, comments); +}; + +exports.deleteComments = async (padId) => { + await db.remove(`comments:${padId}`); +}; + +exports.addComment = async (padId, data) => { + const [commentIds, comments] = await exports.bulkAddComments(padId, [data]); + return [commentIds[0], comments[0]]; +}; + +exports.bulkAddComments = async (padId, data) => { + // get the entry + let comments = await db.get(`comments:${padId}`); + + // the entry doesn't exist so far, let's create it + if (comments == null) comments = {}; + + const newComments = []; + const commentIds = data.map((commentData) => { + // if the comment was copied it already has a commentID, so we don't need create one + const commentId = commentData.commentId || shared.generateCommentId(); + + const comment = { + author: commentData.author || 'empty', + name: commentData.name, + text: commentData.text, + changeTo: commentData.changeTo, + changeFrom: commentData.changeFrom, + timestamp: parseInt(commentData.timestamp) || new Date().getTime(), + }; + // add the entry for this pad + comments[commentId] = comment; + + newComments.push(comment); + return commentId; + }); + + // save the new element back + await db.set(`comments:${padId}`, comments); + + return [commentIds, newComments]; +}; + +exports.copyComments = async (originalPadId, newPadID) => { + // get the comments of original pad + const originalComments = await db.get(`comments:${originalPadId}`); + // make sure we have different copies of the comment between pads + const copiedComments = _.mapObject(originalComments, (thisComment) => _.clone(thisComment)); + + // save the comments on new pad + await db.set(`comments:${newPadID}`, copiedComments); +}; + +exports.getCommentReplies = async (padId) => { + // get the globalComments replies + let replies = await db.get(`comment-replies:${padId}`); + // comment does not exist + if (replies == null) replies = {}; + return {replies}; +}; + +exports.deleteCommentReplies = async (padId) => { + await db.remove(`comment-replies:${padId}`); +}; + +exports.addCommentReply = async (padId, data) => { + const [replyIds, replies] = await exports.bulkAddCommentReplies(padId, [data]); + return [replyIds[0], replies[0]]; +}; + +exports.bulkAddCommentReplies = async (padId, data) => { + // get the entry + let replies = await db.get(`comment-replies:${padId}`); + // the entry doesn't exist so far, let's create it + if (replies == null) replies = {}; + + const newReplies = []; + const replyIds = data.map((replyData) => { + // create the new reply id + const replyId = `c-reply-${randomString(16)}`; + + const metadata = replyData.comment || {}; + + const reply = { + commentId: replyData.commentId, + text: replyData.reply || replyData.text, + changeTo: replyData.changeTo || null, + changeFrom: replyData.changeFrom || null, + author: metadata.author || 'empty', + name: metadata.name || replyData.name, + timestamp: parseInt(replyData.timestamp) || new Date().getTime(), + }; + + // add the entry for this pad + replies[replyId] = reply; + + newReplies.push(reply); + return replyId; + }); + + // save the new element back + await db.set(`comment-replies:${padId}`, replies); + + return [replyIds, newReplies]; +}; + +exports.copyCommentReplies = async (originalPadId, newPadID) => { + // get the replies of original pad + const originalReplies = await db.get(`comment-replies:${originalPadId}`); + // make sure we have different copies of the reply between pads + const copiedReplies = _.mapObject(originalReplies, (thisReply) => _.clone(thisReply)); + + // save the comment replies on new pad + await db.set(`comment-replies:${newPadID}`, copiedReplies); +}; + +exports.changeAcceptedState = async (padId, commentId, state) => { + // Given a comment we update that comment to say the change was accepted or reverted + + // If we're dealing with comment replies we need to a different query + let prefix = 'comments:'; + if (commentId.substring(0, 7) === 'c-reply') { + prefix = 'comment-replies:'; + } + + // get the entry + const comments = await db.get(prefix + padId); + + // add the entry for this pad + const comment = comments[commentId]; + + if (state) { + comment.changeAccepted = true; + comment.changeReverted = false; + } else { + comment.changeAccepted = false; + comment.changeReverted = true; + } + + comments[commentId] = comment; + + // save the new element back + await db.set(prefix + padId, comments); +}; + +exports.changeCommentText = async (padId, commentId, commentText, authorId) => { + if (commentText.length <= 0) { + logger.debug(`ignoring attempt to change comment ${commentId} to the empty string`); + throw new Error('comment_cannot_be_empty'); + } + + // Given a comment we update the comment text + + // If we're dealing with comment replies we need to a different query + let prefix = 'comments:'; + if (commentId.substring(0, 7) === 'c-reply') { + prefix = 'comment-replies:'; + } + + // get the entry + const comments = await db.get(prefix + padId); + if (comments == null || comments[commentId] == null) { + logger.debug(`ignoring attempt to edit non-existent comment ${commentId}`); + throw new Error('no_such_comment'); + } + if (comments[commentId].author !== authorId) { + logger.debug(`author ${authorId} attempted to edit comment ${commentId} ` + + `belonging to author ${comments[commentId].author}`); + throw new Error('unauth'); + } + // update the comment text + comments[commentId].text = commentText; + + // save the comment updated back + await db.set(prefix + padId, comments); +}; diff --git a/src/plugins/md_comments/ep.json b/src/plugins/md_comments/ep.json new file mode 100644 index 000000000..199c875c4 --- /dev/null +++ b/src/plugins/md_comments/ep.json @@ -0,0 +1,36 @@ +{ + "parts": [ + { + "name":"main", + "pre": ["ep_etherpad-lite/webaccess", "ep_page_view/page_view"], + "post": ["ep_etherpad-lite/static"], + "client_hooks": { + "postToolbarInit": "ep_comments/static/js/index", + "postAceInit": "ep_comments/static/js/index", + "collectContentPre": "ep_comments/static/js/shared", + "aceAttribsToClasses": "ep_comments/static/js/index", + "aceEditorCSS": "ep_comments/static/js/index", + "aceEditEvent": "ep_comments/static/js/index", + "aceInitialized": "ep_comments/static/js/index" + }, + "hooks": { + "padInitToolbar": "ep_comments/index", + "padRemove": "ep_comments/index", + "padCopy": "ep_comments/index", + "socketio": "ep_comments/index", + "expressCreateServer": "ep_comments/index", + "collectContentPre": "ep_comments/static/js/shared", + "eejsBlock_editbarMenuLeft": "ep_comments/index", + "eejsBlock_scripts": "ep_comments/index", + "eejsBlock_mySettings": "ep_comments/index", + "eejsBlock_styles": "ep_comments/index", + "clientVars": "ep_comments/index", + "exportHtmlAdditionalTagsWithData": "ep_comments/exportHTML", + "getLineHTMLForExport": "ep_comments/exportHTML", + "exportMuDocAdditionalContent": "ep_comments/index", + "exportHTMLAdditionalContent": "ep_comments/exportHTML", + "handleMessageSecurity": "ep_comments/index" + } + } + ] +} diff --git a/src/plugins/md_comments/exportHTML.js b/src/plugins/md_comments/exportHTML.js new file mode 100644 index 000000000..854420416 --- /dev/null +++ b/src/plugins/md_comments/exportHTML.js @@ -0,0 +1,60 @@ +'use strict'; + +const $ = require('cheerio'); +const commentManager = require('./commentManager'); +const settings = require('ep_etherpad-lite/node/utils/Settings'); + +// Iterate over pad attributes to find only the comment ones +const findAllCommentUsedOn = (pad) => { + const commentsUsed = []; + pad.pool.eachAttrib((key, value) => { if (key === 'comment') commentsUsed.push(value); }); + return commentsUsed; +}; + +// Add the props to be supported in export +exports.exportHtmlAdditionalTagsWithData = + async (hookName, pad) => findAllCommentUsedOn(pad).map((name) => ['comment', name]); + +exports.getLineHTMLForExport = async (hookName, context) => { + if (settings.ep_comments && settings.ep_comments.exportHtml === false) return; + + // I'm not sure how optimal this is - it will do a database lookup for each line.. + const {comments} = await commentManager.getComments(context.padId); + let hasPlugin = false; + // Load the HTML into a throwaway div instead of calling $.load() to avoid + // https://github.com/cheeriojs/cheerio/issues/1031 + const content = $('
    ').html(context.lineContent); + // include links for each comment which we will add content later. + content.find('span').each(function () { + const span = $(this); + const commentId = span.data('comment'); + if (!commentId) return; // not a comment. please optimize me in selector + if (!comments[commentId]) return; // if this comment has been deleted.. + hasPlugin = true; + span.append( + $('').append( + $('').attr('href', `#${commentId}`).text('*'))); + // Replace data-comment="foo" with class="comment foo". + if (/^c-[0-9a-zA-Z]+$/.test(commentId)) { + span.removeAttr('data-comment').addClass('comment').addClass(commentId); + } + }); + if (hasPlugin) context.lineContent = content.html(); +}; + +exports.exportHTMLAdditionalContent = async (hookName, {padId}) => { + if (settings.ep_comments && settings.ep_comments.exportHtml === false) return; + const {comments} = await commentManager.getComments(padId); + if (!comments) return; + const div = $('
    ').attr('id', 'comments'); + for (const [commentId, comment] of Object.entries(comments)) { + div.append( + $('

    ') + .attr('role', 'comment') + .addClass('comment') + .attr('id', commentId) + .text(`* ${comment.text}`)); + } + // adds additional HTML to the body, we get this HTML from the database of comments:padId + return $.html(div); +}; diff --git a/src/plugins/md_comments/index.js b/src/plugins/md_comments/index.js new file mode 100644 index 000000000..e67d46682 --- /dev/null +++ b/src/plugins/md_comments/index.js @@ -0,0 +1,329 @@ +'use strict'; + +const AttributePool = require('ep_etherpad-lite/static/js/AttributePool'); +const Changeset = require('ep_etherpad-lite/static/js/Changeset'); +const eejs = require('ep_etherpad-lite/node/eejs/'); +const settings = require('ep_etherpad-lite/node/utils/Settings'); +const formidable = require('formidable'); +const commentManager = require('./commentManager'); +const apiUtils = require('./apiUtils'); +const _ = require('underscore'); +const padMessageHandler = require('ep_etherpad-lite/node/handler/PadMessageHandler'); +const readOnlyManager = require('ep_etherpad-lite/node/db/ReadOnlyManager.js'); + +let io; + +exports.exportMuDocAdditionalContent = (hookName, context, callback) => callback(['comments']); + +exports.padRemove = async (hookName, context) => { + await Promise.all([ + commentManager.deleteCommentReplies(context.padID), + commentManager.deleteComments(context.padID), + ]); +}; + +exports.padCopy = async (hookName, context) => { + await Promise.all([ + commentManager.copyComments(context.originalPad.id, context.destinationID), + commentManager.copyCommentReplies(context.originalPad.id, context.destinationID), + ]); +}; + +exports.handleMessageSecurity = async (hookName, {message, socket}) => { + const {type: mtype, data: {type: dtype, apool, changeset} = {}} = message; + if (mtype !== 'COLLABROOM') return; + if (dtype !== 'USER_CHANGES') return; + // Nothing needs to be done if the user already has write access. + if (!padMessageHandler.sessioninfos[socket.id].readonly) return; + const pool = new AttributePool().fromJsonable(apool); + const cs = Changeset.unpack(changeset); + const opIter = Changeset.opIterator(cs.ops); + while (opIter.hasNext()) { + const op = opIter.next(); + // Only operations that manipulate the 'comment' attribute on existing text are allowed. + if (op.opcode !== '=') return; + const forbiddenAttrib = new Error(); + try { + Changeset.eachAttribNumber(op.attribs, (n) => { + // Use an exception to break out of the iteration early. + if (pool.getAttribKey(n) !== 'comment') throw forbiddenAttrib; + }); + } catch (err) { + if (err !== forbiddenAttrib) throw err; + return; + } + } + return true; +}; + +exports.socketio = (hookName, args, cb) => { + io = args.io.of('/comment'); + io.on('connection', (socket) => { + const handler = (fn) => (...args) => { + const respond = args.pop(); + (async () => await fn(...args))().then( + (val) => respond(null, val), + (err) => respond({name: err.name, message: err.message})); + }; + + // Join the rooms + socket.on('getComments', handler(async (data) => { + const {padId} = await readOnlyManager.getIds(data.padId); + // Put read-only and read-write users in the same socket.io "room" so that they can see each + // other's updates. + socket.join(padId); + return await commentManager.getComments(padId); + })); + + socket.on('getCommentReplies', handler(async (data) => { + const {padId} = await readOnlyManager.getIds(data.padId); + return await commentManager.getCommentReplies(padId); + })); + + // On add events + socket.on('addComment', handler(async (data) => { + const {padId} = await readOnlyManager.getIds(data.padId); + const content = data.comment; + const [commentId, comment] = await commentManager.addComment(padId, content); + if (commentId != null && comment != null) { + socket.broadcast.to(padId).emit('pushAddComment', commentId, comment); + return [commentId, comment]; + } + })); + + socket.on('deleteComment', handler(async (data) => { + const {padId} = await readOnlyManager.getIds(data.padId); + await commentManager.deleteComment(padId, data.commentId, data.authorId); + socket.broadcast.to(padId).emit('commentDeleted', data.commentId); + })); + + socket.on('revertChange', handler(async (data) => { + const {padId} = await readOnlyManager.getIds(data.padId); + // Broadcast to all other users that this change was accepted. + // Note that commentId here can either be the commentId or replyId.. + await commentManager.changeAcceptedState(padId, data.commentId, false); + socket.broadcast.to(padId).emit('changeReverted', data.commentId); + })); + + socket.on('acceptChange', handler(async (data) => { + const {padId} = await readOnlyManager.getIds(data.padId); + // Broadcast to all other users that this change was accepted. + // Note that commentId here can either be the commentId or replyId.. + await commentManager.changeAcceptedState(padId, data.commentId, true); + socket.broadcast.to(padId).emit('changeAccepted', data.commentId); + })); + + socket.on('bulkAddComment', handler(async (padId, data) => { + padId = (await readOnlyManager.getIds(padId)).padId; + const [commentIds, comments] = await commentManager.bulkAddComments(padId, data); + socket.broadcast.to(padId).emit('pushAddCommentInBulk'); + return _.object(commentIds, comments); // {c-123:data, c-124:data} + })); + + socket.on('bulkAddCommentReplies', handler(async (padId, data) => { + padId = (await readOnlyManager.getIds(padId)).padId; + const [repliesId, replies] = await commentManager.bulkAddCommentReplies(padId, data); + socket.broadcast.to(padId).emit('pushAddCommentReply', repliesId, replies); + return _.zip(repliesId, replies); + })); + + socket.on('updateCommentText', handler(async (data) => { + const {commentId, commentText, authorId} = data; + const {padId} = await readOnlyManager.getIds(data.padId); + await commentManager.changeCommentText(padId, commentId, commentText, authorId); + socket.broadcast.to(padId).emit('textCommentUpdated', commentId, commentText); + })); + + socket.on('addCommentReply', handler(async (data) => { + const {padId} = await readOnlyManager.getIds(data.padId); + const [replyId, reply] = await commentManager.addCommentReply(padId, data); + reply.replyId = replyId; + socket.broadcast.to(padId).emit('pushAddCommentReply', replyId, reply); + return [replyId, reply]; + })); + }); + return cb(); +}; + +exports.eejsBlock_dd_insert = (hookName, args, cb) => { + args.content += eejs.require('ep_comments/templates/menuButtons.ejs'); + return cb(); +}; + +exports.eejsBlock_mySettings = (hookName, args, cb) => { + args.content += eejs.require('ep_comments/templates/settings.ejs'); + return cb(); +}; + +exports.padInitToolbar = (hookName, args, cb) => { + const toolbar = args.toolbar; + + const button = toolbar.button({ + command: 'addComment', + localizationId: 'ep_comments.add_comment.title', + class: 'buttonicon buttonicon-comment-medical', + }); + + toolbar.registerButton('addComment', button); + + return cb(); +}; + +exports.eejsBlock_editbarMenuLeft = (hookName, args, cb) => { + // check if custom button is used + if (JSON.stringify(settings.toolbar).indexOf('addComment') > -1) { + return cb(); + } + args.content += eejs.require('ep_comments/templates/commentBarButtons.ejs'); + return cb(); +}; + +exports.eejsBlock_scripts = (hookName, args, cb) => { + args.content += eejs.require('ep_comments/templates/comments.html'); + args.content += eejs.require('ep_comments/templates/commentIcons.html'); + return cb(); +}; + +exports.eejsBlock_styles = (hookName, args, cb) => { + args.content += eejs.require('ep_comments/templates/styles.html'); + return cb(); +}; + +exports.clientVars = (hook, context, cb) => { + const displayCommentAsIcon = + settings.ep_comments ? settings.ep_comments.displayCommentAsIcon : false; + const highlightSelectedText = + settings.ep_comments ? settings.ep_comments.highlightSelectedText : false; + return cb({ + displayCommentAsIcon, + highlightSelectedText, + }); +}; + +exports.expressCreateServer = (hookName, args, callback) => { + args.app.get('/p/:pad/:rev?/comments', async (req, res) => { + const fields = req.query; + // check the api key + if (!apiUtils.validateApiKey(fields, res)) return; + + // sanitize pad id before continuing + const padIdReceived = (await readOnlyManager.getIds(apiUtils.sanitizePadId(req))).padId; + + let data; + try { + data = await commentManager.getComments(padIdReceived); + } catch (err) { + console.error(err.stack ? err.stack : err.toString()); + res.json({code: 2, message: 'internal error', data: null}); + return; + } + if (data == null) return; + res.json({code: 0, data}); + }); + + args.app.post('/p/:pad/:rev?/comments', async (req, res) => { + const fields = await new Promise((resolve, reject) => { + (new formidable.IncomingForm()).parse(req, (err, fields) => { + if (err != null) return reject(err); + resolve(fields); + }); + }); + + // check the api key + if (!apiUtils.validateApiKey(fields, res)) return; + + // check required fields from comment data + if (!apiUtils.validateRequiredFields(fields, ['data'], res)) return; + + // sanitize pad id before continuing + const padIdReceived = (await readOnlyManager.getIds(apiUtils.sanitizePadId(req))).padId; + + // create data to hold comment information: + let data; + try { + data = JSON.parse(fields.data); + } catch (err) { + res.json({code: 1, message: 'data must be a JSON', data: null}); + return; + } + + let commentIds, comments; + try { + [commentIds, comments] = await commentManager.bulkAddComments(padIdReceived, data); + } catch (err) { + console.error(err.stack ? err.stack : err.toString()); + res.json({code: 2, message: 'internal error', data: null}); + return; + } + if (commentIds == null) return; + for (let i = 0; i < commentIds.length; i++) { + io.to(padIdReceived).emit('pushAddComment', commentIds[i], comments[i]); + } + res.json({code: 0, commentIds}); + }); + + args.app.get('/p/:pad/:rev?/commentReplies', async (req, res) => { + // it's the same thing as the formidable's fields + const fields = req.query; + // check the api key + if (!apiUtils.validateApiKey(fields, res)) return; + + // sanitize pad id before continuing + const padIdReceived = (await readOnlyManager.getIds(apiUtils.sanitizePadId(req))).padId; + + // call the route with the pad id sanitized + let data; + try { + data = await commentManager.getCommentReplies(padIdReceived); + } catch (err) { + console.error(err.stack ? err.stack : err.toString()); + res.json({code: 2, message: 'internal error', data: null}); + return; + } + if (data == null) return; + res.json({code: 0, data}); + }); + + args.app.post('/p/:pad/:rev?/commentReplies', async (req, res) => { + const fields = await new Promise((resolve, reject) => { + (new formidable.IncomingForm()).parse(req, (err, fields) => { + if (err != null) return reject(err); + resolve(fields); + }); + }); + + // check the api key + if (!apiUtils.validateApiKey(fields, res)) return; + + // check required fields from comment data + if (!apiUtils.validateRequiredFields(fields, ['data'], res)) return; + + // sanitize pad id before continuing + const padIdReceived = (await readOnlyManager.getIds(apiUtils.sanitizePadId(req))).padId; + + // create data to hold comment reply information: + let data; + try { + data = JSON.parse(fields.data); + } catch (err) { + res.json({code: 1, message: 'data must be a JSON', data: null}); + return; + } + + let replyIds, replies; + try { + [replyIds, replies] = await commentManager.bulkAddCommentReplies(padIdReceived, data); + } catch (err) { + console.error(err.stack ? err.stack : err.toString()); + res.json({code: 2, message: 'internal error', data: null}); + return; + } + if (replyIds == null) return; + for (let i = 0; i < replyIds.length; i++) { + replies[i].replyId = replyIds[i]; + io.to(padIdReceived).emit('pushAddCommentReply', replyIds[i], replies[i]); + } + res.json({code: 0, replyIds}); + }); + return callback(); +}; diff --git a/src/plugins/md_comments/locales/bn.json b/src/plugins/md_comments/locales/bn.json new file mode 100644 index 000000000..23b2a3400 --- /dev/null +++ b/src/plugins/md_comments/locales/bn.json @@ -0,0 +1,16 @@ +{ + "@metadata": { + "authors": [ + "আফতাবুজ্জামান" + ] + }, + "ep_comments.comment": "মন্তব্য", + "ep_comments.comments": "মন্তব্য", + "ep_comments.show_comments": "মন্তব্য দেখান", + "ep_comments.comments_template.comment.value": "মন্তব্য", + "ep_comments.comments_template.cancel.value": "বাতিল", + "ep_comments.comments_template.reply.value": "উত্তর দিন", + "ep_comments.comments_template.reply.placeholder": "উত্তর দিন", + "ep_comments.comments_template.edit_comment.save": "সংরক্ষণ", + "ep_comments.comments_template.edit_comment.cancel": "বাতিল" +} diff --git a/src/plugins/md_comments/locales/da.json b/src/plugins/md_comments/locales/da.json new file mode 100644 index 000000000..c4629c01c --- /dev/null +++ b/src/plugins/md_comments/locales/da.json @@ -0,0 +1,32 @@ +{ + "@metadata": { + "authors": [ + "Antonla", + "Saederup92" + ] + }, + "ep_comments.comment": "Kommentar", + "ep_comments.comments": "Kommentarer", + "ep_comments.add_comment.title": "Tilføj ny kommentar ved markering", + "ep_comments.add_comment": "Tilføj ny kommentar ved markering", + "ep_comments.add_comment.hint": "Marker venligst først teksten for at kommentere", + "ep_comments.delete_comment.title": "Slet denne kommentar", + "ep_comments.edit_comment.title": "Rediger denne kommentar", + "ep_comments.show_comments": "Vis kommentarer", + "ep_comments.comments_template.suggested_change": "Foreslået ændring", + "ep_comments.comments_template.from": "Fra", + "ep_comments.comments_template.accept_change.value": "Godkend ændring", + "ep_comments.comments_template.revert_change.value": "Fortryd ændring", + "ep_comments.comments_template.suggested_change_from": "Foreslået ændring fra \"{{changeFrom}}\" til \"{{changeTo}}\"", + "ep_comments.comments_template.suggest_change_from": "Foreslå ændring fra \"{{changeFrom}}\" til", + "ep_comments.comments_template.to": "Til", + "ep_comments.comments_template.include_suggestion": "Inkluder foreslået ændring", + "ep_comments.comments_template.comment.value": "Kommentar", + "ep_comments.comments_template.cancel.value": "Annuller", + "ep_comments.comments_template.reply.value": "Svar", + "ep_comments.comments_template.reply.placeholder": "Svar", + "ep_comments.comments_template.edit_comment.save": "gem", + "ep_comments.comments_template.edit_comment.cancel": "annuller", + "ep_comments.error.edit_unauth": "Du kan ikke redigere andre brugeres kommentarer!", + "ep_comments.error.delete_unauth": "Du kan ikke slette andre brugeres kommentarer!" +} diff --git a/src/plugins/md_comments/locales/de.json b/src/plugins/md_comments/locales/de.json new file mode 100644 index 000000000..865fa17c5 --- /dev/null +++ b/src/plugins/md_comments/locales/de.json @@ -0,0 +1,29 @@ +{ + "@metadata": { + "authors": [] + }, + "ep_comments.comment": "Kommentar", + "ep_comments.comments": "Kommentare", + "ep_comments.add_comment.title": "Kommentar zur Auswahl hinzufügen", + "ep_comments.add_comment": "Kommentar zur Auswahl hinzufügen", + "ep_comments.add_comment.hint": "Bitte wählen Sie zuerst den zu kommentierenden Text aus", + "ep_comments.delete_comment.title": "Diesen Kommentar löschen", + "ep_comments.edit_comment.title": "Edit this comment", + "ep_comments.show_comments": "Kommentare anzeigen", + "ep_comments.comments_template.suggested_change": "Vorgeschlagene Änderung", + "ep_comments.comments_template.from": "von", + "ep_comments.comments_template.accept_change.value": "Änderung akzeptieren", + "ep_comments.comments_template.revert_change.value": "Änderung zurücknehmen", + "ep_comments.comments_template.suggested_change_from": "Vorgeschlagene Änderung von", + "ep_comments.comments_template.suggest_change_from": "von", + "ep_comments.comments_template.to": "zu", + "ep_comments.comments_template.include_suggestion": "Änderung vorschlagen", + "ep_comments.comments_template.comment.value": "Kommentar", + "ep_comments.comments_template.cancel.value": "Abbrechen", + "ep_comments.comments_template.reply.value": "Antworten", + "ep_comments.comments_template.reply.placeholder": "Antworten", + "ep_comments.comments_template.edit_comment.save": "save", + "ep_comments.comments_template.edit_comment.cancel": "cancel", + "ep_comments.error.edit_unauth": "You cannot edit other users comments!", + "ep_comments.error.delete_unauth": "You cannot delete other users comments!" +} diff --git a/src/plugins/md_comments/locales/diq.json b/src/plugins/md_comments/locales/diq.json new file mode 100644 index 000000000..67cee5c6c --- /dev/null +++ b/src/plugins/md_comments/locales/diq.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "Mirzali" + ] + }, + "ep_comments.comment": "Mışewre", + "ep_comments.comments_template.comment.value": "Mışewre", + "ep_comments.comments_template.cancel.value": "Bıtexelne", + "ep_comments.comments_template.reply.value": "Cewab bıde", + "ep_comments.comments_template.reply.placeholder": "Cewab bıde", + "ep_comments.comments_template.edit_comment.save": "qeyd ke", + "ep_comments.comments_template.edit_comment.cancel": "bıtexelne" +} diff --git a/src/plugins/md_comments/locales/en.json b/src/plugins/md_comments/locales/en.json new file mode 100644 index 000000000..ba286e541 --- /dev/null +++ b/src/plugins/md_comments/locales/en.json @@ -0,0 +1,26 @@ +{ + "ep_comments.comment" : "Comment", + "ep_comments.comments" : "Comments", + "ep_comments.add_comment.title" : "Add new comment on selection", + "ep_comments.add_comment" : "Add new comment on selection", + "ep_comments.add_comment.hint" : "Please first select the text to comment", + "ep_comments.delete_comment.title" : "Delete this comment", + "ep_comments.edit_comment.title" : "Edit this comment", + "ep_comments.show_comments" : "Show Comments", + "ep_comments.comments_template.suggested_change" : "Suggested Change", + "ep_comments.comments_template.from" : "From", + "ep_comments.comments_template.accept_change.value" : "Accept Change", + "ep_comments.comments_template.revert_change.value" : "Revert Change", + "ep_comments.comments_template.suggested_change_from" : "Suggested change from \"{{changeFrom}}\" to \"{{changeTo}}\"", + "ep_comments.comments_template.suggest_change_from" : "Suggest change from \"{{changeFrom}}\" to", + "ep_comments.comments_template.to" : "To", + "ep_comments.comments_template.include_suggestion" : "Include suggested change", + "ep_comments.comments_template.comment.value" : "Comment", + "ep_comments.comments_template.cancel.value" : "Cancel", + "ep_comments.comments_template.reply.value" : "Reply", + "ep_comments.comments_template.reply.placeholder" : "Reply", + "ep_comments.comments_template.edit_comment.save" : "save", + "ep_comments.comments_template.edit_comment.cancel" :"cancel", + "ep_comments.error.edit_unauth": "You cannot edit other users comments!", + "ep_comments.error.delete_unauth": "You cannot delete other users comments!" +} diff --git a/src/plugins/md_comments/locales/es.json b/src/plugins/md_comments/locales/es.json new file mode 100644 index 000000000..7a7c0c725 --- /dev/null +++ b/src/plugins/md_comments/locales/es.json @@ -0,0 +1,24 @@ +{ + "@metadata": { + "authors": [ + "Avengium", + "Jakeukalane" + ] + }, + "ep_comments.comment": "Comentario", + "ep_comments.comments": "Comentarios", + "ep_comments.add_comment.hint": "Primero selecciona el texto para comentar", + "ep_comments.edit_comment.title": "Editar este comentario", + "ep_comments.comments_template.accept_change.value": "Aceptar cambio", + "ep_comments.comments_template.revert_change.value": "Revertir cambio", + "ep_comments.comments_template.suggested_change_from": "Cambio sugerido de \"{{changeFrom}}\" a \"{{changeTo}}\"", + "ep_comments.comments_template.suggest_change_from": "Sugerir cambio de \"{{changeFrom}}\" a", + "ep_comments.comments_template.include_suggestion": "Incluir cambio sugerido", + "ep_comments.comments_template.comment.value": "Comentario", + "ep_comments.comments_template.cancel.value": "Cancelar", + "ep_comments.comments_template.reply.value": "Responder", + "ep_comments.comments_template.edit_comment.save": "Guardar", + "ep_comments.comments_template.edit_comment.cancel": "cancelar", + "ep_comments.error.edit_unauth": "¡No puedes editar los comentarios de otros usuarios!", + "ep_comments.error.delete_unauth": "¡No puedes eliminar los comentarios de otros usuarios!" +} diff --git a/src/plugins/md_comments/locales/eu.json b/src/plugins/md_comments/locales/eu.json new file mode 100644 index 000000000..4693dd06c --- /dev/null +++ b/src/plugins/md_comments/locales/eu.json @@ -0,0 +1,29 @@ +{ + "@metadata": { + "authors": [] + }, + "ep_comments.comment": "Iruzkina", + "ep_comments.comments": "Iruzkinak", + "ep_comments.add_comment.title": "Gehitu iruzkin berria hautapenari", + "ep_comments.add_comment": "Gehitu iruzkin berria hautapenari", + "ep_comments.add_comment.hint": "Aukeratu ezazu lehenengo iruzkina gehitzeko testua", + "ep_comments.delete_comment.title": "Ezabatu iruzkin hau", + "ep_comments.edit_comment.title": "Editatu iruzkin hau", + "ep_comments.show_comments": "Erakutsi iruzkinak", + "ep_comments.comments_template.suggested_change": "Proposatutako aldaketa", + "ep_comments.comments_template.from": "Jatorria", + "ep_comments.comments_template.accept_change.value": "Onartu aldaketa", + "ep_comments.comments_template.revert_change.value": "Leheneratu aldaketa", + "ep_comments.comments_template.suggested_change_from": "Proposatutako aldaketa \"{{changeFrom}}\"-(e)tik \"{{changeTo}}\"-(e)ra", + "ep_comments.comments_template.suggest_change_from": "Proposatu aldaketa \"{{changeFrom}}\"(e)tik hurrengo honetara:", + "ep_comments.comments_template.to": "Ondorengoa", + "ep_comments.comments_template.include_suggestion": "Gehitu proposatutako aldaketak", + "ep_comments.comments_template.comment.value": "Iruzkina", + "ep_comments.comments_template.cancel.value": "Utzi", + "ep_comments.comments_template.reply.value": "Erantzun", + "ep_comments.comments_template.reply.placeholder": "Erantzun", + "ep_comments.comments_template.edit_comment.save": "gorde", + "ep_comments.comments_template.edit_comment.cancel": "utzi", + "ep_comments.error.edit_unauth": "Ezin dituzu beste erabiltzaileen iruzkinak editatu!", + "ep_comments.error.delete_unauth": "Ezin dituzu beste erabiltzaileen iruzkinak ezabatu!" +} diff --git a/src/plugins/md_comments/locales/fa.json b/src/plugins/md_comments/locales/fa.json new file mode 100644 index 000000000..9ab5fd17f --- /dev/null +++ b/src/plugins/md_comments/locales/fa.json @@ -0,0 +1,29 @@ +{ + "@metadata": { + "authors": [ + "Jeeputer" + ] + }, + "ep_comments.comment": "توضیح", + "ep_comments.comments": "نظرات", + "ep_comments.add_comment": "افزودن توضیح تازه برای منتخب‌ها", + "ep_comments.delete_comment.title": "حذف این نظر", + "ep_comments.edit_comment.title": "ویرایش این نظر", + "ep_comments.show_comments": "نمایش نظرات", + "ep_comments.comments_template.suggested_change": "تغییر پیشنهادی", + "ep_comments.comments_template.from": "از", + "ep_comments.comments_template.accept_change.value": "پذیرفتن تغییر", + "ep_comments.comments_template.revert_change.value": "واگردانی تغییر", + "ep_comments.comments_template.suggested_change_from": "تغییر پیسنهادی از «{{changeFrom}}» به «{{changeTo}}»", + "ep_comments.comments_template.suggest_change_from": "تغییر پیشنهاد از «{{changeFrom}}» به", + "ep_comments.comments_template.to": "به", + "ep_comments.comments_template.include_suggestion": "گنجاندن تغییر پیشنهادی", + "ep_comments.comments_template.comment.value": "نظر", + "ep_comments.comments_template.cancel.value": "لغو", + "ep_comments.comments_template.reply.value": "پاسخ", + "ep_comments.comments_template.reply.placeholder": "پاسخ", + "ep_comments.comments_template.edit_comment.save": "ذخیره", + "ep_comments.comments_template.edit_comment.cancel": "لغو", + "ep_comments.error.edit_unauth": "نمی‌توانید نظرات کاربران دیگر را ویرایش کنید!", + "ep_comments.error.delete_unauth": "نمی‌توانید نظرات کاربران دیگر را حذف کنید!" +} diff --git a/src/plugins/md_comments/locales/fi.json b/src/plugins/md_comments/locales/fi.json new file mode 100644 index 000000000..877bc0654 --- /dev/null +++ b/src/plugins/md_comments/locales/fi.json @@ -0,0 +1,25 @@ +{ + "@metadata": { + "authors": [ + "MITO" + ] + }, + "ep_comments.comment": "Kommentoi", + "ep_comments.comments": "Kommentit", + "ep_comments.delete_comment.title": "Poista tämä kommentti", + "ep_comments.edit_comment.title": "Muokkaa tätä kommenttia", + "ep_comments.show_comments": "Näytä kommentit", + "ep_comments.comments_template.suggested_change": "Muutosehdotus", + "ep_comments.comments_template.from": "Lähettäjä", + "ep_comments.comments_template.accept_change.value": "Hyväksy muutos", + "ep_comments.comments_template.revert_change.value": "Peruuta muutos", + "ep_comments.comments_template.to": "Vastaanottaja", + "ep_comments.comments_template.comment.value": "Kommentoi", + "ep_comments.comments_template.cancel.value": "Peruuta", + "ep_comments.comments_template.reply.value": "Vastaa", + "ep_comments.comments_template.reply.placeholder": "Vastaa", + "ep_comments.comments_template.edit_comment.save": "tallenna", + "ep_comments.comments_template.edit_comment.cancel": "peruuta", + "ep_comments.error.edit_unauth": "Et voi muokata toisten käyttäjien kommentteja!", + "ep_comments.error.delete_unauth": "Et voi poistaa muiden käyttäjien kommentteja!" +} diff --git a/src/plugins/md_comments/locales/fr.json b/src/plugins/md_comments/locales/fr.json new file mode 100644 index 000000000..a91367f61 --- /dev/null +++ b/src/plugins/md_comments/locales/fr.json @@ -0,0 +1,29 @@ +{ + "@metadata": { + "authors": [] + }, + "ep_comments.comment": "Annotation", + "ep_comments.comments": "Annotations", + "ep_comments.add_comment.title": "Annoter la sélection", + "ep_comments.add_comment": "Annoter la sélection", + "ep_comments.add_comment.hint": "Vous devez d'abord sélectionner un texte à annoter", + "ep_comments.delete_comment.title": "Supprimer cette annotation", + "ep_comments.edit_comment.title": "Modifier ce commentaire", + "ep_comments.show_comments": "Afficher les annotations", + "ep_comments.comments_template.suggested_change": "Modification proposée", + "ep_comments.comments_template.from": "Remplacer", + "ep_comments.comments_template.accept_change.value": "Appliquer la proposition", + "ep_comments.comments_template.revert_change.value": "Annuler la proposition", + "ep_comments.comments_template.suggested_change_from": "Propose de remplacer \"{{changeFrom}}\" par \"{{changeTo}}\"", + "ep_comments.comments_template.suggest_change_from": "Remplacer \"{{changeFrom}}\" par", + "ep_comments.comments_template.to": "Par", + "ep_comments.comments_template.include_suggestion": "Proposer une modification", + "ep_comments.comments_template.comment.value": "Annotation", + "ep_comments.comments_template.cancel.value": "Annuler", + "ep_comments.comments_template.reply.value": "Répondre", + "ep_comments.comments_template.reply.placeholder": "Répondre", + "ep_comments.comments_template.edit_comment.save": "Enregistrer", + "ep_comments.comments_template.edit_comment.cancel": "Annuler", + "ep_comments.error.edit_unauth": "Vous ne pouvez pas modifier les commentaires des autres utilisatrices et utilisateurs !", + "ep_comments.error.delete_unauth": "Vous ne pouvez pas supprimer les commentaires des autres utilisatrices et utilisateurs !" +} diff --git a/src/plugins/md_comments/locales/gur.json b/src/plugins/md_comments/locales/gur.json new file mode 100644 index 000000000..a29879b16 --- /dev/null +++ b/src/plugins/md_comments/locales/gur.json @@ -0,0 +1,34 @@ +{ + "@metadata": { + "authors": [ + "Adignyoke", + "Akakiiri", + "Akoonaba", + "Amoramah" + ] + }, + "ep_comments.comment": "Fu yelisum", + "ep_comments.comments": "Lebesegɔ", + "ep_comments.add_comment.title": "Pa'asɛ putɛpaalɛ loisego zuo", + "ep_comments.add_comment": "Pa'asɛ yelesum paalega bo fu loorɛ", + "ep_comments.add_comment.hint": "Zaam zaam loe gɔleseko ti fu yeti fu pa'asɛ la yia", + "ep_comments.delete_comment.title": "Yesi lebesegɔ wã basɛ", + "ep_comments.edit_comment.title": "Demese sɔsekãna wa", + "ep_comments.show_comments": "Pa'ale fu yelesum", + "ep_comments.comments_template.suggested_change": "Pa'asɛ putɛ̃'ɛrɛ tee", + "ep_comments.comments_template.from": "Ze'ele", + "ep_comments.comments_template.accept_change.value": "Sakɛ Teerɛ", + "ep_comments.comments_template.revert_change.value": "Lebege tee", + "ep_comments.comments_template.suggested_change_from": "Puti'irɛ teere ze'ele\"{{tee ze'ele}}\" bo \"{{ tee bo}}\"", + "ep_comments.comments_template.suggest_change_from": "Pa'asɛ putɛ̃'ɛrɛ teere ze'ele{{ teere ze'ele}}", + "ep_comments.comments_template.to": "Wɛ̃", + "ep_comments.comments_template.include_suggestion": "Pa'asɛ putɛ̃'ɛrɛ teere", + "ep_comments.comments_template.comment.value": "Lebese", + "ep_comments.comments_template.cancel.value": "Sanbasɛ", + "ep_comments.comments_template.reply.value": "Lerege", + "ep_comments.comments_template.reply.placeholder": "Lerege", + "ep_comments.comments_template.edit_comment.save": "Seefe", + "ep_comments.comments_template.edit_comment.cancel": "Saabasɛ", + "ep_comments.error.edit_unauth": "Fu kan ta'am demese se'em sɔsega", + "ep_comments.error.delete_unauth": "Fu kanta'am saalum basɛba putɛra basɛ" +} diff --git a/src/plugins/md_comments/locales/ha.json b/src/plugins/md_comments/locales/ha.json new file mode 100644 index 000000000..9f3cc9c9f --- /dev/null +++ b/src/plugins/md_comments/locales/ha.json @@ -0,0 +1,31 @@ +{ + "@metadata": { + "authors": [ + "Abbaty", + "Omar Ali", + "Salihu aliyu" + ] + }, + "ep_comments.comment": "Ra'ayi", + "ep_comments.comments": "sharhi", + "ep_comments.add_comment.title": "Kara sabon ra'ayi akan zaben", + "ep_comments.add_comment": "saka sabon bayani a bangaren", + "ep_comments.add_comment.hint": "zaba farkon sakon sharhi", + "ep_comments.delete_comment.title": "goge wannan sharhin", + "ep_comments.edit_comment.title": "ghera wannan sharhin", + "ep_comments.show_comments": "nuna sharhi", + "ep_comments.comments_template.suggested_change": "chanza shawara", + "ep_comments.comments_template.from": "daga", + "ep_comments.comments_template.accept_change.value": "karban chanji", + "ep_comments.comments_template.revert_change.value": "dawo da chanji", + "ep_comments.comments_template.to": "zuwa", + "ep_comments.comments_template.include_suggestion": "sanya damar sauyi", + "ep_comments.comments_template.comment.value": "Ra'ayi", + "ep_comments.comments_template.cancel.value": "sokewa", + "ep_comments.comments_template.reply.value": "Maidawa", + "ep_comments.comments_template.reply.placeholder": "maidawa", + "ep_comments.comments_template.edit_comment.save": "adanawa", + "ep_comments.comments_template.edit_comment.cancel": "Soke", + "ep_comments.error.edit_unauth": "bazaka iya gheran sharhin wani bah", + "ep_comments.error.delete_unauth": "bazaka iya goge sharhin wani bah" +} diff --git a/src/plugins/md_comments/locales/he.json b/src/plugins/md_comments/locales/he.json new file mode 100644 index 000000000..64ddd3e31 --- /dev/null +++ b/src/plugins/md_comments/locales/he.json @@ -0,0 +1,31 @@ +{ + "@metadata": { + "authors": [ + "Amire80" + ] + }, + "ep_comments.comment": "הערה", + "ep_comments.comments": "הערות", + "ep_comments.add_comment.title": "הוספת הערה חדשה לבחירה", + "ep_comments.add_comment": "הוספת הערה חדשה לבחירה", + "ep_comments.add_comment.hint": "נא לבחור קודם טקסט כדי להעיר", + "ep_comments.delete_comment.title": "למחוק את ההערה הזאת", + "ep_comments.edit_comment.title": "לערוך את ההערה הזאת", + "ep_comments.show_comments": "הצגת ההערה", + "ep_comments.comments_template.suggested_change": "שינוי מוצע", + "ep_comments.comments_template.from": "מאת", + "ep_comments.comments_template.accept_change.value": "קבלת השינוי", + "ep_comments.comments_template.revert_change.value": "שחזור השינוי", + "ep_comments.comments_template.suggested_change_from": "שינוי מוצע מהטקסט \"{{changeFrom}}\" לטקסט \"{{changeTo}}\"", + "ep_comments.comments_template.suggest_change_from": "להציע שינוי של הטקסט \"{{changeFrom}}\" לטקסט", + "ep_comments.comments_template.to": "לטקסט", + "ep_comments.comments_template.include_suggestion": "לכלול את השינוי המוצע", + "ep_comments.comments_template.comment.value": "הערה", + "ep_comments.comments_template.cancel.value": "ביטול", + "ep_comments.comments_template.reply.value": "להשיב", + "ep_comments.comments_template.reply.placeholder": "להשיב", + "ep_comments.comments_template.edit_comment.save": "שמירה", + "ep_comments.comments_template.edit_comment.cancel": "ביטול", + "ep_comments.error.edit_unauth": "אין לך אפשרות לערוך הערות של משתמשים אחרים!", + "ep_comments.error.delete_unauth": "אין לך אפשרות למחוק הערות של משתמשים אחרים!" +} diff --git a/src/plugins/md_comments/locales/hi.json b/src/plugins/md_comments/locales/hi.json new file mode 100644 index 000000000..15a5482f8 --- /dev/null +++ b/src/plugins/md_comments/locales/hi.json @@ -0,0 +1,14 @@ +{ + "@metadata": { + "authors": [ + "Abijeet Patro" + ] + }, + "ep_comments.comment": "टिप्पणी", + "ep_comments.comments": "टिप्पणियाँ", + "ep_comments.comments_template.cancel.value": "रद्द करें", + "ep_comments.comments_template.reply.value": "जवाब दें", + "ep_comments.comments_template.reply.placeholder": "जवाब दें", + "ep_comments.comments_template.edit_comment.save": "सहेजें", + "ep_comments.comments_template.edit_comment.cancel": "रद्द करें" +} diff --git a/src/plugins/md_comments/locales/hu.json b/src/plugins/md_comments/locales/hu.json new file mode 100644 index 000000000..059c71ad9 --- /dev/null +++ b/src/plugins/md_comments/locales/hu.json @@ -0,0 +1,29 @@ +{ + "@metadata": { + "authors": [] + }, + "ep_comments.comment": "Megjegyzés", + "ep_comments.comments": "Megjegyzések", + "ep_comments.add_comment.title": "Új megjegyzés hozzáadása a kijelöléshez", + "ep_comments.add_comment": "Új megjegyzés hozzáadása a kijelöléshez", + "ep_comments.add_comment.hint": "Először jelölje meg a szöveget a megjegyzéshez", + "ep_comments.delete_comment.title": "Megjegyzés törlése", + "ep_comments.edit_comment.title": "Megjegyzés szerkesztése", + "ep_comments.show_comments": "Megjegyzések megjelenítése", + "ep_comments.comments_template.suggested_change": "Javasolt változtatás", + "ep_comments.comments_template.from": "Feladó", + "ep_comments.comments_template.accept_change.value": "Változtatás elfogadása", + "ep_comments.comments_template.revert_change.value": "Változtatás visszavonása", + "ep_comments.comments_template.suggested_change_from": "Változtatás javasolta:", + "ep_comments.comments_template.suggest_change_from": "Változtatás javasolta:", + "ep_comments.comments_template.to": "Címzett:", + "ep_comments.comments_template.include_suggestion": "Javasolt változtatás tartalmazása", + "ep_comments.comments_template.comment.value": "Megjegyzés", + "ep_comments.comments_template.cancel.value": "Mégsem", + "ep_comments.comments_template.reply.value": "Válasz", + "ep_comments.comments_template.reply.placeholder": "Válasz", + "ep_comments.comments_template.edit_comment.save": "mentés", + "ep_comments.comments_template.edit_comment.cancel": "mégsem", + "ep_comments.error.edit_unauth": "You cannot edit other users comments!", + "ep_comments.error.delete_unauth": "You cannot delete other users comments!" +} diff --git a/src/plugins/md_comments/locales/io.json b/src/plugins/md_comments/locales/io.json new file mode 100644 index 000000000..3d9bf0f4d --- /dev/null +++ b/src/plugins/md_comments/locales/io.json @@ -0,0 +1,32 @@ +{ + "@metadata": { + "authors": [ + "JSantos", + "Joao Xavier" + ] + }, + "ep_comments.comment": "Komento", + "ep_comments.comments": "Komenti", + "ep_comments.add_comment.title": "Adjuntez nova komento en l'areo selektita", + "ep_comments.add_comment": "Adjuntez nova komento en l'areo selektita", + "ep_comments.add_comment.hint": "Selektez l'unesma texto por komentar", + "ep_comments.delete_comment.title": "Efacar ca komento", + "ep_comments.edit_comment.title": "Redaktar ca komento", + "ep_comments.show_comments": "Montrar komenti", + "ep_comments.comments_template.suggested_change": "Sugestita modifikuro", + "ep_comments.comments_template.from": "De", + "ep_comments.comments_template.accept_change.value": "Aceptar modifikuro", + "ep_comments.comments_template.revert_change.value": "Desfacar modifikuro", + "ep_comments.comments_template.suggested_change_from": "Sugestita modifikuro de \"{{changeFrom}}\" a \"{{changeTo}}\"", + "ep_comments.comments_template.suggest_change_from": "Modifikuro sugestita de \"{{changeFrom}}\" a", + "ep_comments.comments_template.to": "A", + "ep_comments.comments_template.include_suggestion": "Inkluzez chanjo propozata", + "ep_comments.comments_template.comment.value": "Komento", + "ep_comments.comments_template.cancel.value": "Desfacar", + "ep_comments.comments_template.reply.value": "Respondar", + "ep_comments.comments_template.reply.placeholder": "Respondo", + "ep_comments.comments_template.edit_comment.save": "konservez", + "ep_comments.comments_template.edit_comment.cancel": "desfacar", + "ep_comments.error.edit_unauth": "Vu ne povas redaktar la komenti di altriǃ", + "ep_comments.error.delete_unauth": "Vu ne povas efakar la komenti di altriǃ" +} diff --git a/src/plugins/md_comments/locales/it.json b/src/plugins/md_comments/locales/it.json new file mode 100644 index 000000000..a9f823ad4 --- /dev/null +++ b/src/plugins/md_comments/locales/it.json @@ -0,0 +1,32 @@ +{ + "@metadata": { + "authors": [ + "Ajeje Brazorf", + "Beta16" + ] + }, + "ep_comments.comment": "Commento", + "ep_comments.comments": "Commenti", + "ep_comments.add_comment.title": "Aggiungi nuovo commento sulla selezione", + "ep_comments.add_comment": "Aggiungi nuovo commento sulla selezione", + "ep_comments.add_comment.hint": "Devi prima selezionare un testo su cui aggiungere il commento!", + "ep_comments.delete_comment.title": "Cancella questo commento", + "ep_comments.edit_comment.title": "Modifica questo commento", + "ep_comments.show_comments": "Visualizza commenti", + "ep_comments.comments_template.suggested_change": "Modifica suggerita", + "ep_comments.comments_template.from": "Da", + "ep_comments.comments_template.accept_change.value": "Accetta modifica", + "ep_comments.comments_template.revert_change.value": "Annulla modifica", + "ep_comments.comments_template.suggested_change_from": "Modifica suggerita da \"{{changeFrom}}\" a \"{{changeTo}}\"", + "ep_comments.comments_template.suggest_change_from": "Suggerisci modifica da \"{{changeFrom}}\" a", + "ep_comments.comments_template.to": "A", + "ep_comments.comments_template.include_suggestion": "Suggerisci modfica", + "ep_comments.comments_template.comment.value": "Commenta", + "ep_comments.comments_template.cancel.value": "Annulla", + "ep_comments.comments_template.reply.value": "Rispondi", + "ep_comments.comments_template.reply.placeholder": "Rispondi", + "ep_comments.comments_template.edit_comment.save": "salva", + "ep_comments.comments_template.edit_comment.cancel": "Cancella", + "ep_comments.error.edit_unauth": "Non puoi modificare i commenti di altri autori!", + "ep_comments.error.delete_unauth": "Non puoi eliminare i commenti di altri autori!" +} diff --git a/src/plugins/md_comments/locales/ko.json b/src/plugins/md_comments/locales/ko.json new file mode 100644 index 000000000..5e39f129c --- /dev/null +++ b/src/plugins/md_comments/locales/ko.json @@ -0,0 +1,30 @@ +{ + "@metadata": { + "authors": [ + "Ykhwong", + "그냥기여자" + ] + }, + "ep_comments.comment": "의견", + "ep_comments.comments": "의견", + "ep_comments.add_comment.title": "선택 영역에 새 댓글 추가", + "ep_comments.add_comment": "선택 영역에 새 댓글 추가", + "ep_comments.add_comment.hint": "먼저 의견을 전달할 대상 텍스트를 선택해 주십시오", + "ep_comments.delete_comment.title": "이 의견 삭제", + "ep_comments.edit_comment.title": "이 의견 편집하기", + "ep_comments.show_comments": "의견 표시", + "ep_comments.comments_template.suggested_change": "제안된 변경 사항", + "ep_comments.comments_template.accept_change.value": "변경 수락", + "ep_comments.comments_template.revert_change.value": "변경사항 되돌리기", + "ep_comments.comments_template.suggested_change_from": "\"{{changeFrom}}\"에서 \"{{changeTo}}\"(으)로 변경 제안됨", + "ep_comments.comments_template.suggest_change_from": "\"{{changeFrom}}\"에서 다음으로 변경할 것으로 제안:", + "ep_comments.comments_template.include_suggestion": "제안된 변경사항 포함", + "ep_comments.comments_template.comment.value": "의견", + "ep_comments.comments_template.cancel.value": "취소", + "ep_comments.comments_template.reply.value": "답변", + "ep_comments.comments_template.reply.placeholder": "답변", + "ep_comments.comments_template.edit_comment.save": "저장", + "ep_comments.comments_template.edit_comment.cancel": "취소", + "ep_comments.error.edit_unauth": "다른 사용자의 의견을 편집할 수 없습니다!", + "ep_comments.error.delete_unauth": "다른 사용자의 의견을 삭제할 수 없습니다!" +} diff --git a/src/plugins/md_comments/locales/lb.json b/src/plugins/md_comments/locales/lb.json new file mode 100644 index 000000000..2244ffae3 --- /dev/null +++ b/src/plugins/md_comments/locales/lb.json @@ -0,0 +1,25 @@ +{ + "@metadata": { + "authors": [ + "Robby" + ] + }, + "ep_comments.comment": "Bemierkung", + "ep_comments.comments": "Bemierkungen", + "ep_comments.delete_comment.title": "Dës Bemierkung läschen", + "ep_comments.edit_comment.title": "Dës Bemierkung änneren", + "ep_comments.show_comments": "Bemierkunge weisen", + "ep_comments.comments_template.suggested_change": "Proposéiert Ännerung", + "ep_comments.comments_template.from": "Vum", + "ep_comments.comments_template.accept_change.value": "Ännerung akzeptéieren", + "ep_comments.comments_template.revert_change.value": "Ännerung zrécksetzen", + "ep_comments.comments_template.to": "Fir", + "ep_comments.comments_template.comment.value": "Bemierkung", + "ep_comments.comments_template.cancel.value": "Ofbriechen", + "ep_comments.comments_template.reply.value": "Äntweren", + "ep_comments.comments_template.reply.placeholder": "Äntweren", + "ep_comments.comments_template.edit_comment.save": "späicheren", + "ep_comments.comments_template.edit_comment.cancel": "ofbriechen", + "ep_comments.error.edit_unauth": "Dir kënnt d'Bemierkunge vun anere Benotzer net änneren", + "ep_comments.error.delete_unauth": "Dir kënnt d'Bemierkunge vun anere Benotzer net läschen!" +} diff --git a/src/plugins/md_comments/locales/mk.json b/src/plugins/md_comments/locales/mk.json new file mode 100644 index 000000000..1195d0cdc --- /dev/null +++ b/src/plugins/md_comments/locales/mk.json @@ -0,0 +1,31 @@ +{ + "@metadata": { + "authors": [ + "Bjankuloski06" + ] + }, + "ep_comments.comment": "Коментар", + "ep_comments.comments": "Коментари", + "ep_comments.add_comment.title": "Дај нов коментар за избраното", + "ep_comments.add_comment": "Дај нов коментар на избраното", + "ep_comments.add_comment.hint": "Најпрвин изберете текст за коментирање", + "ep_comments.delete_comment.title": "Избриши го коментаров", + "ep_comments.edit_comment.title": "Измени го коментаров", + "ep_comments.show_comments": "Прикажи коментари", + "ep_comments.comments_template.suggested_change": "Предложена промена", + "ep_comments.comments_template.from": "Од", + "ep_comments.comments_template.accept_change.value": "Прифати промена", + "ep_comments.comments_template.revert_change.value": "Отповикај промена", + "ep_comments.comments_template.suggested_change_from": "Предложена промена од „{{changeFrom}}“ во „{{changeTo}}“", + "ep_comments.comments_template.suggest_change_from": "Предложена промена од „{{changeFrom}}“ во", + "ep_comments.comments_template.to": "На", + "ep_comments.comments_template.include_suggestion": "Вклучи предложена промена", + "ep_comments.comments_template.comment.value": "Коментирај", + "ep_comments.comments_template.cancel.value": "Откажи", + "ep_comments.comments_template.reply.value": "Одговори", + "ep_comments.comments_template.reply.placeholder": "Одговори", + "ep_comments.comments_template.edit_comment.save": "зачувај", + "ep_comments.comments_template.edit_comment.cancel": "откажи", + "ep_comments.error.edit_unauth": "Не можеда те менувате туѓи коментари!", + "ep_comments.error.delete_unauth": "Не можете да бришете туѓи коментари!" +} diff --git a/src/plugins/md_comments/locales/nl.json b/src/plugins/md_comments/locales/nl.json new file mode 100644 index 000000000..7a700e3fc --- /dev/null +++ b/src/plugins/md_comments/locales/nl.json @@ -0,0 +1,31 @@ +{ + "@metadata": { + "authors": [ + "McDutchie" + ] + }, + "ep_comments.comment": "Opmerking", + "ep_comments.comments": "Opmerkingen", + "ep_comments.add_comment.title": "Voeg opmerking toe aan selectie", + "ep_comments.add_comment": "Voeg opmerking toe aan selectie", + "ep_comments.add_comment.hint": "Selecteer eerst een stuk tekst om een opmerking aan toe te voegen", + "ep_comments.delete_comment.title": "Verwijder deze opmerking", + "ep_comments.edit_comment.title": "Bewerk deze opmerking", + "ep_comments.show_comments": "Toon opmerkingen", + "ep_comments.comments_template.suggested_change": "Voorgestelde wijziging", + "ep_comments.comments_template.from": "Van", + "ep_comments.comments_template.accept_change.value": "Accepteer wijziging", + "ep_comments.comments_template.revert_change.value": "Draai wijziging terug", + "ep_comments.comments_template.suggested_change_from": "Voorgestelde wijziging van \"{{changeFrom}}\" in \"{{changeTo}}\"", + "ep_comments.comments_template.suggest_change_from": "Voorgestelde wijziging van \"{{changeFrom}}\" in", + "ep_comments.comments_template.to": "Naar", + "ep_comments.comments_template.include_suggestion": "Voeg voorgestelde wijziging toe", + "ep_comments.comments_template.comment.value": "Opmerking", + "ep_comments.comments_template.cancel.value": "Annuleer", + "ep_comments.comments_template.reply.value": "Antwoord", + "ep_comments.comments_template.reply.placeholder": "Antwoord", + "ep_comments.comments_template.edit_comment.save": "bewaar", + "ep_comments.comments_template.edit_comment.cancel": "annuleer", + "ep_comments.error.edit_unauth": "You cannot edit other users comments!", + "ep_comments.error.delete_unauth": "You cannot delete other users comments!" +} diff --git a/src/plugins/md_comments/locales/oc.json b/src/plugins/md_comments/locales/oc.json new file mode 100644 index 000000000..783708b0e --- /dev/null +++ b/src/plugins/md_comments/locales/oc.json @@ -0,0 +1,15 @@ +{ + "@metadata": { + "authors": [ + "Quentí" + ] + }, + "ep_comments.comment": "Comentari", + "ep_comments.comments": "Comentaris", + "ep_comments.add_comment.title": "Apondre un comentari novèl sus la seleccion", + "ep_comments.comments_template.accept_change.value": "Acceptar la modificacion", + "ep_comments.comments_template.cancel.value": "Anullar", + "ep_comments.comments_template.reply.value": "Respondre", + "ep_comments.comments_template.edit_comment.save": "enregistrar", + "ep_comments.comments_template.edit_comment.cancel": "anullar" +} diff --git a/src/plugins/md_comments/locales/pl.json b/src/plugins/md_comments/locales/pl.json new file mode 100644 index 000000000..69772cf98 --- /dev/null +++ b/src/plugins/md_comments/locales/pl.json @@ -0,0 +1,29 @@ +{ + "@metadata": { + "authors": [] + }, + "ep_comments.comment": "Komentarz", + "ep_comments.comments": "Komentarze", + "ep_comments.add_comment.title": "Dodaj nowy komentarz do sekcji", + "ep_comments.add_comment": "Dodaj nowy komentarz do sekcji", + "ep_comments.add_comment.hint": "Najpierw wybierz tekst do skomentowania", + "ep_comments.delete_comment.title": "Usuń komentarz", + "ep_comments.edit_comment.title": "Edit this comment", + "ep_comments.show_comments": "Pokaż komentarze", + "ep_comments.comments_template.suggested_change": "Sugerowane zmiany", + "ep_comments.comments_template.from": "Od", + "ep_comments.comments_template.accept_change.value": "Zaakceptuj zmiany", + "ep_comments.comments_template.revert_change.value": "Przywróc zmiany", + "ep_comments.comments_template.suggested_change_from": "Sugerowana zmiana z", + "ep_comments.comments_template.suggest_change_from": "Zaproponuj zmiane z", + "ep_comments.comments_template.to": "Do", + "ep_comments.comments_template.include_suggestion": "Dołącz sugestie", + "ep_comments.comments_template.comment.value": "Komentarz", + "ep_comments.comments_template.cancel.value": "Anuluj", + "ep_comments.comments_template.reply.value": "Odpowiedź", + "ep_comments.comments_template.reply.placeholder": "Odpowiedź", + "ep_comments.comments_template.edit_comment.save": "save", + "ep_comments.comments_template.edit_comment.cancel": "cancel", + "ep_comments.error.edit_unauth": "You cannot edit other users comments!", + "ep_comments.error.delete_unauth": "You cannot delete other users comments!" +} diff --git a/src/plugins/md_comments/locales/pt-br.json b/src/plugins/md_comments/locales/pt-br.json new file mode 100644 index 000000000..b4562cd81 --- /dev/null +++ b/src/plugins/md_comments/locales/pt-br.json @@ -0,0 +1,26 @@ +{ + "ep_comments.comment" : "Comentário", + "ep_comments.comments" : "Comentários", + "ep_comments.add_comment.title" : "Adicionar novo comentário ao texto selecionado", + "ep_comments.add_comment" : "Adicionar novo comentário ao texto selecionado", + "ep_comments.add_comment.hint" : "Por favor, selecione primeiro o texto para comentar", + "ep_comments.delete_comment.title" : "Apagar este comentário", + "ep_comments.edit_comment.title" : "Editar este comentário", + "ep_comments.show_comments" : "Mostrar Comentários", + "ep_comments.comments_template.suggested_change" : "Suggested Change", + "ep_comments.comments_template.from" : "From", + "ep_comments.comments_template.accept_change.value" : "Aceitar Sugestão", + "ep_comments.comments_template.revert_change.value" : "Reverter Sugestão", + "ep_comments.comments_template.suggested_change_from" : "Alteração sugerida de", + "ep_comments.comments_template.suggest_change_from" : "Sugerir alteração de", + "ep_comments.comments_template.to" : "Para", + "ep_comments.comments_template.include_suggestion" : "Incluir alteração sugerida", + "ep_comments.comments_template.comment.value" : "Comentário", + "ep_comments.comments_template.cancel.value" : "Cancelar", + "ep_comments.comments_template.reply.value":"Responder", + "ep_comments.comments_template.reply.placeholder":"Responder", + "ep_comments.comments_template.edit_comment.save" : "salvar", + "ep_comments.comments_template.edit_comment.cancel" :"cancelar", + "ep_comments.error.edit_unauth": "You cannot edit other users comments!", + "ep_comments.error.delete_unauth": "You cannot delete other users comments!" +} \ No newline at end of file diff --git a/src/plugins/md_comments/locales/qqq.json b/src/plugins/md_comments/locales/qqq.json new file mode 100644 index 000000000..02e2274b2 --- /dev/null +++ b/src/plugins/md_comments/locales/qqq.json @@ -0,0 +1,9 @@ +{ + "@metadata": { + "authors": [ + "Ajeje Brazorf" + ] + }, + "ep_comments.comments_template.cancel.value": "{{Identical|Cancel}}", + "ep_comments.comments_template.edit_comment.cancel": "{{Identical|Cancel}}" +} diff --git a/src/plugins/md_comments/locales/ru.json b/src/plugins/md_comments/locales/ru.json new file mode 100644 index 000000000..6db54429a --- /dev/null +++ b/src/plugins/md_comments/locales/ru.json @@ -0,0 +1,31 @@ +{ + "@metadata": { + "authors": [ + "DDPAT" + ] + }, + "ep_comments.comment": "Примечание", + "ep_comments.comments": "Примечания", + "ep_comments.add_comment.title": "Добавьте примечание к выделенному тексту", + "ep_comments.add_comment": "Добавьте примечание к выделенному тексту", + "ep_comments.add_comment.hint": "Выделите текст чтобы создать примечание", + "ep_comments.delete_comment.title": "Удалить примечание", + "ep_comments.edit_comment.title": "Отредактировать примечание", + "ep_comments.show_comments": "Показывать примечания", + "ep_comments.comments_template.suggested_change": "Предлагаемое изменение", + "ep_comments.comments_template.from": "Заменить", + "ep_comments.comments_template.accept_change.value": "Принять изменение", + "ep_comments.comments_template.revert_change.value": "Отменить изменение", + "ep_comments.comments_template.suggested_change_from": "Предлагаемое изменение с «{{changeFrom}}» на «{{changeTo}}»", + "ep_comments.comments_template.suggest_change_from": "Предложить изменение с «{{changeFrom}}» на", + "ep_comments.comments_template.to": "на", + "ep_comments.comments_template.include_suggestion": "Предложить правку", + "ep_comments.comments_template.comment.value": "Отправить", + "ep_comments.comments_template.cancel.value": "Отменить", + "ep_comments.comments_template.reply.value": "Ответить", + "ep_comments.comments_template.reply.placeholder": "Ответить", + "ep_comments.comments_template.edit_comment.save": "сохранить", + "ep_comments.comments_template.edit_comment.cancel": "отменить", + "ep_comments.error.edit_unauth": "You cannot edit other users comments!", + "ep_comments.error.delete_unauth": "You cannot delete other users comments!" +} diff --git a/src/plugins/md_comments/locales/scn.json b/src/plugins/md_comments/locales/scn.json new file mode 100644 index 000000000..b0fc627bd --- /dev/null +++ b/src/plugins/md_comments/locales/scn.json @@ -0,0 +1,16 @@ +{ + "@metadata": { + "authors": [ + "Ajeje Brazorf" + ] + }, + "ep_comments.comment": "Cummentu", + "ep_comments.comments": "Cummenti", + "ep_comments.delete_comment.title": "Cancella stu cummento", + "ep_comments.edit_comment.title": "Cancia stu cummentu", + "ep_comments.show_comments": "Ammustra cummenti", + "ep_comments.comments_template.comment.value": "Cummentu", + "ep_comments.comments_template.cancel.value": "Annulla", + "ep_comments.comments_template.edit_comment.save": "sarva", + "ep_comments.comments_template.edit_comment.cancel": "annulla" +} diff --git a/src/plugins/md_comments/locales/sk.json b/src/plugins/md_comments/locales/sk.json new file mode 100644 index 000000000..e2879d32c --- /dev/null +++ b/src/plugins/md_comments/locales/sk.json @@ -0,0 +1,31 @@ +{ + "@metadata": { + "authors": [ + "Yardom78" + ] + }, + "ep_comments.comment": "Komentár", + "ep_comments.comments": "Komentáre", + "ep_comments.add_comment.title": "Pridať nový komentár", + "ep_comments.add_comment": "Pridať nový komentár", + "ep_comments.add_comment.hint": "Prosím vyberte text na komentár", + "ep_comments.delete_comment.title": "Vymazať tento komentár", + "ep_comments.edit_comment.title": "Upraviť tento komentár", + "ep_comments.show_comments": "Zobraziť komentáre", + "ep_comments.comments_template.suggested_change": "Navrhovaná zmena", + "ep_comments.comments_template.from": "Od", + "ep_comments.comments_template.accept_change.value": "Súhlasiť so zmenou", + "ep_comments.comments_template.revert_change.value": "Vrátiť zmenu späť", + "ep_comments.comments_template.suggested_change_from": "Navrhované zmeny od \"{{changeFrom}}\" do \"{{changeTo}}\"", + "ep_comments.comments_template.suggest_change_from": "Navrhnúť zmeny od \"{{changeFrom}}\" do", + "ep_comments.comments_template.to": "Komu", + "ep_comments.comments_template.include_suggestion": "Zahrnúť navrhovanú zmenu", + "ep_comments.comments_template.comment.value": "Komentár", + "ep_comments.comments_template.cancel.value": "Zrušiť", + "ep_comments.comments_template.reply.value": "Odpovedať", + "ep_comments.comments_template.reply.placeholder": "Odpovedať", + "ep_comments.comments_template.edit_comment.save": "uložiť", + "ep_comments.comments_template.edit_comment.cancel": "zrušiť", + "ep_comments.error.edit_unauth": "Nemôžete upravovať komentáre iných používateľov!", + "ep_comments.error.delete_unauth": "Nemôžete vymazať komentáre iných používateľov!" +} diff --git a/src/plugins/md_comments/locales/sv.json b/src/plugins/md_comments/locales/sv.json new file mode 100644 index 000000000..07ebac12e --- /dev/null +++ b/src/plugins/md_comments/locales/sv.json @@ -0,0 +1,31 @@ +{ + "@metadata": { + "authors": [ + "Sabelöga" + ] + }, + "ep_comments.comment": "Kommentar", + "ep_comments.comments": "Kommentarer", + "ep_comments.add_comment.title": "Lägg till ny kommentar till markering", + "ep_comments.add_comment": "Lägg till ny kommentar till markering", + "ep_comments.add_comment.hint": "Markera först den text du vill kommentera", + "ep_comments.delete_comment.title": "Radera den här kommentaren", + "ep_comments.edit_comment.title": "Redigera den här kommentaren", + "ep_comments.show_comments": "Visa kommentarer", + "ep_comments.comments_template.suggested_change": "Föreslagen ändring", + "ep_comments.comments_template.from": "Från", + "ep_comments.comments_template.accept_change.value": "Godkänn ändring", + "ep_comments.comments_template.revert_change.value": "Ångra ändring", + "ep_comments.comments_template.suggested_change_from": "Föreslagen ändring från \"{{changeFrom}}\" till \"{{changeTo}}\"", + "ep_comments.comments_template.suggest_change_from": "Föreslå ändring från \"{{changeFrom}}\" till", + "ep_comments.comments_template.to": "Till", + "ep_comments.comments_template.include_suggestion": "Bifoga ändringsförslag", + "ep_comments.comments_template.comment.value": "Kommentera", + "ep_comments.comments_template.cancel.value": "Avbryt", + "ep_comments.comments_template.reply.value": "Svara", + "ep_comments.comments_template.reply.placeholder": "Svar", + "ep_comments.comments_template.edit_comment.save": "spara", + "ep_comments.comments_template.edit_comment.cancel": "avbryt", + "ep_comments.error.edit_unauth": "Du kan inte redigera andra användares kommentarer!", + "ep_comments.error.delete_unauth": "Du kan inte radera andra användares kommentarer!" +} diff --git a/src/plugins/md_comments/locales/th.json b/src/plugins/md_comments/locales/th.json new file mode 100644 index 000000000..7b9ac0434 --- /dev/null +++ b/src/plugins/md_comments/locales/th.json @@ -0,0 +1,32 @@ +{ + "@metadata": { + "authors": [ + "Prame Tan", + "Thas Tayapongsak" + ] + }, + "ep_comments.comment": "ความคิดเห็น", + "ep_comments.comments": "ความเห็น", + "ep_comments.add_comment.title": "เพิ่มความเห็นสำหรับส่วนที่เลือก", + "ep_comments.add_comment": "เพิ่มความเห็นสำหรับส่วนที่เลือก", + "ep_comments.add_comment.hint": "กรุณาเลือกข้อความที่จะแสดงความเห็น", + "ep_comments.delete_comment.title": "ลบความเห็น", + "ep_comments.edit_comment.title": "แก้ไขความคิดเห็น", + "ep_comments.show_comments": "แสดงความเห็น", + "ep_comments.comments_template.suggested_change": "การเปลี่ยนแปลงที่เสนอ", + "ep_comments.comments_template.from": "จาก", + "ep_comments.comments_template.accept_change.value": "ยอมรับการเปลี่ยนแปลง", + "ep_comments.comments_template.revert_change.value": "ย้อนคืนการเปลี่ยนแปลง", + "ep_comments.comments_template.suggested_change_from": "เสนอให้เปลี่ยน \"{{changeFrom}}\" ไปเป็น \"{{changeTo}}\"", + "ep_comments.comments_template.suggest_change_from": "เสนอให้เปลี่ยนจาก \"{{changeFrom}}\" ไปเป็น", + "ep_comments.comments_template.to": "ถึง", + "ep_comments.comments_template.include_suggestion": "เพิ่มการเปลี่ยนแปลงที่เสนอ", + "ep_comments.comments_template.comment.value": "ความเห็น", + "ep_comments.comments_template.cancel.value": "ยกเลิก", + "ep_comments.comments_template.reply.value": "ตอบกลับ", + "ep_comments.comments_template.reply.placeholder": "ตอบกลับ", + "ep_comments.comments_template.edit_comment.save": "บันทึก", + "ep_comments.comments_template.edit_comment.cancel": "ยกเลิก", + "ep_comments.error.edit_unauth": "คุณไม่สามารถแก้ไขความเห็นของผู้ใช้อื่นได้", + "ep_comments.error.delete_unauth": "คุณไม่สามารถลบความเห็นของผู้ใช้อื่นได้" +} diff --git a/src/plugins/md_comments/locales/tr.json b/src/plugins/md_comments/locales/tr.json new file mode 100644 index 000000000..aba5079a7 --- /dev/null +++ b/src/plugins/md_comments/locales/tr.json @@ -0,0 +1,33 @@ +{ + "@metadata": { + "authors": [ + "Can", + "Erdemkose", + "Hedda" + ] + }, + "ep_comments.comment": "Yorum", + "ep_comments.comments": "Yorumlar", + "ep_comments.add_comment.title": "Seçime yeni yorum ekle", + "ep_comments.add_comment": "Seçime yeni yorum ekle", + "ep_comments.add_comment.hint": "Lütfen önce yorum yapılacak metni seçin", + "ep_comments.delete_comment.title": "Bu yorumu sil", + "ep_comments.edit_comment.title": "Bu yorumu düzenle", + "ep_comments.show_comments": "Yorumları göster", + "ep_comments.comments_template.suggested_change": "Önerilen Değişiklik", + "ep_comments.comments_template.from": "Gönderen", + "ep_comments.comments_template.accept_change.value": "Değişikliği Kabul Et", + "ep_comments.comments_template.revert_change.value": "Değişikliği Geri Al", + "ep_comments.comments_template.suggested_change_from": "\"{{changeFrom}}\"'dan/den, {{changeTo}}'ye/ya önerilen değişiklik", + "ep_comments.comments_template.suggest_change_from": "\"{{changeFrom}}\"'den/dan şuraya önerilen değişiklik", + "ep_comments.comments_template.to": "Alıcı", + "ep_comments.comments_template.include_suggestion": "Önerilen değişikliği dahil et", + "ep_comments.comments_template.comment.value": "Yorum", + "ep_comments.comments_template.cancel.value": "İptal", + "ep_comments.comments_template.reply.value": "Yanıtla", + "ep_comments.comments_template.reply.placeholder": "Yanıtla", + "ep_comments.comments_template.edit_comment.save": "kaydet", + "ep_comments.comments_template.edit_comment.cancel": "iptal", + "ep_comments.error.edit_unauth": "Diğer kullanıcıların yorumlarını düzenleyemezsiniz!", + "ep_comments.error.delete_unauth": "Diğer kullanıcıların yorumlarını silemezsiniz!" +} diff --git a/src/plugins/md_comments/locales/uk.json b/src/plugins/md_comments/locales/uk.json new file mode 100644 index 000000000..892b8c20f --- /dev/null +++ b/src/plugins/md_comments/locales/uk.json @@ -0,0 +1,31 @@ +{ + "@metadata": { + "authors": [ + "DDPAT" + ] + }, + "ep_comments.comment": "Коментар", + "ep_comments.comments": "Коментарі", + "ep_comments.add_comment.title": "Додати новий коментар до вибору", + "ep_comments.add_comment": "Додати новий коментар до вибору", + "ep_comments.add_comment.hint": "Будь ласка, спочатку виберіть текст для коментування", + "ep_comments.delete_comment.title": "Видалити цей коментар", + "ep_comments.edit_comment.title": "Редагувати цей коментар", + "ep_comments.show_comments": "Показати коментарі", + "ep_comments.comments_template.suggested_change": "Запропонована зміна", + "ep_comments.comments_template.from": "Від", + "ep_comments.comments_template.accept_change.value": "Прийняти зміну", + "ep_comments.comments_template.revert_change.value": "Скасувати зміни", + "ep_comments.comments_template.suggested_change_from": "Пропонована зміна з «{{changeFrom}}» на «{{changeTo}}»", + "ep_comments.comments_template.suggest_change_from": "Запропонувати зміну з «{{changeFrom}}» на", + "ep_comments.comments_template.to": "Кому:", + "ep_comments.comments_template.include_suggestion": "Включити запропоновану зміну", + "ep_comments.comments_template.comment.value": "Коментар", + "ep_comments.comments_template.cancel.value": "Скасувати", + "ep_comments.comments_template.reply.value": "Відповісти", + "ep_comments.comments_template.reply.placeholder": "Відповісти", + "ep_comments.comments_template.edit_comment.save": "зберегти", + "ep_comments.comments_template.edit_comment.cancel": "скасувати", + "ep_comments.error.edit_unauth": "Ви не можете редагувати коментарі інших користувачів!", + "ep_comments.error.delete_unauth": "Ви не можете видаляти коментарі інших користувачів!" +} diff --git a/src/plugins/md_comments/locales/xmf.json b/src/plugins/md_comments/locales/xmf.json new file mode 100644 index 000000000..585a9926a --- /dev/null +++ b/src/plugins/md_comments/locales/xmf.json @@ -0,0 +1,17 @@ +{ + "@metadata": { + "authors": [ + "Narazeni" + ] + }, + "ep_comments.delete_comment.title": "ათე კომენტარიშ ლასუა", + "ep_comments.show_comments": "კომენტარეფიშ ძირაფა", + "ep_comments.comments_template.accept_change.value": "თირუაშ მეღება", + "ep_comments.comments_template.comment.value": "კომენტარი", + "ep_comments.comments_template.cancel.value": "გოუქვაფა", + "ep_comments.comments_template.reply.value": "გამაშ მეჭარუა", + "ep_comments.comments_template.edit_comment.save": "ჩუალა", + "ep_comments.comments_template.edit_comment.cancel": "გოუქვაფა", + "ep_comments.error.edit_unauth": "თქვა ვეშეილებჷნა შხვა მახვარებუეფიშ კომენტარეფიშ რედაქტირაფა!", + "ep_comments.error.delete_unauth": "თქვა ვეშეილებჷნა შხვა მახვარებუეფიშ კომენტარეფიშ ლასუა!" +} diff --git a/src/plugins/md_comments/locales/zh-hans.json b/src/plugins/md_comments/locales/zh-hans.json new file mode 100644 index 000000000..a517037ae --- /dev/null +++ b/src/plugins/md_comments/locales/zh-hans.json @@ -0,0 +1,32 @@ +{ + "@metadata": { + "authors": [ + "TsuyaMarisa", + "列维劳德" + ] + }, + "ep_comments.comment": "评论", + "ep_comments.comments": "评论", + "ep_comments.add_comment.title": "在选项中添加新评论", + "ep_comments.add_comment": "添加新的评论", + "ep_comments.add_comment.hint": "请先选择文本用以评论", + "ep_comments.delete_comment.title": "删除此评论", + "ep_comments.edit_comment.title": "编辑此注释", + "ep_comments.show_comments": "显示评论", + "ep_comments.comments_template.suggested_change": "建议更改", + "ep_comments.comments_template.from": "来自", + "ep_comments.comments_template.accept_change.value": "接受更改", + "ep_comments.comments_template.revert_change.value": "最近更改", + "ep_comments.comments_template.suggested_change_from": "从“{{changeFrom}}”提出的变化到“{{changeTo}}”", + "ep_comments.comments_template.suggest_change_from": "建议从“{{changeFrom}}”变化", + "ep_comments.comments_template.to": "至", + "ep_comments.comments_template.include_suggestion": "包括建议的变化", + "ep_comments.comments_template.comment.value": "注释", + "ep_comments.comments_template.cancel.value": "取消", + "ep_comments.comments_template.reply.value": "回复", + "ep_comments.comments_template.reply.placeholder": "回复", + "ep_comments.comments_template.edit_comment.save": "保存", + "ep_comments.comments_template.edit_comment.cancel": "取消", + "ep_comments.error.edit_unauth": "您不能编辑其他用户的评论!", + "ep_comments.error.delete_unauth": "您不能删除其他用户的评论!" +} diff --git a/src/plugins/md_comments/locales/zh-hant.json b/src/plugins/md_comments/locales/zh-hant.json new file mode 100644 index 000000000..7f0f3b6f8 --- /dev/null +++ b/src/plugins/md_comments/locales/zh-hant.json @@ -0,0 +1,31 @@ +{ + "@metadata": { + "authors": [ + "Kly" + ] + }, + "ep_comments.comment": "意見", + "ep_comments.comments": "意見", + "ep_comments.add_comment.title": "在所選添加新的意見", + "ep_comments.add_comment": "在所選添加新的意見", + "ep_comments.add_comment.hint": "請先選擇意見內容的文字", + "ep_comments.delete_comment.title": "刪除此意見", + "ep_comments.edit_comment.title": "編輯此意見", + "ep_comments.show_comments": "顯示意見", + "ep_comments.comments_template.suggested_change": "建議更改", + "ep_comments.comments_template.from": "來自", + "ep_comments.comments_template.accept_change.value": "接受更改", + "ep_comments.comments_template.revert_change.value": "回復更改", + "ep_comments.comments_template.suggested_change_from": "將「{{changeFrom}}」改成「{{changeTo}}」的建議更改", + "ep_comments.comments_template.suggest_change_from": "建議更改,將「{{changeFrom}}」改成", + "ep_comments.comments_template.to": "至", + "ep_comments.comments_template.include_suggestion": "包含建議更改", + "ep_comments.comments_template.comment.value": "意見", + "ep_comments.comments_template.cancel.value": "取消", + "ep_comments.comments_template.reply.value": "回覆", + "ep_comments.comments_template.reply.placeholder": "回覆", + "ep_comments.comments_template.edit_comment.save": "儲存", + "ep_comments.comments_template.edit_comment.cancel": "取消", + "ep_comments.error.edit_unauth": "您不能編輯其他使用者的意見!", + "ep_comments.error.delete_unauth": "您不能刪除其他使用者的意見!" +} diff --git a/src/plugins/md_comments/package.json b/src/plugins/md_comments/package.json new file mode 100644 index 000000000..69e66ae92 --- /dev/null +++ b/src/plugins/md_comments/package.json @@ -0,0 +1,94 @@ +{ + "_from": "ep_comments", + "_id": "ep_comments@0.0.5", + "_inBundle": false, + "_integrity": "sha512-jp9U4cCkgSl9ryCnALYUH3fM80j7j3FQThpCcsmka7tjXBdpbXI9BMcIEio217OdII/MGqjUcDn08d3KbJlcqw==", + "_location": "/ep_comments", + "_phantomChildren": { + "boolbase": "1.0.0", + "inherits": "2.0.3", + "lodash.assignin": "4.2.0", + "lodash.bind": "4.2.1", + "lodash.defaults": "4.2.0", + "lodash.filter": "4.6.0", + "lodash.flatten": "4.4.0", + "lodash.foreach": "4.5.0", + "lodash.map": "4.6.0", + "lodash.merge": "4.6.2", + "lodash.pick": "4.4.0", + "lodash.reduce": "4.6.0", + "lodash.reject": "4.6.0", + "lodash.some": "4.6.0", + "util-deprecate": "1.0.2" + }, + "_requested": { + "type": "tag", + "registry": true, + "raw": "ep_comments", + "name": "ep_comments", + "escapedName": "ep_comments", + "rawSpec": "", + "saveSpec": null, + "fetchSpec": "latest" + }, + "_requiredBy": [ + "#USER" + ], + "_resolved": "https://registry.npmjs.org/ep_comments/-/ep_comments-0.0.5.tgz", + "_shasum": "79245f5ccc5e196d98c4ce160f9494cb8a7a6434", + "_spec": "ep_comments", + "_where": "/Users/dev/Github/Edumatica/MuDocs/src", + "author": { + "name": "Akhil Naidu", + "email": "kaparapu.akhilnaidu@gmail.com" + }, + "bugs": { + "url": "https://github.com/akhil-naidu/ep_comments/issues" + }, + "bundleDependencies": false, + "dependencies": { + "cheerio": "^0.22.0", + "formidable": "^1.2.2", + "underscore": "^1.13.1" + }, + "deprecated": false, + "description": "Adds comments on sidebar and link it to the text", + "devDependencies": { + "eslint": "^7.32.0", + "eslint-config-etherpad": "^2.0.2", + "eslint-plugin-cypress": "^2.12.1", + "eslint-plugin-eslint-comments": "^3.2.0", + "eslint-plugin-mocha": "^9.0.0", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prefer-arrow": "^1.2.3", + "eslint-plugin-promise": "^5.1.1", + "eslint-plugin-you-dont-need-lodash-underscore": "^6.12.0", + "socket.io-client": "^2.3.0", + "superagent": "^6.1.0" + }, + "engines": { + "node": ">=12.13.0" + }, + "eslintConfig": { + "root": true, + "extends": "etherpad/plugin", + "ignorePatterns": [ + "/static/js/jquery.tmpl.min.js", + "/static/js/moment-with-locales.min.js" + ] + }, + "homepage": "https://github.com/akhil-naidu/ep_comments#readme", + "name": "ep_comments", + "peerDependencies": { + "ep_etherpad-lite": ">=0.0.2" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/akhil-naidu/ep_comments.git" + }, + "scripts": { + "lint": "eslint .", + "lint:fix": "eslint --fix ." + }, + "version": "0.0.5" +} diff --git a/src/plugins/md_comments/static/css/comment.css b/src/plugins/md_comments/static/css/comment.css new file mode 100644 index 000000000..6cd37a3a4 --- /dev/null +++ b/src/plugins/md_comments/static/css/comment.css @@ -0,0 +1,210 @@ +/* Text commented inside editor */ +#innerdocbody .ace-line .comment { + background-color: #fff382; + color: #222; +} +#innerdocbody .ace-line .comment[data-open="true"]{ + color: orange !important; +} + + +/* Comment right side container */ +#comments { + width: 250px; + order: 3; + position: relative; +} +#comments:not(.active) { + display: none; +} +@media (max-width: 900px) { + #commentIcons, #comments { + display: none !important; + } +} + +.sidebar-comment { + position: absolute; + width: 100%; +} + +/* WITH ICONS */ +#comments.with-icons { + display: none; +} + +/* NEW COMMENT FORM (included both in popup and reply) */ +.new-comment .comment-content { + width: 100%; +} +.comment-reply .new-comment:not(.editing) .form-more { + display: none; +} +input.error, textarea.error { + border-color: red; +} + +/* COMMENT GENERAL STYLE */ +.comment-author-name { + font-weight: bold; +} +.comment-created-at { + font-size: .8em; + opacity: .7; + margin-left: 5px; +} +.comment-actions-wrapper { + float: right; +} + +/* COMMENT COMPACTED (Visible on right side) */ +.sidebar-comment:not(.full-display) .full-display-content { + display: none; +} +.compact-display-content { + text-overflow: ellipsis; + white-space: nowrap; + overflow: hidden; + padding: 0 10px; + background-color: #eeeeed; +} + +/* COMMENT FULL (when mouse hover) */ +.sidebar-comment.full-display { + z-index: 2; +} +.sidebar-comment.full-display .full-display-content { + display: block; + margin-top: -10px; +} +.sidebar-comment.full-display .compact-display-content { + display: none; +} +.full-display-content { + background-color: white; + border-radius: 5px; + overflow: hidden; + box-shadow: 0 2px 4px #ddd; + z-index: 99; +} +.full-display-content .comment-title-wrapper, +.full-display-content .comment-reply { + padding: 10px; +} +.full-display-content .comment-title-wrapper .comment-text { + display: block; + margin-top: 10px; + white-space: normal; +} +.full-display-content .comment-title-wrapper .comment-text.default-text { + display: none; +} + +/* SUGGESTION */ +.suggestion, .reply-suggestion { + display: none; +} +.suggestion-display { + margin-top: 5px; + white-space: normal; +} +.suggestion-display .from-label, +.suggestion-display .to-label { + margin-right: 2px; +} +.suggestion-display .from-value, +.suggestion-display .to-value { + opacity: .8; + font-style: italic; +} +.suggestion-display .from-value:after, .suggestion-display .from-value:before, +.suggestion-display .to-value:after, .suggestion-display .to-value:before { + content: '"'; +} +.suggestion-create .from-label, +.suggestion-create .to-label { + display: block; + font-weight: bold; + margin: 5px 0; +} +.approve-suggestion-btn, .revert-suggestion-btn { + display: block; + margin-bottom: 10px; +} +.suggestion-create textarea.to-value { + width: 100%; +} +.comment-container .revert-suggestion-btn { display: none; } +.comment-container.change-accepted .revert-suggestion-btn { display: block; } +.comment-container.change-accepted .approve-suggestion-btn { display: none; } + +.comment-container.change-accepted .comment-replies-container .revert-suggestion-btn { display: none; } +.comment-container.change-accepted .comment-replies-container .approve-suggestion-btn { display: block; } +.comment-container.change-accepted .comment-replies-container .comment-container.change-accepted .revert-suggestion-btn { display: block; } +.comment-container.change-accepted .comment-replies-container .comment-container.change-accepted .approve-suggestion-btn { display: none; } +/* REPLIES */ +.comment-reply { + background-color: #f9f9f9; + border-top: 1px solid #eee; +} +/* One previous reply */ +.sidebar-comment-reply { + margin-bottom: 10px; +} +.sidebar-comment-reply .comment-text { + display: inline; + margin-top: 5px; + white-space: normal; +} +.sidebar-comment-reply .comment-edit { + display: none; + margin-left: 5px; + font-size: 1em; + opacity: .7; + transition: opacity .2s; +} +.sidebar-comment-reply:hover:not(.editing) .comment-edit { + display: inline; +} +.sidebar-comment-reply:hover:not(.editing) .comment-edit:hover { + opacity: 1; +} +.reply-comment-suggest, .comment-suggest { + margin-top: 10px; +} + +/* EDITING COMMENT */ +.comment-edit-text { + width: 100%; +} +.comment-edit-form + .comment-text { + display: none !important; +} + +/* MODAL FOR MOBILES */ +.comment-modal { + bottom: auto !important; + right: auto !important; +} +.comment-modal-comment { + padding: 0; +} +.comment-modal-comment .sidebar-comment { + position: relative; + top: 0; +} +.comment-modal-comment .compact-display-content { + display: none; +} +.comment-modal-comment .full-display-content { + display: block !important; + margin: 0; +} +.comment-modal-comment .comment-content { + margin-top: 0 !important; +} + +/* OTHER */ +.hidden { + display: none; +} \ No newline at end of file diff --git a/src/plugins/md_comments/static/css/commentIcon.css b/src/plugins/md_comments/static/css/commentIcon.css new file mode 100644 index 000000000..a7265e441 --- /dev/null +++ b/src/plugins/md_comments/static/css/commentIcon.css @@ -0,0 +1,40 @@ +#commentIcons { + display: block; + z-index: 1; + margin-left: 15px; + width: 50px; + position: relative; +} +#commentIcons:not(.active) { + display: none; +} + +.comment-icon-line { + position: absolute; + margin-top: 2px; +} +.comment-icon { + background-repeat: no-repeat; + display: inline-block; + height: 16px; + vertical-align: middle; + width: 16px; + margin-right: 5px; + cursor: pointer; +} +.comment-icon:before { + font-family: "fontawesome-etherpad"; + content: "\E850"; + color:#666; + font-size:14px; + padding-top:2px; + line-height:17px; +} + +.comment-icon.with-reply:before { + content: "\E82D"; +} + +.comment-icon.active:before { + color:orange; +} \ No newline at end of file diff --git a/src/plugins/md_comments/static/css/main.css b/src/plugins/md_comments/static/css/main.css new file mode 100644 index 000000000..99f80d3ee --- /dev/null +++ b/src/plugins/md_comments/static/css/main.css @@ -0,0 +1,15 @@ +.commenticon { + background-repeat: no-repeat; + display: inline-block; + height: 16px; + vertical-align: middle; + width: 16px; +} +.commenticon:before{ + font-family: "fontawesome-etherpad"; + content: "\e828"; + color:#666; + font-size:14px; + padding-top:2px; + line-height:17px; +} diff --git a/src/plugins/md_comments/static/js/commentBoxes.js b/src/plugins/md_comments/static/js/commentBoxes.js new file mode 100644 index 000000000..695f92b52 --- /dev/null +++ b/src/plugins/md_comments/static/js/commentBoxes.js @@ -0,0 +1,123 @@ +'use strict'; + +// Easier access to outter pad +let padOuter; +const getPadOuter = () => { + padOuter = padOuter || $('iframe[name="ace_outer"]').contents(); + return padOuter; +}; + +const getCommentsContainer = () => getPadOuter().find('#comments'); + +/* ***** Public methods: ***** */ + +const hideComment = (commentId, hideCommentTitle) => { + const commentElm = getCommentsContainer().find(`#${commentId}`); + commentElm.removeClass('full-display'); + + // hide even the comment title + if (hideCommentTitle) commentElm.hide(); + + const inner = $('iframe[name="ace_outer"]').contents().find('iframe[name="ace_inner"]'); + inner.contents().find('head .comment-style').remove(); + + getPadOuter().find('.comment-modal').removeClass('popup-show'); +}; + +const hideAllComments = () => { + getCommentsContainer().find('.sidebar-comment').removeClass('full-display'); + getPadOuter().find('.comment-modal').removeClass('popup-show'); +}; + +const highlightComment = (commentId, e, editorComment) => { + const container = getCommentsContainer(); + const commentElm = container.find(`#${commentId}`); + const inner = $('iframe[name="ace_outer"]').contents().find('iframe[name="ace_inner"]'); + + if (container.is(':visible')) { + // hide all other comments + container.find('.sidebar-comment').each(function () { + inner.contents().find('head .comment-style').remove(); + $(this).removeClass('full-display'); + }); + + // Then highlight new comment + commentElm.addClass('full-display'); + // now if we apply a class such as mouseover to the editor it will go shitty + // so what we need to do is add CSS for the specific ID to the document... + // It's fucked up but that's how we do it.. + inner.contents().find('head').append( + ``); + } else { + // make a full copy of the html, including listeners + const commentElmCloned = commentElm.clone(true, true); + + // before of appending clear the css (like top positionning) + commentElmCloned.attr('style', ''); + // fix checkbox, because as we are duplicating the sidebar-comment, we lose unique input names + commentElmCloned.find('.label-suggestion-checkbox').click(function () { + $(this).siblings('input[type="checkbox"]').click(); + }); + + // hovering comment view + getPadOuter().find('.comment-modal-comment').html('').append(commentElmCloned); + const padInner = getPadOuter().find('iframe[name="ace_inner"]'); + // get modal position + const containerWidth = getPadOuter().find('#outerdocbody').outerWidth(true); + const modalWitdh = getPadOuter().find('.comment-modal').outerWidth(true); + let targetLeft = e.clientX; + let targetTop = $(e.target).offset().top; + if (editorComment) { + targetLeft += padInner.offset().left; + targetTop += parseInt(padInner.css('padding-top').split('px')[0]); + targetTop += parseInt(padOuter.find('#outerdocbody').css('padding-top').split('px')[0]); + } else { + // mean we are clicking from a comment Icon + targetLeft = $(e.target).offset().left - 20; + } + + // if positioning modal on target left will make part of the modal to be + // out of screen, we place it closer to the middle of the screen + if (targetLeft + modalWitdh > containerWidth) { + targetLeft = containerWidth - modalWitdh - 25; + } + const editorCommentHeight = editorComment ? editorComment.outerHeight(true) : 30; + getPadOuter().find('.comment-modal').addClass('popup-show').css({ + left: `${targetLeft}px`, + top: `${targetTop + editorCommentHeight}px`, + }); + } +}; + +// Adjust position of the comment detail on the container, to be on the same +// height of the pad text associated to the comment, and return the affected element +const adjustTopOf = (commentId, baseTop) => { + const commentElement = getPadOuter().find(`#${commentId}`); + commentElement.css('top', `${baseTop}px`); + + return commentElement; +}; + +// Indicates if comment is on the expected position (baseTop-5) +const isOnTop = (commentId, baseTop) => { + const commentElement = getPadOuter().find(`#${commentId}`); + const expectedTop = `${baseTop}px`; + return commentElement.css('top') === expectedTop; +}; + +// Indicates if event was on one of the elements that does not close comment +const shouldNotCloseComment = (e) => { + // a comment box + if ($(e.target).closest('.sidebar-comment').length || + $(e.target).closest('.comment-modal').length) { // the comment modal + return true; + } + return false; +}; + +exports.hideComment = hideComment; +exports.hideAllComments = hideAllComments; +exports.highlightComment = highlightComment; +exports.adjustTopOf = adjustTopOf; +exports.isOnTop = isOnTop; +exports.shouldNotCloseComment = shouldNotCloseComment; diff --git a/src/plugins/md_comments/static/js/commentIcons.js b/src/plugins/md_comments/static/js/commentIcons.js new file mode 100644 index 000000000..47aace7f8 --- /dev/null +++ b/src/plugins/md_comments/static/js/commentIcons.js @@ -0,0 +1,222 @@ +'use strict'; + +const commentBoxes = require('ep_comments/static/js/commentBoxes'); + +// Indicates if MuDoc is configured to display icons +const displayIcons = () => clientVars.displayCommentAsIcon; + +// Easier access to outer pad +let padOuter; +const getPadOuter = () => { + padOuter = padOuter || $('iframe[name="ace_outer"]').contents(); + return padOuter; +}; + +// Easier access to inner pad +let padInner; +const getPadInner = () => { + padInner = padInner || getPadOuter().find('iframe[name="ace_inner"]').contents(); + return padInner; +}; + +const getOrCreateIconsContainerAt = (top) => { + const iconContainer = getPadOuter().find('#commentIcons'); + const iconClass = `icon-at-${top}`; + + // is this the 1st comment on that line? + let iconsAtLine = iconContainer.find(`.${iconClass}`); + const isFirstIconAtLine = iconsAtLine.length === 0; + + // create container for icons at target line, if it does not exist yet + if (isFirstIconAtLine) { + iconContainer.append(`

    `); + iconsAtLine = iconContainer.find(`.${iconClass}`); + iconsAtLine.css('top', `${top}px`); + } + + return iconsAtLine; +}; + +const targetCommentIdOf = (e) => e.currentTarget.getAttribute('data-commentid'); + +const highlightTargetTextOf = (commentId) => { + getPadInner().find('head').append( + ``); +}; + +const removeHighlightTargetText = () => { + getPadInner().find('head .comment-style').remove(); +}; + +const toggleActiveCommentIcon = (target) => { + target.toggleClass('active').toggleClass('inactive'); +}; + +const addListenersToCommentIcons = () => { + getPadOuter().find('#commentIcons').on('mouseover', '.comment-icon', (e) => { + removeHighlightTargetText(); + const commentId = targetCommentIdOf(e); + highlightTargetTextOf(commentId); + }).on('mouseout', '.comment-icon', (e) => { + removeHighlightTargetText(); + }).on('click', '.comment-icon.active', function (e) { + toggleActiveCommentIcon($(this)); + + const commentId = targetCommentIdOf(e); + commentBoxes.hideComment(commentId, true); + }).on('click', '.comment-icon.inactive', function (e) { + // deactivate/hide other comment boxes that are opened, so we have only + // one comment box opened at a time + commentBoxes.hideAllComments(); + const allActiveIcons = getPadOuter().find('#commentIcons').find('.comment-icon.active'); + toggleActiveCommentIcon(allActiveIcons); + + // activate/show only target comment + toggleActiveCommentIcon($(this)); + const commentId = targetCommentIdOf(e); + commentBoxes.highlightComment(commentId, e); + }); +}; + +// Listen to clicks on the page to be able to close comment when clicking +// outside of it +const addListenersToCloseOpenedComment = () => { + // we need to add listeners to the different iframes of the page + $(document).on('touchstart click', (e) => { + closeOpenedCommentIfNotOnSelectedElements(e); + }); + getPadOuter().find('html').on('touchstart click', (e) => { + closeOpenedCommentIfNotOnSelectedElements(e); + }); + getPadInner().find('html').on('touchstart click', (e) => { + closeOpenedCommentIfNotOnSelectedElements(e); + }); +}; + +// Close comment if event target was outside of comment or on a comment icon +const closeOpenedCommentIfNotOnSelectedElements = (e) => { + // Don't do anything if clicked on the following elements: + // any of the comment icons + if (shouldNotCloseComment(e) || commentBoxes.shouldNotCloseComment(e)) { + // a comment box or the comment modal + return; + } + + // All clear, can close the comment + const openedComment = findOpenedComment(); + if (openedComment) { + toggleActiveCommentIcon($(openedComment)); + + const commentId = openedComment.getAttribute('data-commentid'); + commentBoxes.hideComment(commentId, true); + } +}; + +// Search on the page for an opened comment +const findOpenedComment = () => getPadOuter().find('#commentIcons .comment-icon.active').get(0); + +/* ***** Public methods: ***** */ + +// Create container to hold comment icons +const insertContainer = () => { + // we're only doing something if icons will be displayed at all + if (!displayIcons()) return; + + getPadOuter().find('#sidediv').after('
    '); + getPadOuter().find('#comments').addClass('with-icons'); + addListenersToCommentIcons(); + addListenersToCloseOpenedComment(); +}; + +// Create a new comment icon +const addIcon = (commentId, comment) => { + // we're only doing something if icons will be displayed at all + if (!displayIcons()) return; + + const inlineComment = getPadInner().find(`.comment.${commentId}`); + const top = inlineComment.get(0).offsetTop; + const iconsAtLine = getOrCreateIconsContainerAt(top); + const icon = $('#commentIconTemplate').tmpl(comment); + + icon.appendTo(iconsAtLine); +}; + +// Hide comment icons from container +const hideIcons = () => { + // we're only doing something if icons will be displayed at all + if (!displayIcons()) return; + + getPadOuter().find('#commentIcons').children().children().each(function () { + $(this).hide(); + }); +}; + +// Adjust position of the comment icon on the container, to be on the same +// height of the pad text associated to the comment, and return the affected icon +const adjustTopOf = (commentId, baseTop) => { + // we're only doing something if icons will be displayed at all + if (!displayIcons()) return; + + const icon = getPadOuter().find(`#icon-${commentId}`); + const targetTop = baseTop; + const iconsAtLine = getOrCreateIconsContainerAt(targetTop); + + // move icon from one line to the other + if (iconsAtLine !== icon.parent()) icon.appendTo(iconsAtLine); + + icon.show(); + + return icon; +}; + +// Indicate if comment detail currently opened was shown by a click on +// comment icon. +const isCommentOpenedByClickOnIcon = () => { + // we're only doing something if icons will be displayed at all + if (!displayIcons()) return false; + + const iconClicked = getPadOuter().find('#commentIcons').find('.comment-icon.active'); + const commentOpenedByClickOnIcon = iconClicked.length !== 0; + + return commentOpenedByClickOnIcon; +}; + +// Mark comment as a comment-with-reply, so it can be displayed with a +// different icon +const commentHasReply = (commentId) => { + // we're only doing something if icons will be displayed at all + if (!displayIcons()) return; + + // change comment icon + const iconForComment = getPadOuter().find('#commentIcons').find(`#icon-${commentId}`); + iconForComment.addClass('with-reply'); +}; + +// Indicate if sidebar comment should be shown, checking if it had the characteristics +// of a comment that was being displayed on the screen +const shouldShow = (sidebarComent) => { + let shouldShowComment = false; + + if (!displayIcons()) { + // if icons are not being displayed, we always show comments + shouldShowComment = true; + } else if (sidebarComent.hasClass('mouseover')) { + // if icons are being displayed, we only show comments clicked by user + shouldShowComment = true; + } + + return shouldShowComment; +}; + +// Indicates if event was on one of the elements that does not close comment (any of the comment +// icons) +const shouldNotCloseComment = (e) => $(e.target).closest('.comment-icon').length !== 0; + +exports.insertContainer = insertContainer; +exports.addIcon = addIcon; +exports.hideIcons = hideIcons; +exports.adjustTopOf = adjustTopOf; +exports.isCommentOpenedByClickOnIcon = isCommentOpenedByClickOnIcon; +exports.commentHasReply = commentHasReply; +exports.shouldShow = shouldShow; +exports.shouldNotCloseComment = shouldNotCloseComment; diff --git a/src/plugins/md_comments/static/js/commentL10n.js b/src/plugins/md_comments/static/js/commentL10n.js new file mode 100644 index 000000000..f48630255 --- /dev/null +++ b/src/plugins/md_comments/static/js/commentL10n.js @@ -0,0 +1,9 @@ +'use strict'; + +/* ***** Public methods: ***** */ + +const localize = (element) => { + html10n.translateElement(html10n.translations, element.get(0)); +}; + +exports.localize = localize; diff --git a/src/plugins/md_comments/static/js/copyPasteEvents.js b/src/plugins/md_comments/static/js/copyPasteEvents.js new file mode 100644 index 000000000..4b941e195 --- /dev/null +++ b/src/plugins/md_comments/static/js/copyPasteEvents.js @@ -0,0 +1,334 @@ +'use strict'; + +const _ = require('underscore'); +const randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; +const shared = require('./shared'); + +exports.addTextOnClipboard = (e, ace, padInner, removeSelection, comments, replies) => { + let commentIdOnFirstPositionSelected; + let hasCommentOnSelection; + ace.callWithAce((ace) => { + commentIdOnFirstPositionSelected = ace.ace_getCommentIdOnFirstPositionSelected(); + hasCommentOnSelection = ace.ace_hasCommentOnSelection(); + }); + + if (hasCommentOnSelection) { + let commentsData; + const range = padInner.contents()[0].getSelection().getRangeAt(0); + const rawHtml = createHiddenDiv(range); + let html = rawHtml; + const onlyTextIsSelected = selectionHasOnlyText(rawHtml); + + // when the range selection is fully inside a tag, 'rawHtml' will have no HTML tag, so we have + // to build it. Ex: if we have 'abcdefgh" and user selects 'de', the value + // of 'rawHtml' will be 'de', not 'de'. As it is not possible to have two comments in the + // same text commentIdOnFirstPositionSelected is the commentId in this partial selection + if (onlyTextIsSelected) { + const textSelected = rawHtml[0].textContent; + html = buildHtmlToCopyWhenSelectionHasOnlyText( + textSelected, range, commentIdOnFirstPositionSelected); + } + const commentIds = getCommentIds(html); + commentsData = buildCommentsData(html, comments); + const htmlToCopy = replaceCommentIdsWithFakeIds(commentsData, html); + commentsData = JSON.stringify(commentsData); + let replyData = getReplyData(replies, commentIds); + replyData = JSON.stringify(replyData); + e.originalEvent.clipboardData.setData('text/objectReply', replyData); + e.originalEvent.clipboardData.setData('text/objectComment', commentsData); + // here we override the default copy behavior + e.originalEvent.clipboardData.setData('text/html', htmlToCopy); + e.preventDefault(); + + // if it is a cut event we have to remove the selection + if (removeSelection) { + padInner.contents()[0].execCommand('delete'); + } + } +}; + +const getReplyData = (replies, commentIds) => { + let replyData = {}; + _.each(commentIds, (commentId) => { + replyData = _.extend(getRepliesFromCommentId(replies, commentId), replyData); + }); + return replyData; +}; + +const getRepliesFromCommentId = (replies, commentId) => { + const repliesFromCommentID = {}; + _.each(replies, (reply, replyId) => { + if (reply.commentId === commentId) { + repliesFromCommentID[replyId] = reply; + } + }); + return repliesFromCommentID; +}; + +const buildCommentIdToFakeIdMap = (commentsData) => { + const commentIdToFakeId = {}; + _.each(commentsData, (comment, fakeCommentId) => { + const commentId = comment.data.originalCommentId; + commentIdToFakeId[commentId] = fakeCommentId; + }); + return commentIdToFakeId; +}; + +const replaceCommentIdsWithFakeIds = (commentsData, html) => { + const commentIdToFakeId = buildCommentIdToFakeIdMap(commentsData); + _.each(commentIdToFakeId, (fakeCommentId, commentId) => { + $(html).find(`.${commentId}`).removeClass(commentId).addClass(fakeCommentId); + }); + const htmlWithFakeCommentIds = getHtml(html); + return htmlWithFakeCommentIds; +}; + +const buildCommentsData = (html, comments) => { + const commentsData = {}; + const originalCommentIds = getCommentIds(html); + _.each(originalCommentIds, (originalCommentId) => { + const fakeCommentId = generateFakeCommentId(); + const comment = comments[originalCommentId]; + comment.data.originalCommentId = originalCommentId; + commentsData[fakeCommentId] = comment; + }); + return commentsData; +}; + +const generateFakeCommentId = () => { + const commentId = `fakecomment-${randomString(16)}`; + return commentId; +}; + +const getCommentIds = (html) => { + const allSpans = $(html).find('span'); + const commentIds = []; + _.each(allSpans, (span) => { + const cls = $(span).attr('class'); + const classCommentId = /(?:^| )(c-[A-Za-z0-9]*)/.exec(cls); + const commentId = (classCommentId) ? classCommentId[1] : false; + if (commentId) { + commentIds.push(commentId); + } + }); + const onlyUnique = (value, index, self) => self.indexOf(value) === index; + const uniqueCommentIds = commentIds.filter(onlyUnique); + return uniqueCommentIds; +}; + +const createHiddenDiv = (range) => { + const content = range.cloneContents(); + const div = document.createElement('div'); + const hiddenDiv = $(div).html(content); + return hiddenDiv; +}; + +const getHtml = (hiddenDiv) => $(hiddenDiv).html(); + +const selectionHasOnlyText = (rawHtml) => { + const html = getHtml(rawHtml); + const htmlDecoded = htmlDecode(html); + const text = $(rawHtml).text(); + return htmlDecoded === text; +}; + +const buildHtmlToCopyWhenSelectionHasOnlyText = (text, range, commentId) => { + const htmlWithSpans = buildHtmlWithTwoSpanTags(text, commentId); + const html = buildHtmlWithFormattingTagsOfSelection(htmlWithSpans, range); + + const htmlToCopy = $.parseHTML(`
    ${html}
    `); + return htmlToCopy; +}; + +const buildHtmlWithFormattingTagsOfSelection = (html, range) => { + const htmlOfParentNode = range.commonAncestorContainer.parentNode; + const tags = getTagsInSelection(htmlOfParentNode); + + // this case happens when we got a selection with one or more styling (bold, italic, underline, + // strikethrough) applied in all selection in the same range. For example, + // text + if (tags) { + html = buildOpenTags(tags) + html + buildCloseTags(tags); + } + + return html; +}; + +// FIXME - Allow to copy a comment when user copies only one char +// This is a hack to preserve the comment classes when user pastes a comment. When user pastes a +// span like this thing, chrome removes the classes and keeps +// only the style of the class. With comments chrome keeps the background-color. To avoid this we +// create two spans. The first one, thi has the text until the +// last but one character and second one with the last character g. +// MuDoc does a good job joining the two spans into one after the paste is triggered. +const buildHtmlWithTwoSpanTags = (text, commentId) => { + // text until before last char + const firstSpan = `${text.slice(0, -1)}`; + const secondSpan = `${text.slice(-1)}`; // last char + + return firstSpan + secondSpan; +}; + +const buildOpenTags = (tags) => { + let openTags = ''; + tags.forEach((tag) => { + openTags += `<${tag}>`; + }); + return openTags; +}; + +const buildCloseTags = (tags) => { + let closeTags = ''; + for (const tag of tags.slice().reverse()) { + closeTags += ``; + } + return closeTags; +}; + +const getTagsInSelection = (htmlObject) => { + const tags = []; + let tag; + while ($(htmlObject)[0].localName !== 'span') { + const html = $(htmlObject).prop('outerHTML'); + const stylingTagRegex = /<(b|i|u|s)>/.exec(html); + tag = stylingTagRegex ? stylingTagRegex[1] : ''; + tags.push(tag); + htmlObject = $(htmlObject).parent(); + } + return tags; +}; + +exports.saveCommentsAndReplies = (e) => { + let comments = e.originalEvent.clipboardData.getData('text/objectComment'); + let replies = e.originalEvent.clipboardData.getData('text/objectReply'); + if (comments && replies) { + comments = JSON.parse(comments); + replies = JSON.parse(replies); + saveComments(comments); + saveReplies(replies); + } +}; + +const saveComments = (comments) => { + const commentsToSave = {}; + const padId = clientVars.padId; + + const mapOriginalCommentsId = pad.plugins.ep_comments.mapOriginalCommentsId; + const mapFakeComments = pad.plugins.ep_comments.mapFakeComments; + + _.each(comments, (comment, fakeCommentId) => { + const newCommentId = shared.generateCommentId(); + mapFakeComments[fakeCommentId] = newCommentId; + const originalCommentId = comment.data.originalCommentId; + mapOriginalCommentsId[originalCommentId] = newCommentId; + commentsToSave[newCommentId] = comment; + }); + pad.plugins.ep_comments.saveCommentWithoutSelection(padId, commentsToSave); +}; + +const saveReplies = (replies) => { + const repliesToSave = {}; + const padId = clientVars.padId; + const mapOriginalCommentsId = pad.plugins.ep_comments.mapOriginalCommentsId; + _.each(replies, (reply, replyId) => { + const originalCommentId = reply.commentId; + // as the comment copied has got a new commentId, we set this id in the reply as well + reply.commentId = mapOriginalCommentsId[originalCommentId]; + repliesToSave[replyId] = reply; + }); + pad.plugins.ep_comments.saveCommentReplies(padId, repliesToSave); +}; + +// copied from https://css-tricks.com/snippets/javascript/unescape-html-in-js/ +const htmlDecode = (input) => { + const e = document.createElement('div'); + e.innerHTML = input; + return e.childNodes.length === 0 ? '' : e.childNodes[0].nodeValue; +}; + +// here we find the comment id on a position [line, column]. This function is used to get the +// comment id of one line when there is ONLY text selected. E.g In the line with comment, something, and user copies the text 'omethin'. The span tags are not +// copied only the text. So as the comment is applied on the selection we get the commentId using +// the first position selected of the line. +// P.S: It's not possible to have two or more comments when there is only text selected, because for +// each comment created it's generated a and to copy only the text it MUST NOT HAVE any tag +// on the selection +exports.getCommentIdOnFirstPositionSelected = function () { + const attributeManager = this.documentAttributeManager; + const rep = this.rep; + const commentId = _.object( + attributeManager.getAttributesOnPosition(rep.selStart[0], rep.selStart[1])).comment; + return commentId; +}; + +exports.hasCommentOnSelection = function () { + let hasComment; + const attributeManager = this.documentAttributeManager; + const rep = this.rep; + const selFirstLine = rep.selStart[0]; + const firstColumn = rep.selStart[1]; + const lastColumn = rep.selEnd[1]; + const selLastLine = rep.selEnd[0]; + const selectionOfMultipleLine = hasMultipleLineSelected(selFirstLine, selLastLine); + + if (selectionOfMultipleLine) { + hasComment = hasCommentOnMultipleLineSel(selFirstLine, selLastLine, rep, attributeManager); + } else { + hasComment = hasCommentOnLine(selFirstLine, firstColumn, lastColumn, attributeManager); + } + return hasComment; +}; + +const hasCommentOnMultipleLineSel = (selFirstLine, selLastLine, rep, attributeManager) => { + let foundLineWithComment = false; + for (let line = selFirstLine; line <= selLastLine && !foundLineWithComment; line++) { + const firstColumn = getFirstColumnOfSelection(line, rep, selFirstLine); + const lastColumn = getLastColumnOfSelection(line, rep, selLastLine); + const hasComment = hasCommentOnLine(line, firstColumn, lastColumn, attributeManager); + if (hasComment) { + foundLineWithComment = true; + } + } + return foundLineWithComment; +}; + +const getFirstColumnOfSelection = + (line, rep, firstLineOfSelection) => line !== firstLineOfSelection ? 0 : rep.selStart[1]; + +const getLastColumnOfSelection = (line, rep, lastLineOfSelection) => { + let lastColumnOfSelection; + if (line !== lastLineOfSelection) { + lastColumnOfSelection = getLength(line, rep); // length of line + } else { + lastColumnOfSelection = rep.selEnd[1] - 1; // position of last character selected + } + return lastColumnOfSelection; +}; + +const hasCommentOnLine = (lineNumber, firstColumn, lastColumn, attributeManager) => { + let foundCommentOnLine = false; + for (let column = firstColumn; column <= lastColumn && !foundCommentOnLine; column++) { + const commentId = + _.object(attributeManager.getAttributesOnPosition(lineNumber, column)).comment; + if (commentId !== undefined) { + foundCommentOnLine = true; + } + } + return foundCommentOnLine; +}; + +const hasMultipleLineSelected = + (firstLineOfSelection, lastLineOfSelection) => firstLineOfSelection !== lastLineOfSelection; + +const getLength = (line, rep) => { + const nextLine = line + 1; + const startLineOffset = rep.lines.offsetOfIndex(line); + const endLineOffset = rep.lines.offsetOfIndex(nextLine); + + // lineLength without \n + const lineLength = endLineOffset - startLineOffset - 1; + + return lineLength; +}; diff --git a/src/plugins/md_comments/static/js/index.js b/src/plugins/md_comments/static/js/index.js new file mode 100644 index 000000000..5e9aa4bee --- /dev/null +++ b/src/plugins/md_comments/static/js/index.js @@ -0,0 +1,1404 @@ +'use strict'; + +/* TODO: +- lable reply textarea +- Make the chekbox appear above the suggested changes even when activated +*/ + + +const _ = require('underscore'); +const browser = require('ep_etherpad-lite/static/js/browser'); +const commentBoxes = require('ep_comments/static/js/commentBoxes'); +const commentIcons = require('ep_comments/static/js/commentIcons'); +const commentL10n = require('ep_comments/static/js/commentL10n'); +const events = require('ep_comments/static/js/copyPasteEvents'); +const moment = require('ep_comments/static/js/moment-with-locales.min'); +const newComment = require('ep_comments/static/js/newComment'); +const padcookie = require('ep_etherpad-lite/static/js/pad_cookie').padcookie; +const preCommentMark = require('ep_comments/static/js/preCommentMark'); +const getCommentIdOnFirstPositionSelected = events.getCommentIdOnFirstPositionSelected; +const hasCommentOnSelection = events.hasCommentOnSelection; +const Security = require('ep_etherpad-lite/static/js/security'); + +const cssFiles = [ + 'ep_comments/static/css/comment.css', + 'ep_comments/static/css/commentIcon.css', +]; + +const UPDATE_COMMENT_LINE_POSITION_EVENT = 'updateCommentLinePosition'; + +const parseMultiline = (text) => { + if (!text) return text; + text = JSON.stringify(text); + return text.substr(1, (text.length - 2)); +}; + +/* ******************************************************************** + * ep_comments Plugin * + ******************************************************************** */ + +// Container +const MdComments = function (context) { + this.container = null; + this.padOuter = null; + this.padInner = null; + this.ace = context.ace; + + // Required for instances running on weird ports + // This probably needs some work for instances running on root or not on /p/ + const loc = document.location; + const port = loc.port === '' ? (loc.protocol === 'https:' ? 443 : 80) : loc.port; + const url = `${loc.protocol}//${loc.hostname}:${port}/comment`; + + this.padId = clientVars.padId; + this.socket = io.connect(url, { + query: `padId=${this.padId}`, + }); + + this.comments = []; + this.commentReplies = {}; + this.mapFakeComments = []; + this.mapOriginalCommentsId = []; + this.shouldCollectComment = false; + this.init(); + this.preCommentMarker = preCommentMark.init(this.ace); +}; + +// Init MuDoc plugin comment pads +MdComments.prototype.init = async function () { + const self = this; + moment.locale(html10n.getLanguage()); + + // Init prerequisite + this.findContainers(); + this.insertContainers(); // Insert comment containers in sidebar + + // Init icons container + commentIcons.insertContainer(); + + const [comments, replies] = await Promise.all([this.getComments(), this.getCommentReplies()]); + if (!$.isEmptyObject(comments)) { + this.setComments(comments); + this.collectComments(); + } + if (!$.isEmptyObject(replies)) { + this.commentReplies = replies; + this.collectCommentReplies(); + } + this.commentRepliesListen(); + this.commentListen(); + + // Init add push event + this.pushComment('add', (commentId, comment) => { + this.setComment(commentId, comment); + this.collectCommentsAfterSomeIntervalsOfTime(); + }); + + // When language is changed, we need to reload the comments to make sure + // all templates are localized + html10n.bind('localized', () => { + // Fall back to 'en' if moment.js doesn't support the language. + moment.locale([html10n.getLanguage(), 'en']); + this.localizeExistingComments(); + }); + + // Recalculate position when editor is resized + $('#settings input, #skin-variant-full-width').on('change', (e) => { + this.setYofComments(); + }); + this.padInner.contents().on(UPDATE_COMMENT_LINE_POSITION_EVENT, (e) => { + this.setYofComments(); + }); + $(window).resize(_.debounce(() => { this.setYofComments(); }, 100)); + + // On click comment icon toolbar + $('.addComment').on('click', (e) => { + e.preventDefault(); // stops focus from being lost + this.displayNewCommentForm(); + }); + + // Import for below listener : we are using this.container.parent() so we include + // events on both comment-modal and sidebar + + // Listen for events to delete a comment + // All this does is remove the comment attr on the selection + this.container.parent().on('click', '.comment-delete', async function () { + const commentId = $(this).closest('.comment-container')[0].id; + try { + await self._send('deleteComment', { + padId: self.padId, + commentId, + authorId: clientVars.userId, + }); + } catch (err) { + if (err.message !== 'unauth') throw err; // Let the uncaught error handler handle it. + $.gritter.add({ + title: html10n.translations['ep_comments.error'] || 'Error', + text: html10n.translations['ep_comments.error.delete_unauth'] || + 'You cannot delete other users comments!', + class_name: 'error', + }); + return; + } + self.deleteComment(commentId); + const padOuter = $('iframe[name="ace_outer"]').contents(); + const padInner = padOuter.find('iframe[name="ace_inner"]'); + const selector = `.${commentId}`; + const ace = self.ace; + + ace.callWithAce((aceTop) => { + const repArr = aceTop.ace_getRepFromSelector(selector, padInner); + // rep is an array of reps.. I will need to iterate over each to do something meaningful.. + $.each(repArr, (index, rep) => { + // I don't think we need this nested call + ace.callWithAce((ace) => { + ace.ace_performSelectionChange(rep[0], rep[1], true); + ace.ace_setAttributeOnSelection('comment', 'comment-deleted'); + // Note that this is the correct way of doing it, instead of there being + // a commentId we now flag it as "comment-deleted" + }); + }); + }, 'deleteCommentedSelection', true); + }); + + // Listen for events to edit a comment + // Here, it adds a form to edit the comment text + this.container.parent().on('click', '.comment-edit', function () { + const $commentBox = $(this).closest('.comment-container'); + $commentBox.addClass('editing'); + + const textBox = self.findCommentText($commentBox).last(); + + // if edit form not already there + if (textBox.siblings('.comment-edit-form').length === 0) { + // add a form to edit the field + const data = {}; + data.text = textBox.text(); + const content = $('#editCommentTemplate').tmpl(data); + // localize the comment/reply edit form + commentL10n.localize(content); + // insert form + textBox.before(content); + } + }); + + // submit the edition on the text and update the comment text + this.container.parent().on('click', '.comment-edit-submit', async function (e) { + e.preventDefault(); + e.stopPropagation(); + const $commentBox = $(this).closest('.comment-container'); + const $commentForm = $(this).closest('.comment-edit-form'); + const commentId = $commentBox.data('commentid'); + const commentText = $commentForm.find('.comment-edit-text').val(); + const data = {}; + data.commentId = commentId; + data.padId = clientVars.padId; + data.commentText = commentText; + data.authorId = clientVars.userId; + + try { + await self._send('updateCommentText', data); + } catch (err) { + if (err.message !== 'unauth') throw err; // Let the uncaught error handler handle it. + $.gritter.add({ + title: html10n.translations['ep_comments.error'] || 'Error', + text: html10n.translations['ep_comments.error.edit_unauth'] || + 'You cannot edit other users comments!', + class_name: 'error', + }); + return; + } + $commentForm.remove(); + $commentBox.removeClass('editing'); + self.updateCommentBoxText(commentId, commentText); + + // although the comment or reply was saved on the data base successfully, it needs + // to update the comment or comment reply variable with the new text saved + self.setCommentOrReplyNewText(commentId, commentText); + }); + + // hide the edit form and make the comment author and text visible again + this.container.parent().on('click', '.comment-edit-cancel', function (e) { + e.preventDefault(); + e.stopPropagation(); + const $commentBox = $(this).closest('.comment-container'); + const textBox = self.findCommentText($commentBox).last(); + textBox.siblings('.comment-edit-form').remove(); + $commentBox.removeClass('editing'); + }); + + // Listen for include suggested change toggle + this.container.parent().on('change', '.suggestion-checkbox', function () { + const parentComment = $(this).closest('.comment-container'); + const parentSuggest = $(this).closest('.comment-reply'); + + if ($(this).is(':checked')) { + const commentId = parentComment.data('commentid'); + const padOuter = $('iframe[name="ace_outer"]').contents(); + const padInner = padOuter.find('iframe[name="ace_inner"]'); + + const currentString = padInner.contents().find(`.${commentId}`).html(); + + parentSuggest.find('.from-value').html(currentString); + parentSuggest.find('.suggestion').show(); + } else { + parentSuggest.find('.suggestion').hide(); + } + }); + + // User accepts or revert a change + this.container.parent().on('submit', '.comment-changeTo-form', function (e) { + e.preventDefault(); + const data = self.getCommentData(); + const commentEl = $(this).closest('.comment-container'); + data.commentId = commentEl.data('commentid'); + const padOuter = $('iframe[name="ace_outer"]').contents(); + const padInner = padOuter.find('iframe[name="ace_inner"]').contents(); + + // Are we reverting a change? + const isRevert = commentEl.hasClass('change-accepted'); + let newString = + isRevert ? $(this).find('.from-value').html() : $(this).find('.to-value').html(); + + // In case of suggested change is inside a reply, the parentId is different from the commentId + // (=replyId) + const parentId = $(this).closest('.sidebar-comment').data('commentid'); + // Nuke all that aren't first lines of this comment + padInner.find(`.${parentId}:not(:first)`).html(''); + + const padCommentSpan = padInner.find(`.${parentId}`).first(); + newString = newString.replace(/(?:\r\n|\r)/g, '
    '); + + // Write the new pad contents + padCommentSpan.html(newString); + + if (isRevert) { + // Tell all users this change was reverted + self._send('revertChange', data); + self.showChangeAsReverted(data.commentId); + } else { + // Tell all users this change was accepted + self._send('acceptChange', data); + // Update our own comments container with the accepted change + self.showChangeAsAccepted(data.commentId); + } + + // TODO: we need ace editor to commit the change so other people get it + // currently after approving or reverting, you need to do other thing on the pad + // for ace to commit + }); + + // When input reply is focused we display more option + this.container.parent().on('focus', '.comment-content', function (e) { + $(this).closest('.new-comment').addClass('editing'); + }); + // When we leave we reset the form option to its minimal (only input) + this.container.parent().on('mouseleave', '.comment-container', function (e) { + $(this).find('.suggestion-checkbox').prop('checked', false); + $(this).find('.new-comment').removeClass('editing'); + }); + + // When a reply get submitted + this.container.parent().on('submit', '.new-comment', async function (e) { + e.preventDefault(); + + const data = self.getCommentData(); + data.commentId = $(this).closest('.comment-container').data('commentid'); + data.reply = $(this).find('.comment-content').val(); + data.changeTo = $(this).find('.to-value').val() || null; + data.changeFrom = $(this).find('.from-value').text() || null; + $(this).trigger('reset_reply'); + await self._send('addCommentReply', data); + const replies = await self.getCommentReplies(); + self.commentReplies = replies; + self.collectCommentReplies(); + // Once the new reply is displayed, we clear the form + $('iframe[name="ace_outer"]').contents().find('.new-comment').removeClass('editing'); + }); + this.container.parent().on('reset_reply', '.new-comment', function (e) { + // Reset the form + $(this).find('.comment-content').val(''); + $(this).find(':focus').blur(); + $(this).find('.to-value').val(''); + $(this).find('.suggestion-checkbox').prop('checked', false); + $(this).removeClass('editing'); + }); + // When click cancel reply + this.container.parent().on('click', '.btn-cancel-reply', function (e) { + $(this).closest('.new-comment').trigger('reset_reply'); + }); + + + // Enable and handle cookies + if (padcookie.getPref('comments') === false) { + this.padOuter.find('#comments, #commentIcons').removeClass('active'); + $('#options-comments').attr('checked', 'unchecked'); + $('#options-comments').attr('checked', false); + } else { + $('#options-comments').attr('checked', 'checked'); + } + + $('#options-comments').on('change', () => { + const checked = $('#options-comments').is(':checked'); + padcookie.setPref('comments', checked); + this.padOuter.find('#comments, #commentIcons').toggleClass('active', checked); + $('body').toggleClass('comments-active', checked); + $('iframe[name="ace_outer"]').contents().find('body').toggleClass('comments-active', checked); + }); + + // Check to see if we should show already.. + $('#options-comments').trigger('change'); + + // TODO - Implement to others browser like, Microsoft Edge, Opera, IE + // Override copy, cut, paste events on Google chrome and Mozilla Firefox. + // When an user copies a comment and selects only the span, or part of it, Google chrome + // does not copy the classes only the styles, for example: + // text to be copied + // As the comment classes are not only used for styling we have to add these classes when it + // pastes the content + // The same does not occur when the user selects more than the span, for example: + // textto be copied + if (browser.chrome || browser.firefox) { + this.padInner.contents().on('copy', (e) => { + events.addTextOnClipboard( + e, this.ace, this.padInner, false, this.comments, this.commentReplies); + }); + + this.padInner.contents().on('cut', (e) => { + events.addTextOnClipboard(e, this.ace, this.padInner, true); + }); + + this.padInner.contents().on('paste', (e) => { + events.saveCommentsAndReplies(e); + }); + } +}; + +MdComments.prototype.findCommentText = function ($commentBox) { + const isReply = $commentBox.hasClass('sidebar-comment-reply'); + if (isReply) return $commentBox.find('.comment-text'); + return $commentBox.find('.compact-display-content .comment-text, ' + + '.full-display-content .comment-title-wrapper .comment-text'); +}; +// This function is useful to collect new comments on the collaborators +MdComments.prototype.collectCommentsAfterSomeIntervalsOfTime = async function () { + await new Promise((resolve) => window.setTimeout(resolve, 300)); + this.collectComments(); + + let countComments = Object.keys(this.comments).length; + const padOuter = $('iframe[name="ace_outer"]').contents(); + this.padOuter = padOuter; + this.padInner = padOuter.find('iframe[name="ace_inner"]'); + let padComment = this.padInner.contents().find('.comment'); + if (countComments <= padComment.length) return; + + await new Promise((resolve) => window.setTimeout(resolve, 1000)); + this.collectComments(); + countComments = Object.keys(this.comments).length; + padComment = this.padInner.contents().find('.comment'); + if (countComments <= padComment.length) return; + + await new Promise((resolve) => window.setTimeout(resolve, 3000)); + this.collectComments(); + countComments = Object.keys(this.comments).length; + padComment = this.padInner.contents().find('.comment'); + if (countComments <= padComment.length) return; + + await new Promise((resolve) => window.setTimeout(resolve, 9000)); + this.collectComments(); +}; + +// Insert comments container on element use for linenumbers +MdComments.prototype.findContainers = function () { + const padOuter = $('iframe[name="ace_outer"]').contents(); + this.padOuter = padOuter; + this.padInner = padOuter.find('iframe[name="ace_inner"]'); + this.outerBody = padOuter.find('#outerdocbody'); +}; + +// Collect Comments and link text content to the comments div +MdComments.prototype.collectComments = function (callback) { + const self = this; + const container = this.container; + const comments = this.comments; + const padComment = this.padInner.contents().find('.comment'); + + padComment.each(function (it) { + const $this = $(this); + const cls = $this.attr('class'); + const classCommentId = /(?:^| )(c-[A-Za-z0-9]*)/.exec(cls); + const commentId = (classCommentId) ? classCommentId[1] : null; + if (!commentId) return; + + self.padInner.contents().find('#innerdocbody').addClass('comments'); + + const commentElm = container.find(`#${commentId}`); + + const comment = comments[commentId]; + if (comment) { + comment.data.changeFrom = parseMultiline(comment.data.changeFrom); + // If comment is not in sidebar insert it + if (commentElm.length === 0) { + self.insertComment(commentId, comment.data, it); + } + // localize comment element + commentL10n.localize(commentElm); + } + const prevCommentElm = commentElm.prev(); + let commentPos = 0; + + if (prevCommentElm.length !== 0) { + const prevCommentPos = prevCommentElm.css('top'); + const prevCommentHeight = prevCommentElm.innerHeight(); + + commentPos = parseInt(prevCommentPos) + prevCommentHeight + 30; + } + + commentElm.css({top: commentPos}); + }); + + // HOVER SIDEBAR COMMENT + let hideCommentTimer; + this.container.on('mouseover', '.sidebar-comment', (e) => { + // highlight comment + clearTimeout(hideCommentTimer); + commentBoxes.highlightComment(e.currentTarget.id, e); + }).on('mouseout', '.sidebar-comment', (e) => { + // do not hide directly the comment, because sometime the mouse get out accidently + hideCommentTimer = setTimeout(() => { + commentBoxes.hideComment(e.currentTarget.id); + }, 1000); + }); + + // HOVER OR CLICK THE COMMENTED TEXT IN THE EDITOR + // hover event + this.padInner.contents().on('mouseover', '.comment', function (e) { + if (container.is(':visible')) { // not on mobile + clearTimeout(hideCommentTimer); + const commentId = self.commentIdOf(e); + commentBoxes.highlightComment(commentId, e, $(this)); + } + }); + + // click event + this.padInner.contents().on('click', '.comment', function (e) { + const commentId = self.commentIdOf(e); + commentBoxes.highlightComment(commentId, e, $(this)); + }); + + this.padInner.contents().on('mouseleave', '.comment', (e) => { + const commentOpenedByClickOnIcon = commentIcons.isCommentOpenedByClickOnIcon(); + // only closes comment if it was not opened by a click on the icon + if (!commentOpenedByClickOnIcon && container.is(':visible')) { + hideCommentTimer = setTimeout(() => { + self.closeOpenedComment(e); + }, 1000); + } + }); + + this.addListenersToCloseOpenedComment(); + + this.setYofComments(); + if (callback) callback(); +}; + +MdComments.prototype.addListenersToCloseOpenedComment = function () { + // we need to add listeners to the different iframes of the page + $(document).on('touchstart click', (e) => { + this.closeOpenedCommentIfNotOnSelectedElements(e); + }); + this.padOuter.find('html').on('touchstart click', (e) => { + this.closeOpenedCommentIfNotOnSelectedElements(e); + }); + this.padInner.contents().find('html').on('touchstart click', (e) => { + this.closeOpenedCommentIfNotOnSelectedElements(e); + }); +}; + +// Close comment that is opened +MdComments.prototype.closeOpenedComment = function (e) { + const commentId = this.commentIdOf(e); + commentBoxes.hideComment(commentId); +}; + +// Close comment if event target was outside of comment or on a comment icon +MdComments.prototype.closeOpenedCommentIfNotOnSelectedElements = function (e) { + // Don't do anything if clicked on the allowed elements: + // any of the comment icons + if (commentIcons.shouldNotCloseComment(e) || commentBoxes.shouldNotCloseComment(e)) return; + // All clear, can close the comment + this.closeOpenedComment(e); +}; + +// Collect Comments and link text content to the comments div +MdComments.prototype.collectCommentReplies = function (callback) { + $.each(this.commentReplies, (replyId, reply) => { + const commentId = reply.commentId; + if (commentId) { + // tell comment icon that this comment has 1+ replies + commentIcons.commentHasReply(commentId); + + const existsAlready = $('iframe[name="ace_outer"]').contents().find(`#${replyId}`).length; + if (existsAlready) return; + + reply.replyId = replyId; + reply.text = reply.text || ''; + reply.date = moment(reply.timestamp).fromNow(); + reply.formattedDate = new Date(reply.timestamp).toISOString(); + + const content = $('#replyTemplate').tmpl(reply); + if (reply.author !== clientVars.userId) { + $(content).find('.comment-edit').remove(); + } + // localize comment reply + commentL10n.localize(content); + const repliesContainer = + $('iframe[name="ace_outer"]').contents().find(`#${commentId} .comment-replies-container`); + repliesContainer.append(content); + } + }); +}; + +MdComments.prototype.commentIdOf = function (e) { + const cls = e.currentTarget.classList; + const classCommentId = /(?:^| )(c-[A-Za-z0-9]*)/.exec(cls); + + return (classCommentId) ? classCommentId[1] : null; +}; + +// Insert comment container in sidebar +MdComments.prototype.insertContainers = function () { + const target = $('iframe[name="ace_outer"]').contents().find('#outerdocbody'); + + // Create hover modal + target.prepend( + $('
    ').addClass('comment-modal popup').append( + $('
    ').addClass('popup-content comment-modal-comment'))); + + // Add comments side bar container + target.prepend($('
    ').attr('id', 'comments')); + + this.container = this.padOuter.find('#comments'); +}; + +// Insert a comment node +MdComments.prototype.insertComment = function (commentId, comment, index) { + let content = null; + const container = this.container; + const commentAfterIndex = container.find('.sidebar-comment').eq(index); + + comment.commentId = commentId; + comment.reply = true; + content = $('#commentsTemplate').tmpl(comment); + if (comment.author !== clientVars.userId) { + $(content).find('.comment-actions-wrapper').addClass('hidden'); + } + commentL10n.localize(content); + + // position doesn't seem to be relative to rep + + if (index === 0) { + content.prependTo(container); + } else if (commentAfterIndex.length === 0) { + content.appendTo(container); + } else { + commentAfterIndex.before(content); + } + + // insert icon + commentIcons.addIcon(commentId, comment); +}; + +// Set all comments to be inline with their target REP +MdComments.prototype.setYofComments = function () { + // for each comment in the pad + const padOuter = $('iframe[name="ace_outer"]').contents(); + const padInner = padOuter.find('iframe[name="ace_inner"]'); + const inlineComments = this.getFirstOcurrenceOfCommentIds(); + const commentsToBeShown = []; + + $.each(inlineComments, function () { + // classname is the ID of the comment + const commentId = /(?:^| )(c-[A-Za-z0-9]*)/.exec(this.className); + if (!commentId || !commentId[1]) return; + const commentEle = padOuter.find(`#${commentId[1]}`); + + let topOffset = this.offsetTop; + topOffset += parseInt(padInner.css('padding-top').split('px')[0]); + topOffset += parseInt($(this).css('padding-top').split('px')[0]); + + if (commentId) { + // adjust outer comment... + commentBoxes.adjustTopOf(commentId[1], topOffset); + // ... and adjust icons too + commentIcons.adjustTopOf(commentId[1], topOffset); + + // mark this comment to be displayed if it was visible before we start adjusting its position + if (commentIcons.shouldShow(commentEle)) commentsToBeShown.push(commentEle); + } + }); + + // re-display comments that were visible before + _.each(commentsToBeShown, (commentEle) => { + commentEle.show(); + }); +}; + +MdComments.prototype.getFirstOcurrenceOfCommentIds = function () { + const padOuter = $('iframe[name="ace_outer"]').contents(); + const padInner = padOuter.find('iframe[name="ace_inner"]').contents(); + const commentsId = this.getUniqueCommentsId(padInner); + const firstOcurrenceOfCommentIds = + _.map(commentsId, (commentId) => padInner.find(`.${commentId}`).first().get(0)); + return firstOcurrenceOfCommentIds; +}; + +MdComments.prototype.getUniqueCommentsId = function (padInner) { + const inlineComments = padInner.find('.comment'); + const commentsId = _.map(inlineComments, (inlineComment) => { + const commentId = /(?:^| )(c-[A-Za-z0-9]*)/.exec(inlineComment.className); + // avoid when it has a '.comment' that it has a fakeComment class 'fakecomment-123' yet. + if (commentId && commentId[1]) return commentId[1]; + }); + const onlyUnique = (value, index, self) => self.indexOf(value) === index; + return commentsId.filter(onlyUnique); +}; + +// Indicates if all comments are on the correct Y position, and don't need to +// be adjusted +MdComments.prototype.allCommentsOnCorrectYPosition = function () { + // for each comment in the pad + const padOuter = $('iframe[name="ace_outer"]').contents(); + const padInner = padOuter.find('iframe[name="ace_inner"]'); + const inlineComments = padInner.contents().find('.comment'); + let allCommentsAreCorrect = true; + + $.each(inlineComments, function () { + const y = this.offsetTop; + const commentId = /(?:^| )(c-[A-Za-z0-9]*)/.exec(this.className); + if (commentId && commentId[1]) { + if (!commentBoxes.isOnTop(commentId[1], y)) { // found one comment on the incorrect place + allCommentsAreCorrect = false; + return false; // to break loop + } + } + }); + + return allCommentsAreCorrect; +}; + +MdComments.prototype.localizeExistingComments = function () { + const self = this; + const padComments = this.padInner.contents().find('.comment'); + const comments = this.comments; + + padComments.each((key, it) => { + const $this = $(it); + const cls = $this.attr('class'); + const classCommentId = /(?:^| )(c-[A-Za-z0-9]*)/.exec(cls); + const commentId = (classCommentId) ? classCommentId[1] : null; + + if (commentId != null) { + const commentElm = self.container.find(`#${commentId}`); + const comment = comments[commentId]; + + // localize comment element... + commentL10n.localize(commentElm); + // ... and update its date + comment.data.date = moment(comment.data.timestamp).fromNow(); + comment.data.formattedDate = new Date(comment.data.timestamp).toISOString(); + $(commentElm).find('.comment-created-at').html(comment.data.date); + } + }); +}; + +// Set comments content data +MdComments.prototype.setComments = function (comments) { + for (const [commentId, comment] of Object.entries(comments)) { + this.setComment(commentId, comment); + } +}; + +// Set comment data +MdComments.prototype.setComment = function (commentId, comment) { + const comments = this.comments; + comment.date = moment(comment.timestamp).fromNow(); + comment.formattedDate = new Date(comment.timestamp).toISOString(); + + if (comments[commentId] == null) comments[commentId] = {}; + comments[commentId].data = comment; +}; + +// commentReply = ['c-reply-123', commentDataObject] +// commentDataObject = {author:..., name:..., text:..., ...} +MdComments.prototype.setCommentReply = function (commentReply) { + const commentReplies = this.commentReplies; + const replyId = commentReply[0]; + commentReplies[replyId] = commentReply[1]; +}; + +// set the text of the comment or comment reply +MdComments.prototype.setCommentOrReplyNewText = function (commentOrReplyId, text) { + if (this.comments[commentOrReplyId]) { + this.comments[commentOrReplyId].data.text = text; + } else if (this.commentReplies[commentOrReplyId]) { + this.commentReplies[commentOrReplyId].text = text; + } +}; + +MdComments.prototype._send = async function (type, ...args) { + return await new Promise((resolve, reject) => { + this.socket.emit(type, ...args, (errj, val) => { + if (errj != null) return reject(Object.assign(new Error(errj.message), {name: errj.name})); + resolve(val); + }); + }); +}; + +// Get all comments +MdComments.prototype.getComments = async function () { + return (await this._send('getComments', {padId: this.padId})).comments; +}; + +// Get all comment replies +MdComments.prototype.getCommentReplies = async function () { + return (await this._send('getCommentReplies', {padId: this.padId})).replies; +}; + +MdComments.prototype.getCommentData = function () { + const data = {}; + + // Insert comment data + data.padId = this.padId; + data.comment = {}; + data.comment.author = clientVars.userId; + data.comment.name = pad.myUserInfo.name; + data.comment.timestamp = new Date().getTime(); + + // If client is anonymous + if (data.comment.name === undefined) { + data.comment.name = clientVars.userAgent; + } + + return data; +}; + +// Delete a pad comment +MdComments.prototype.deleteComment = function (commentId) { + $('iframe[name="ace_outer"]').contents().find(`#${commentId}`).remove(); +}; + +const cloneLine = (line) => { + const padOuter = $('iframe[name="ace_outer"]').contents(); + const padInner = padOuter.find('iframe[name="ace_inner"]'); + + const lineElem = $(line.lineNode); + const lineClone = lineElem.clone(); + const innerOffset = $(padInner).offset().left; + const innerPadding = parseInt(padInner.css('padding-left') + lineElem.offset().left); + const innerdocbodyMargin = innerOffset + innerPadding || 0; + padInner.contents().find('body').append(lineClone); + lineClone.css({position: 'absolute'}); + lineClone.css(lineElem.offset()); + lineClone.css({left: innerdocbodyMargin}); + lineClone.width(lineElem.width()); + + return lineClone; +}; + +let isHeading = function (index) { + const attribs = this.documentAttributeManager.getAttributesOnLine(index); + for (let i = 0; i < attribs.length; i++) { + if (attribs[i][0] === 'heading') { + const value = attribs[i][1]; + i = attribs.length; + return value; + } + } + return false; +}; + +const getXYOffsetOfRep = (rep) => { + let selStart = rep.selStart; + let selEnd = rep.selEnd; + // make sure end is after start + if (selStart[0] > selEnd[0] || (selStart[0] === selEnd[0] && selStart[1] > selEnd[1])) { + selEnd = selStart; + selStart = _.clone(selStart); + } + + let startIndex = 0; + const endIndex = selEnd[1]; + const lineIndex = selEnd[0]; + if (selStart[0] === selEnd[0]) { + startIndex = selStart[1]; + } + + const padInner = $('iframe[name="ace_outer"]').contents().find('iframe[name="ace_inner"]'); + + // Get the target Line + const startLine = rep.lines.atIndex(selStart[0]); + const endLine = rep.lines.atIndex(selEnd[0]); + const clone = cloneLine(endLine); + let lineText = Security.escapeHTML($(endLine.lineNode).text()).split(''); + lineText.splice(endIndex, 0, ''); + lineText.splice(startIndex, 0, ''); + lineText = lineText.join(''); + + const heading = isHeading(lineIndex); + if (heading) { + lineText = `<${heading}>${lineText}`; + } + $(clone).html(lineText); + + // Is the line visible yet? + if ($(startLine.lineNode).length !== 0) { + const worker = $(clone).find('#selectWorker'); + // A standard generic offset' + let top = worker.offset().top + padInner.offset().top + parseInt(padInner.css('padding-top')); + let left = worker.offset().left; + // adjust position + top += worker[0].offsetHeight; + + if (left < 0) { + left = 0; + } + // Remove the clone element + $(clone).remove(); + return [left, top]; + } +}; + +MdComments.prototype.displayNewCommentForm = function () { + const rep = {}; + const ace = this.ace; + + ace.callWithAce((ace) => { + const saveRep = ace.ace_getRep(); + + rep.lines = saveRep.lines; + rep.selStart = saveRep.selStart; + rep.selEnd = saveRep.selEnd; + }, 'saveCommentedSelection', true); + + const selectedText = this.getSelectedText(rep); + // we have nothing selected, do nothing + const noTextSelected = (selectedText.length === 0); + if (noTextSelected) { + $.gritter.add({ + text: html10n.translations['ep_comments.add_comment.hint'] || + 'Please first select the text to comment', + }); + return; + } + + this.createNewCommentFormIfDontExist(rep); + + // Write the text to the changeFrom form + $('#newComment').find('.from-value').text(selectedText); + + // Display form + setTimeout(() => { + const position = getXYOffsetOfRep(rep); + newComment.showNewCommentPopup(position); + }); + + // Check if the first element selected is visible in the viewport + const $firstSelectedElement = this.getFirstElementSelected(); + const firstSelectedElementInViewport = this.isElementInViewport($firstSelectedElement); + + if (!firstSelectedElementInViewport) { + this.scrollViewportIfSelectedTextIsNotVisible($firstSelectedElement); + } + + // Adjust focus on the form + $('#newComment').find('.comment-content').focus(); +}; + +MdComments.prototype.scrollViewportIfSelectedTextIsNotVisible = function ($firstSelectedElement) { + // Set the top of the form to be the same Y as the target Rep + const y = $firstSelectedElement.offsetTop; + const padOuter = $('iframe[name="ace_outer"]').contents(); + padOuter.find('#outerdocbody').scrollTop(y); // Works in Chrome + padOuter.find('#outerdocbody').parent().scrollTop(y); // Works in Firefox +}; + +MdComments.prototype.isElementInViewport = function (element) { + const elementPosition = element.getBoundingClientRect(); + const outerdocbody = $('iframe[name="ace_outer"]').contents().find('#outerdocbody'); + const scrolltop = (outerdocbody.scrollTop() || + // Works only on Firefox: + outerdocbody.parent().scrollTop()); + // position relative to the current viewport + const elementPositionTopOnViewport = elementPosition.top - scrolltop; + const elementPositionBottomOnViewport = elementPosition.bottom - scrolltop; + + const $aceOuter = $('iframe[name="ace_outer"]'); + const aceOuterHeight = $aceOuter.height(); + const aceOuterPaddingTop = this.getIntValueOfCSSProperty($aceOuter, 'padding-top'); + + const clientHeight = aceOuterHeight - aceOuterPaddingTop; + + const elementAboveViewportTop = elementPositionTopOnViewport < 0; + const elementBelowViewportBottom = elementPositionBottomOnViewport > clientHeight; + + return !(elementAboveViewportTop || elementBelowViewportBottom); +}; + +MdComments.prototype.getIntValueOfCSSProperty = function ($element, property) { + const valueString = $element.css(property); + return parseInt(valueString) || 0; +}; + +MdComments.prototype.getFirstElementSelected = function () { + let element; + + this.ace.callWithAce((ace) => { + const rep = ace.ace_getRep(); + const line = rep.lines.atIndex(rep.selStart[0]); + const key = `#${line.key}`; + const padOuter = $('iframe[name="ace_outer"]').contents(); + const padInner = padOuter.find('iframe[name="ace_inner"]').contents(); + element = padInner.find(key); + }, 'getFirstElementSelected', true); + + return element[0]; +}; + +// Indicates if user selected some text on editor +MdComments.prototype.checkNoTextSelected = function (rep) { + const noTextSelected = ((rep.selStart[0] === rep.selEnd[0]) && + (rep.selStart[1] === rep.selEnd[1])); + + return noTextSelected; +}; + +// Create form to add comment +MdComments.prototype.createNewCommentFormIfDontExist = function (rep) { + const data = this.getCommentData(); + + // If a new comment box doesn't already exist, create one + data.changeFrom = parseMultiline(this.getSelectedText(rep)); + newComment.insertNewCommentPopupIfDontExist(data, (comment, index) => { + if (comment.changeTo) { + data.comment.changeFrom = comment.changeFrom; + data.comment.changeTo = comment.changeTo; + } + data.comment.text = comment.text; + + this.saveComment(data, rep); + }); +}; + +// Get a string representation of the text selected on the editor +MdComments.prototype.getSelectedText = function (rep) { + // The selection representation looks like this if it starts with the fifth character in the + // second line and ends at (but does not include) the third character in the eighth line: + // rep.selStart = [1, 4]; // 2nd line 5th char + // rep.selEnd = [7, 2]; // 8th line 3rd char + const selectedTextLines = []; + const lastLine = this.getLastLine(rep.selStart[0], rep); + for (let lineNumber = rep.selStart[0]; lineNumber <= lastLine; ++lineNumber) { + const line = rep.lines.atIndex(lineNumber); + const selStartsAfterLine = rep.selStart[0] > lineNumber || + (rep.selStart[0] === lineNumber && rep.selStart[1] >= line.text.length); + if (selStartsAfterLine) continue; // Nothing in this line is selected. + const selEndsBeforeLine = rep.selEnd[0] < lineNumber || + (rep.selEnd[0] === lineNumber && rep.selEnd[1] <= 0); + if (selEndsBeforeLine) continue; // Nothing in this line is selected. + const selStartsBeforeLine = rep.selStart[0] < lineNumber || rep.selStart[1] < 0; + const posStart = selStartsBeforeLine ? 0 : rep.selStart[1]; + const selEndsAfterLine = rep.selEnd[0] > lineNumber || rep.selEnd[1] > line.text.length; + const posEnd = selEndsAfterLine ? line.text.length : rep.selEnd[1]; + // If the selection includes the very beginning of line, and the line has a line marker, it + // means the line marker was selected as well. Exclude it from the selected text. + selectedTextLines.push( + line.text.substring((posStart === 0 && this.lineHasMarker(line)) ? 1 : posStart, posEnd)); + } + return selectedTextLines.join('\n'); +}; + +MdComments.prototype.getLastLine = function (firstLine, rep) { + let lastLineSelected = rep.selEnd[0]; + + if (lastLineSelected > firstLine) { + // Ignore last line if the selected text of it it is empty + if (this.lastLineSelectedIsEmpty(rep, lastLineSelected)) { + lastLineSelected--; + } + } + return lastLineSelected; +}; + +MdComments.prototype.lastLineSelectedIsEmpty = function (rep, lastLineSelected) { + const line = rep.lines.atIndex(lastLineSelected); + // when we've a line with line attribute, the first char line position + // in a line is 1 because of the *, otherwise is 0 + const firstCharLinePosition = this.lineHasMarker(line) ? 1 : 0; + const lastColumnSelected = rep.selEnd[1]; + + return lastColumnSelected === firstCharLinePosition; +}; + +MdComments.prototype.lineHasMarker = function (line) { + return line.lineMarker === 1; +}; + +// Save comment +MdComments.prototype.saveComment = async function (data, rep) { + const res = await this._send('addComment', data); + if (res == null) return; + const [commentId, comment] = res; + comment.commentId = commentId; + + this.ace.callWithAce((ace) => { + // we should get rep again because the document might have changed.. + // details at https://github.com/akhil-naidu/ep_comments/issues/133 + rep = ace.ace_getRep(); + ace.ace_performSelectionChange(rep.selStart, rep.selEnd, true); + ace.ace_setAttributeOnSelection('comment', commentId); + }, 'insertComment', true); + + this.setComment(commentId, comment); + this.collectComments(); +}; + +// commentData = {c-newCommentId123: data:{author:..., date:..., ...}, +// c-newCommentId124: data:{...}} +MdComments.prototype.saveCommentWithoutSelection = async function (padId, commentData) { + const data = this.buildComments(commentData); + const comments = await this._send('bulkAddComment', padId, data); + this.setComments(comments); + this.shouldCollectComment = true; +}; + +MdComments.prototype.buildComments = function (commentsData) { + const comments = + _.map(commentsData, (commentData, commentId) => this.buildComment(commentId, commentData.data)); + return comments; +}; + +// commentData = {c-newCommentId123: data:{author:..., date:..., ...}, ... +MdComments.prototype.buildComment = function (commentId, commentData) { + const data = {}; + data.padId = this.padId; + data.commentId = commentId; + data.text = commentData.text; + data.changeTo = commentData.changeTo; + data.changeFrom = commentData.changeFrom; + data.name = commentData.name; + data.timestamp = parseInt(commentData.timestamp); + + return data; +}; + +MdComments.prototype.getMapfakeComments = function () { + return this.mapFakeComments; +}; + +// commentReplyData = {c-reply-123:{commentReplyData1}, c-reply-234:{commentReplyData1}, ...} +MdComments.prototype.saveCommentReplies = async function (padId, commentReplyData) { + const data = this.buildCommentReplies(commentReplyData); + const replies = await this._send('bulkAddCommentReplies', padId, data); + _.each(replies, (reply) => { + this.setCommentReply(reply); + }); + this.shouldCollectComment = true; // force collect the comment replies saved +}; + +MdComments.prototype.buildCommentReplies = function (repliesData) { + const replies = _.map(repliesData, (replyData) => this.buildCommentReply(replyData)); + return replies; +}; + +// take a replyData and add more fields necessary. E.g. 'padId' +MdComments.prototype.buildCommentReply = function (replyData) { + const data = {}; + data.padId = this.padId; + data.commentId = replyData.commentId; + data.text = replyData.text; + data.changeTo = replyData.changeTo; + data.changeFrom = replyData.changeFrom; + data.replyId = replyData.replyId; + data.name = replyData.name; + data.timestamp = parseInt(replyData.timestamp); + + return data; +}; + +// Listen for comment +MdComments.prototype.commentListen = function () { + const socket = this.socket; + socket.on('pushAddCommentInBulk', async () => { + const allComments = await this.getComments(); + if (!$.isEmptyObject(allComments)) { + // we get the comments in this format {c-123:{author:...}, c-124:{author:...}} + // but it's expected to be {c-123: {data: {author:...}}, c-124:{data:{author:...}}} + // in this.comments + const commentsProcessed = {}; + _.map(allComments, (comment, commentId) => { + commentsProcessed[commentId] = {}; + commentsProcessed[commentId].data = comment; + }); + this.comments = commentsProcessed; + this.collectCommentsAfterSomeIntervalsOfTime(); // here we collect on the collaborators + } + }); +}; + +// Listen for comment replies +MdComments.prototype.commentRepliesListen = function () { + this.socket.on('pushAddCommentReply', async (replyId, reply) => { + const replies = await this.getCommentReplies(); + if (!$.isEmptyObject(replies)) { + this.commentReplies = replies; + this.collectCommentReplies(); + } + }); +}; + +MdComments.prototype.updateCommentBoxText = function (commentId, commentText) { + const $comment = this.container.parent().find(`[data-commentid='${commentId}']`); + const textBox = this.findCommentText($comment); + textBox.text(commentText); +}; + +MdComments.prototype.showChangeAsAccepted = function (commentId) { + const self = this; + + // Get the comment + const comment = this.container.parent().find(`[data-commentid='${commentId}']`); + // Revert other comment that have already been accepted + comment.closest('.sidebar-comment') + .find('.comment-container.change-accepted').addBack('.change-accepted') + .each(function () { + $(this).removeClass('change-accepted'); + const data = {commentId: $(this).attr('data-commentid'), padId: self.padId}; + self._send('revertChange', data); + }); + + // this comment get accepted + comment.addClass('change-accepted'); +}; + +MdComments.prototype.showChangeAsReverted = function (commentId) { + // Get the comment + const comment = this.container.parent().find(`[data-commentid='${commentId}']`); + comment.removeClass('change-accepted'); +}; + +// Push comment from collaborators +MdComments.prototype.pushComment = function (eventType, callback) { + const socket = this.socket; + + socket.on('textCommentUpdated', (commentId, commentText) => { + this.updateCommentBoxText(commentId, commentText); + }); + + socket.on('commentDeleted', (commentId) => { + this.deleteComment(commentId); + }); + + socket.on('changeAccepted', (commentId) => { + this.showChangeAsAccepted(commentId); + }); + + socket.on('changeReverted', (commentId) => { + this.showChangeAsReverted(commentId); + }); + + // On collaborator add a comment in the current pad + if (eventType === 'add') { + socket.on('pushAddComment', (commentId, comment) => { + callback(commentId, comment); + }); + } else if (eventType === 'addCommentReply') { + socket.on('pushAddCommentReply', (replyId, reply) => { + callback(replyId, reply); + }); + } +}; + +/* ******************************************************************** + * MuDoc Hooks * + ******************************************************************** */ + +const hooks = { + + // Init pad comments + postAceInit: (hookName, context, cb) => { + if (!pad.plugins) pad.plugins = {}; + const Comments = new MdComments(context); + pad.plugins.ep_comments = Comments; + + if (!$('#editorcontainerbox').hasClass('flex-layout')) { + $.gritter.add({ + title: 'Error', + text: 'ep_comments: Please upgrade to MuDoc 0.0.2 ' + + 'for this plugin to work correctly', + sticky: true, + class_name: 'error', + }); + } + return cb(); + }, + + postToolbarInit: (hookName, args, cb) => { + const editbar = args.toolbar; + + editbar.registerCommand('addComment', () => { + pad.plugins.ep_comments.displayNewCommentForm(); + }); + return cb(); + }, + + aceEditEvent: (hookName, context) => { + if (!pad.plugins) pad.plugins = {}; + // first check if some text is being marked/unmarked to add comment to it + const eventType = context.callstack.editEvent.eventType; + if (eventType === 'unmarkPreSelectedTextToComment') { + pad.plugins.ep_comments.preCommentMarker.handleUnmarkText(context); + } else if (eventType === 'markPreSelectedTextToComment') { + pad.plugins.ep_comments.preCommentMarker.handleMarkText(context); + } + + if (['setup', 'setBaseText', 'importText'].includes(eventType)) return; + + if (context.callstack.docTextChanged && pad.plugins.ep_comments) { + pad.plugins.ep_comments.setYofComments(); + } + + // some times on init ep_comments is not yet on the plugin list + if (pad.plugins.ep_comments) { + const commentWasPasted = pad.plugins.ep_comments.shouldCollectComment; + const domClean = context.callstack.domClean; + // we have to wait the DOM update from a fakeComment 'fakecomment-123' to a comment class + // 'c-123' + if (commentWasPasted && domClean) { + pad.plugins.ep_comments.collectComments(() => { + pad.plugins.ep_comments.collectCommentReplies(); + pad.plugins.ep_comments.shouldCollectComment = false; + }); + } + } + return; + }, + + // Insert comments classes + aceAttribsToClasses: (hookName, context, cb) => { + if (context.key === 'comment' && context.value !== 'comment-deleted') { + return cb(['comment', context.value]); + } + // only read marks made by current user + if (context.key === preCommentMark.MARK_CLASS && context.value === clientVars.userId) { + return cb([preCommentMark.MARK_CLASS, context.value]); + } + return cb(); + }, + + aceEditorCSS: (hookName, context) => cssFiles, +}; + +exports.aceEditorCSS = hooks.aceEditorCSS; +exports.postAceInit = hooks.postAceInit; +exports.postToolbarInit = hooks.postToolbarInit; +exports.aceAttribsToClasses = hooks.aceAttribsToClasses; +exports.aceEditEvent = hooks.aceEditEvent; + +// Given a CSS selector and a target element (in this case pad inner) +// return the rep as an array of array of tuples IE [[[0,1],[0,2]], [[1,3],[1,5]]] +// We have to return an array of a array of tuples because there can be multiple reps +// For a given selector +// A more sane data structure might be an object such as.. +/* +0:{ + xStart: 0, + xEnd: 1, + yStart: 0, + yEnd: 1 +}, +1:... +*/ +// Alas we follow the MuDoc convention of using tuples here. +const getRepFromSelector = function (selector, container) { + const attributeManager = this.documentAttributeManager; + + const repArr = []; + + // first find the element + const elements = container.contents().find(selector); + // One might expect this to be a rep for the entire document + // However what we actually need to do is find each selection that includes + // this comment and remove it. This is because content can be pasted + // Mid comment which would mean a remove selection could have unexpected consequences + + $.each(elements, (index, span) => { + // create a rep array container we can push to.. + const rep = [[], []]; + + // span not be the div so we have to go to parents until we find a div + const parentDiv = $(span).closest('div'); + // line Number is obviously relative to entire document + // So find out how many elements before in this parent? + const lineNumber = $(parentDiv).prevAll('div').length; + // We can set beginning of rep Y (lineNumber) + rep[0][0] = lineNumber; + + // We can also update the end rep Y + rep[1][0] = lineNumber; + + // Given the comment span, how many characters are before it? + + // All we need to know is the number of characters before .foo + /* + +
    + hello + + world + + are you + + here? + +
    + + */ + // In the example before the correct number would be 21 + // I guess we could do prevAll each length? + // If there are no spans before we get 0, simples! + // Note that this only works if spans are being used, which imho + // Is the correct container however if block elements are registered + // It's plausable that attributes are not maintained :( + let leftOffset = 0; + + // If the line has a lineAttribute then leftOffset should be +1 + // Get each line Attribute on this line.. + let hasLineAttribute = false; + const attrArr = attributeManager.getAttributesOnLine(lineNumber); + $.each(attrArr, (attrK, value) => { + if (value[0] === 'lmkr') hasLineAttribute = true; + }); + if (hasLineAttribute) leftOffset++; + + $(span).prevAll('span').each(function () { + const spanOffset = $(this).text().length; + leftOffset += spanOffset; + }); + rep[0][1] = leftOffset; + rep[1][1] = rep[0][1] + $(span).text().length; // Easy! + repArr.push(rep); + }); + return repArr; +}; + +// Once ace is initialized, we set ace_doInsertHeading and bind it to the context +exports.aceInitialized = (hookName, context, cb) => { + const editorInfo = context.editorInfo; + isHeading = isHeading.bind(context); + editorInfo.ace_getRepFromSelector = getRepFromSelector.bind(context); + editorInfo.ace_getCommentIdOnFirstPositionSelected = + getCommentIdOnFirstPositionSelected.bind(context); + editorInfo.ace_hasCommentOnSelection = hasCommentOnSelection.bind(context); + return cb(); +}; diff --git a/src/plugins/md_comments/static/js/jquery.tmpl.min.js b/src/plugins/md_comments/static/js/jquery.tmpl.min.js new file mode 100644 index 000000000..f08e81dcc --- /dev/null +++ b/src/plugins/md_comments/static/js/jquery.tmpl.min.js @@ -0,0 +1 @@ +(function(a){var r=a.fn.domManip,d="_tmplitem",q=/^[^<]*(<[\w\W]+>)[^>]*$|\{\{\! /,b={},f={},e,p={key:0,data:{}},h=0,c=0,l=[];function g(e,d,g,i){var c={data:i||(d?d.data:{}),_wrap:d?d._wrap:null,tmpl:null,parent:d||null,nodes:[],calls:u,nest:w,wrap:x,html:v,update:t};e&&a.extend(c,e,{nodes:[],parent:d});if(g){c.tmpl=g;c._ctnt=c._ctnt||c.tmpl(a,c);c.key=++h;(l.length?f:b)[h]=c}return c}a.each({appendTo:"append",prependTo:"prepend",insertBefore:"before",insertAfter:"after",replaceAll:"replaceWith"},function(f,d){a.fn[f]=function(n){var g=[],i=a(n),k,h,m,l,j=this.length===1&&this[0].parentNode;e=b||{};if(j&&j.nodeType===11&&j.childNodes.length===1&&i.length===1){i[d](this[0]);g=this}else{for(h=0,m=i.length;h0?this.clone(true):this).get();a.fn[d].apply(a(i[h]),k);g=g.concat(k)}c=0;g=this.pushStack(g,f,i.selector)}l=e;e=null;a.tmpl.complete(l);return g}});a.fn.extend({tmpl:function(d,c,b){return a.tmpl(this[0],d,c,b)},tmplItem:function(){return a.tmplItem(this[0])},template:function(b){return a.template(b,this[0])},domManip:function(d,l,j){if(d[0]&&d[0].nodeType){var f=a.makeArray(arguments),g=d.length,i=0,h;while(i1)f[0]=[a.makeArray(d)];if(h&&c)f[2]=function(b){a.tmpl.afterManip(this,b,j)};r.apply(this,f)}else r.apply(this,arguments);c=0;!e&&a.tmpl.complete(b);return this}});a.extend({tmpl:function(d,h,e,c){var j,k=!c;if(k){c=p;d=a.template[d]||a.template(null,d);f={}}else if(!d){d=c.tmpl;b[c.key]=c;c.nodes=[];c.wrapped&&n(c,c.wrapped);return a(i(c,null,c.tmpl(a,c)))}if(!d)return[];if(typeof h==="function")h=h.call(c||{});e&&e.wrapped&&n(e,e.wrapped);j=a.isArray(h)?a.map(h,function(a){return a?g(e,c,d,a):null}):[g(e,c,d,h)];return k?a(i(c,null,j)):j},tmplItem:function(b){var c;if(b instanceof a)b=b[0];while(b&&b.nodeType===1&&!(c=a.data(b,"tmplItem"))&&(b=b.parentNode));return c||p},template:function(c,b){if(b){if(typeof b==="string")b=o(b);else if(b instanceof a)b=b[0]||{};if(b.nodeType)b=a.data(b,"tmpl")||a.data(b,"tmpl",o(b.innerHTML));return typeof c==="string"?(a.template[c]=b):b}return c?typeof c!=="string"?a.template(null,c):a.template[c]||a.template(null,q.test(c)?c:a(c)):null},encode:function(a){return(""+a).split("<").join("<").split(">").join(">").split('"').join(""").split("'").join("'")}});a.extend(a.tmpl,{tag:{tmpl:{_default:{$2:"null"},open:"if($notnull_1){_=_.concat($item.nest($1,$2));}"},wrap:{_default:{$2:"null"},open:"$item.calls(_,$1,$2);_=[];",close:"call=$item.calls();_=call._.concat($item.wrap(call,_));"},each:{_default:{$2:"$index, $value"},open:"if($notnull_1){$.each($1a,function($2){with(this){",close:"}});}"},"if":{open:"if(($notnull_1) && $1a){",close:"}"},"else":{_default:{$1:"true"},open:"}else if(($notnull_1) && $1a){"},html:{open:"if($notnull_1){_.push($1a);}"},"=":{_default:{$1:"$data"},open:"if($notnull_1){_.push($.encode($1a));}"},"!":{open:""}},complete:function(){b={}},afterManip:function(f,b,d){var e=b.nodeType===11?a.makeArray(b.childNodes):b.nodeType===1?[b]:[];d.call(f,b);m(e);c++}});function i(e,g,f){var b,c=f?a.map(f,function(a){return typeof a==="string"?e.key?a.replace(/(<\w+)(?=[\s>])(?![^>]*_tmplitem)([^>]*)/g,"$1 "+d+'="'+e.key+'" $2'):a:i(a,e,a._ctnt)}):e;if(g)return c;c=c.join("");c.replace(/^\s*([^<\s][^<]*)?(<[\w\W]+>)([^>]*[^>\s])?\s*$/,function(f,c,e,d){b=a(e).get();m(b);if(c)b=j(c).concat(b);if(d)b=b.concat(j(d))});return b?b:j(c)}function j(c){var b=document.createElement("div");b.innerHTML=c;return a.makeArray(b.childNodes)}function o(b){return new Function("jQuery","$item","var $=jQuery,call,_=[],$data=$item.data;with($data){_.push('"+a.trim(b).replace(/([\\'])/g,"\\$1").replace(/[\r\t\n]/g," ").replace(/\$\{([^\}]*)\}/g,"{{= $1}}").replace(/\{\{(\/?)(\w+|.)(?:\(((?:[^\}]|\}(?!\}))*?)?\))?(?:\s+(.*?)?)?(\(((?:[^\}]|\}(?!\}))*?)\))?\s*\}\}/g,function(m,l,j,d,b,c,e){var i=a.tmpl.tag[j],h,f,g;if(!i)throw"Template command not found: "+j;h=i._default||[];if(c&&!/\w$/.test(b)){b+=c;c=""}if(b){b=k(b);e=e?","+k(e)+")":c?")":"";f=c?b.indexOf(".")>-1?b+c:"("+b+").call($item"+e:b;g=c?f:"(typeof("+b+")==='function'?("+b+").call($item):("+b+"))"}else g=f=h.$1||"null";d=k(d);return"');"+i[l?"close":"open"].split("$notnull_1").join(b?"typeof("+b+")!=='undefined' && ("+b+")!=null":"true").split("$1a").join(g).split("$1").join(f).split("$2").join(d?d.replace(/\s*([^\(]+)\s*(\((.*?)\))?/g,function(d,c,b,a){a=a?","+a+")":b?")":"";return a?"("+c+").call($item"+a:d}):h.$2||"")+"_.push('"})+"');}return _;")}function n(c,b){c._wrap=i(c,true,a.isArray(b)?b:[q.test(b)?b:a(b).html()]).join("")}function k(a){return a?a.replace(/\\'/g,"'").replace(/\\\\/g,"\\"):null}function s(b){var a=document.createElement("div");a.appendChild(b.cloneNode(true));return a.innerHTML}function m(o){var n="_"+c,k,j,l={},e,p,i;for(e=0,p=o.length;e=0;i--)m(j[i]);m(k)}function m(j){var p,i=j,k,e,m;if(m=j.getAttribute(d)){while(i.parentNode&&(i=i.parentNode).nodeType===1&&!(p=i.getAttribute(d)));if(p!==m){i=i.parentNode?i.nodeType===11?0:i.getAttribute(d)||0:0;if(!(e=b[m])){e=f[m];e=g(e,b[i]||f[i],null,true);e.key=++h;b[h]=e}c&&o(m)}j.removeAttribute(d)}else if(c&&(e=a.data(j,"tmplItem"))){o(e.key);b[e.key]=e;i=a.data(j.parentNode,"tmplItem");i=i?i.key:0}if(e){k=e;while(k&&k.key!=i){k.nodes.push(j);k=k.parent}delete e._ctnt;delete e._wrap;a.data(j,"tmplItem",e)}function o(a){a=a+n;e=l[a]=l[a]||g(e,b[e.parent.key+n]||e.parent,null,true)}}}function u(a,d,c,b){if(!a)return l.pop();l.push({_:a,tmpl:d,item:this,data:c,options:b})}function w(d,c,b){return a.tmpl(a.template(d),c,b,this)}function x(b,d){var c=b.options||{};c.wrapped=d;return a.tmpl(a.template(b.tmpl),b.data,c,b.item)}function v(d,c){var b=this._wrap;return a.map(a(a.isArray(b)?b.join(""):b).filter(d||"*"),function(a){return c?a.innerText||a.textContent:a.outerHTML||s(a)})}function t(){var b=this.nodes;a.tmpl(null,null,null,this).insertBefore(b[0]);a(b).remove()}})(jQuery) \ No newline at end of file diff --git a/src/plugins/md_comments/static/js/moment-with-locales.min.js b/src/plugins/md_comments/static/js/moment-with-locales.min.js new file mode 100644 index 000000000..e7320a71e --- /dev/null +++ b/src/plugins/md_comments/static/js/moment-with-locales.min.js @@ -0,0 +1 @@ +!function(e,a){"object"==typeof exports&&"undefined"!=typeof module?module.exports=a():"function"==typeof define&&define.amd?define(a):e.moment=a()}(this,function(){"use strict";var e,n;function l(){return e.apply(null,arguments)}function _(e){return e instanceof Array||"[object Array]"===Object.prototype.toString.call(e)}function i(e){return null!=e&&"[object Object]"===Object.prototype.toString.call(e)}function o(e){return void 0===e}function m(e){return"number"==typeof e||"[object Number]"===Object.prototype.toString.call(e)}function u(e){return e instanceof Date||"[object Date]"===Object.prototype.toString.call(e)}function M(e,a){var t,s=[];for(t=0;t>>0,s=0;sTe(e)?(d=e+1,r=_-Te(e)):(d=e,r=_),{year:d,dayOfYear:r}}function Ie(e,a,t){var s,n,d=Ne(e.year(),a,t),r=Math.floor((e.dayOfYear()-d-1)/7)+1;return r<1?s=r+Ce(n=e.year()-1,a,t):r>Ce(e.year(),a,t)?(s=r-Ce(e.year(),a,t),n=e.year()+1):(n=e.year(),s=r),{week:s,year:n}}function Ce(e,a,t){var s=Ne(e,a,t),n=Ne(e+1,a,t);return(Te(e)-s+n)/7}I("w",["ww",2],"wo","week"),I("W",["WW",2],"Wo","isoWeek"),P("week","w"),P("isoWeek","W"),A("week",5),A("isoWeek",5),ie("w",B),ie("ww",B,V),ie("W",B),ie("WW",B,V),Me(["w","ww","W","WW"],function(e,a,t,s){a[s.substr(0,1)]=g(e)});I("d",0,"do","day"),I("dd",0,0,function(e){return this.localeData().weekdaysMin(this,e)}),I("ddd",0,0,function(e){return this.localeData().weekdaysShort(this,e)}),I("dddd",0,0,function(e){return this.localeData().weekdays(this,e)}),I("e",0,0,"weekday"),I("E",0,0,"isoWeekday"),P("day","d"),P("weekday","e"),P("isoWeekday","E"),A("day",11),A("weekday",11),A("isoWeekday",11),ie("d",B),ie("e",B),ie("E",B),ie("dd",function(e,a){return a.weekdaysMinRegex(e)}),ie("ddd",function(e,a){return a.weekdaysShortRegex(e)}),ie("dddd",function(e,a){return a.weekdaysRegex(e)}),Me(["dd","ddd","dddd"],function(e,a,t,s){var n=t._locale.weekdaysParse(e,s,t._strict);null!=n?a.d=n:Y(t).invalidWeekday=e}),Me(["d","e","E"],function(e,a,t,s){a[s]=g(e)});var Ge="Sunday_Monday_Tuesday_Wednesday_Thursday_Friday_Saturday".split("_");var Ue="Sun_Mon_Tue_Wed_Thu_Fri_Sat".split("_");var Ve="Su_Mo_Tu_We_Th_Fr_Sa".split("_");var Ke=re;var $e=re;var Ze=re;function Be(){function e(e,a){return a.length-e.length}var a,t,s,n,d,r=[],_=[],i=[],o=[];for(a=0;a<7;a++)t=c([2e3,1]).day(a),s=this.weekdaysMin(t,""),n=this.weekdaysShort(t,""),d=this.weekdays(t,""),r.push(s),_.push(n),i.push(d),o.push(s),o.push(n),o.push(d);for(r.sort(e),_.sort(e),i.sort(e),o.sort(e),a=0;a<7;a++)_[a]=me(_[a]),i[a]=me(i[a]),o[a]=me(o[a]);this._weekdaysRegex=new RegExp("^("+o.join("|")+")","i"),this._weekdaysShortRegex=this._weekdaysRegex,this._weekdaysMinRegex=this._weekdaysRegex,this._weekdaysStrictRegex=new RegExp("^("+i.join("|")+")","i"),this._weekdaysShortStrictRegex=new RegExp("^("+_.join("|")+")","i"),this._weekdaysMinStrictRegex=new RegExp("^("+r.join("|")+")","i")}function qe(){return this.hours()%12||12}function Qe(e,a){I(e,0,0,function(){return this.localeData().meridiem(this.hours(),this.minutes(),a)})}function Xe(e,a){return a._meridiemParse}I("H",["HH",2],0,"hour"),I("h",["hh",2],0,qe),I("k",["kk",2],0,function(){return this.hours()||24}),I("hmm",0,0,function(){return""+qe.apply(this)+F(this.minutes(),2)}),I("hmmss",0,0,function(){return""+qe.apply(this)+F(this.minutes(),2)+F(this.seconds(),2)}),I("Hmm",0,0,function(){return""+this.hours()+F(this.minutes(),2)}),I("Hmmss",0,0,function(){return""+this.hours()+F(this.minutes(),2)+F(this.seconds(),2)}),Qe("a",!0),Qe("A",!1),P("hour","h"),A("hour",13),ie("a",Xe),ie("A",Xe),ie("H",B),ie("h",B),ie("k",B),ie("HH",B,V),ie("hh",B,V),ie("kk",B,V),ie("hmm",q),ie("hmmss",Q),ie("Hmm",q),ie("Hmmss",Q),le(["H","HH"],Ye),le(["k","kk"],function(e,a,t){var s=g(e);a[Ye]=24===s?0:s}),le(["a","A"],function(e,a,t){t._isPm=t._locale.isPM(e),t._meridiem=e}),le(["h","hh"],function(e,a,t){a[Ye]=g(e),Y(t).bigHour=!0}),le("hmm",function(e,a,t){var s=e.length-2;a[Ye]=g(e.substr(0,s)),a[ye]=g(e.substr(s)),Y(t).bigHour=!0}),le("hmmss",function(e,a,t){var s=e.length-4,n=e.length-2;a[Ye]=g(e.substr(0,s)),a[ye]=g(e.substr(s,2)),a[fe]=g(e.substr(n)),Y(t).bigHour=!0}),le("Hmm",function(e,a,t){var s=e.length-2;a[Ye]=g(e.substr(0,s)),a[ye]=g(e.substr(s))}),le("Hmmss",function(e,a,t){var s=e.length-4,n=e.length-2;a[Ye]=g(e.substr(0,s)),a[ye]=g(e.substr(s,2)),a[fe]=g(e.substr(n))});var ea,aa=Se("Hours",!0),ta={calendar:{sameDay:"[Today at] LT",nextDay:"[Tomorrow at] LT",nextWeek:"dddd [at] LT",lastDay:"[Yesterday at] LT",lastWeek:"[Last] dddd [at] LT",sameElse:"L"},longDateFormat:{LTS:"h:mm:ss A",LT:"h:mm A",L:"MM/DD/YYYY",LL:"MMMM D, YYYY",LLL:"MMMM D, YYYY h:mm A",LLLL:"dddd, MMMM D, YYYY h:mm A"},invalidDate:"Invalid date",ordinal:"%d",dayOfMonthOrdinalParse:/\d{1,2}/,relativeTime:{future:"in %s",past:"%s ago",s:"a few seconds",ss:"%d seconds",m:"a minute",mm:"%d minutes",h:"an hour",hh:"%d hours",d:"a day",dd:"%d days",M:"a month",MM:"%d months",y:"a year",yy:"%d years"},months:Pe,monthsShort:Oe,week:{dow:0,doy:6},weekdays:Ge,weekdaysMin:Ve,weekdaysShort:Ue,meridiemParse:/[ap]\.?m?\.?/i},sa={},na={};function da(e){return e?e.toLowerCase().replace("_","-"):e}function ra(e){var a=null;if(!sa[e]&&"undefined"!=typeof module&&module&&module.exports)try{a=ea._abbr,require("./locale/"+e),_a(a)}catch(e){}return sa[e]}function _a(e,a){var t;return e&&((t=o(a)?oa(e):ia(e,a))?ea=t:"undefined"!=typeof console&&console.warn&&console.warn("Locale "+e+" not found. Did you forget to load it?")),ea._abbr}function ia(e,a){if(null!==a){var t,s=ta;if(a.abbr=e,null!=sa[e])S("defineLocaleOverride","use moment.updateLocale(localeName, config) to change an existing locale. moment.defineLocale(localeName, config) should only be used for creating a new locale See http://momentjs.com/guides/#/warnings/define-locale/ for more info."),s=sa[e]._config;else if(null!=a.parentLocale)if(null!=sa[a.parentLocale])s=sa[a.parentLocale]._config;else{if(null==(t=ra(a.parentLocale)))return na[a.parentLocale]||(na[a.parentLocale]=[]),na[a.parentLocale].push({name:e,config:a}),null;s=t._config}return sa[e]=new j(b(s,a)),na[e]&&na[e].forEach(function(e){ia(e.name,e.config)}),_a(e),sa[e]}return delete sa[e],null}function oa(e){var a;if(e&&e._locale&&e._locale._abbr&&(e=e._locale._abbr),!e)return ea;if(!_(e)){if(a=ra(e))return a;e=[e]}return function(e){for(var a,t,s,n,d=0;d=a&&r(n,t,!0)>=a-1)break;a--}d++}return ea}(e)}function ma(e){var a,t=e._a;return t&&-2===Y(e).overflow&&(a=t[Le]<0||11je(t[he],t[Le])?ce:t[Ye]<0||24Ce(t,d,r)?Y(e)._overflowWeeks=!0:null!=i?Y(e)._overflowWeekday=!0:(_=Re(t,s,n,d,r),e._a[he]=_.year,e._dayOfYear=_.dayOfYear)}(e),null!=e._dayOfYear&&(d=ua(e._a[he],s[he]),(e._dayOfYear>Te(d)||0===e._dayOfYear)&&(Y(e)._overflowDayOfYear=!0),t=Je(d,0,e._dayOfYear),e._a[Le]=t.getUTCMonth(),e._a[ce]=t.getUTCDate()),a=0;a<3&&null==e._a[a];++a)e._a[a]=r[a]=s[a];for(;a<7;a++)e._a[a]=r[a]=null==e._a[a]?2===a?1:0:e._a[a];24===e._a[Ye]&&0===e._a[ye]&&0===e._a[fe]&&0===e._a[ke]&&(e._nextDay=!0,e._a[Ye]=0),e._d=(e._useUTC?Je:function(e,a,t,s,n,d,r){var _=new Date(e,a,t,s,n,d,r);return e<100&&0<=e&&isFinite(_.getFullYear())&&_.setFullYear(e),_}).apply(null,r),n=e._useUTC?e._d.getUTCDay():e._d.getDay(),null!=e._tzm&&e._d.setUTCMinutes(e._d.getUTCMinutes()-e._tzm),e._nextDay&&(e._a[Ye]=24),e._w&&void 0!==e._w.d&&e._w.d!==n&&(Y(e).weekdayMismatch=!0)}}var Ma=/^\s*((?:[+-]\d{6}|\d{4})-(?:\d\d-\d\d|W\d\d-\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?::\d\d(?::\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,ha=/^\s*((?:[+-]\d{6}|\d{4})(?:\d\d\d\d|W\d\d\d|W\d\d|\d\d\d|\d\d))(?:(T| )(\d\d(?:\d\d(?:\d\d(?:[.,]\d+)?)?)?)([\+\-]\d\d(?::?\d\d)?|\s*Z)?)?$/,La=/Z|[+-]\d\d(?::?\d\d)?/,ca=[["YYYYYY-MM-DD",/[+-]\d{6}-\d\d-\d\d/],["YYYY-MM-DD",/\d{4}-\d\d-\d\d/],["GGGG-[W]WW-E",/\d{4}-W\d\d-\d/],["GGGG-[W]WW",/\d{4}-W\d\d/,!1],["YYYY-DDD",/\d{4}-\d{3}/],["YYYY-MM",/\d{4}-\d\d/,!1],["YYYYYYMMDD",/[+-]\d{10}/],["YYYYMMDD",/\d{8}/],["GGGG[W]WWE",/\d{4}W\d{3}/],["GGGG[W]WW",/\d{4}W\d{2}/,!1],["YYYYDDD",/\d{7}/]],Ya=[["HH:mm:ss.SSSS",/\d\d:\d\d:\d\d\.\d+/],["HH:mm:ss,SSSS",/\d\d:\d\d:\d\d,\d+/],["HH:mm:ss",/\d\d:\d\d:\d\d/],["HH:mm",/\d\d:\d\d/],["HHmmss.SSSS",/\d\d\d\d\d\d\.\d+/],["HHmmss,SSSS",/\d\d\d\d\d\d,\d+/],["HHmmss",/\d\d\d\d\d\d/],["HHmm",/\d\d\d\d/],["HH",/\d\d/]],ya=/^\/?Date\((\-?\d+)/i;function fa(e){var a,t,s,n,d,r,_=e._i,i=Ma.exec(_)||ha.exec(_);if(i){for(Y(e).iso=!0,a=0,t=ca.length;at.valueOf():t.valueOf()this.clone().month(0).utcOffset()||this.utcOffset()>this.clone().month(5).utcOffset()},it.isLocal=function(){return!!this.isValid()&&!this._isUTC},it.isUtcOffset=function(){return!!this.isValid()&&this._isUTC},it.isUtc=Na,it.isUTC=Na,it.zoneAbbr=function(){return this._isUTC?"UTC":""},it.zoneName=function(){return this._isUTC?"Coordinated Universal Time":""},it.dates=t("dates accessor is deprecated. Use date instead.",tt),it.months=t("months accessor is deprecated. Use month instead",Ee),it.years=t("years accessor is deprecated. Use year instead",ve),it.zone=t("moment().zone is deprecated, use moment().utcOffset instead. http://momentjs.com/guides/#/warnings/zone/",function(e,a){return null!=e?("string"!=typeof e&&(e=-e),this.utcOffset(e,a),this):-this.utcOffset()}),it.isDSTShifted=t("isDSTShifted is deprecated. See http://momentjs.com/guides/#/warnings/dst-shifted/ for more information",function(){if(!o(this._isDSTShifted))return this._isDSTShifted;var e={};if(k(e,this),(e=wa(e))._a){var a=e._isUTC?c(e._a):Sa(e._a);this._isDSTShifted=this.isValid()&&0 { + const text = form.find('.comment-content').val(); + const changeFrom = form.find('.from-value').text(); + const changeTo = form.find('.to-value').val() || null; + const comment = {}; + + comment.text = text; + if (changeTo) { + comment.changeFrom = changeFrom; + comment.changeTo = changeTo; + } + + return comment; +}; + +// Callback for new comment Cancel +const cancelNewComment = () => { + hideNewCommentPopup(); +}; + +// Callback for new comment Submit +const submitNewComment = (callback) => { + const index = 0; + const form = $('#newComment'); + const comment = buildCommentFrom(form); + if (comment.text.length > 0 || comment.changeTo && comment.changeTo.length > 0) { + form.find('.comment-content, .to-value').removeClass('error'); + hideNewCommentPopup(); + callback(comment, index); + } else { + if (comment.text.length === 0) form.find('.comment-content').addClass('error'); + if (comment.changeTo && comment.changeTo.length === 0) form.find('.to-value').addClass('error'); + } + return false; +}; + +/* ***** Public methods: ***** */ + +const localizenewCommentPopup = () => { + const newCommentPopup = $('#newComment'); + if (newCommentPopup.length !== 0) commentL10n.localize(newCommentPopup); +}; + +// Insert new Comment Form +const insertNewCommentPopupIfDontExist = (comment, callback) => { + $('#newComment').remove(); + + comment.commentId = ''; + const newCommentPopup = $('#newCommentTemplate').tmpl(comment); + newCommentPopup.appendTo($('#editorcontainerbox')); + + localizenewCommentPopup(); + + // Listen for include suggested change toggle + $('#newComment').find('.suggestion-checkbox').change(function () { + $('#newComment').find('.suggestion').toggle($(this).is(':checked')); + }); + + // Cancel btn + newCommentPopup.find('#comment-reset').on('click', () => { + cancelNewComment(); + }); + // Create btn + $('#newComment').on('submit', (e) => { + e.preventDefault(); + return submitNewComment(callback); + }); + + return newCommentPopup; +}; + +const showNewCommentPopup = (position) => { + // position below comment icon + position = position || []; + let left = position[0]; + if ($('.toolbar .addComment').length) { + left = $('.toolbar .addComment').offset().left; + } + const top = position[1]; + $('#newComment').css('left', left); + if (left === position[0]) { + $('#newComment').css('top', top); + } + // Reset form to make sure it is all clear + $('#newComment').find('.suggestion-checkbox').prop('checked', false).trigger('change'); + $('#newComment').find('textarea').val(''); + $('#newComment').find('.comment-content, .to-value').removeClass('error'); + + // Show popup + $('#newComment').addClass('popup-show'); + + // mark selected text, so it is clear to user which text range the comment is being applied to + pad.plugins.ep_comments.preCommentMarker.markSelectedText(); +}; + +const hideNewCommentPopup = () => { + $('#newComment').removeClass('popup-show'); + + // force focus to be lost, so virtual keyboard is hidden on mobile devices + $('#newComment').find(':focus').blur(); + + // unmark selected text, as now there is no text being commented + pad.plugins.ep_comments.preCommentMarker.unmarkSelectedText(); +}; + +exports.localizenewCommentPopup = localizenewCommentPopup; +exports.insertNewCommentPopupIfDontExist = insertNewCommentPopupIfDontExist; +exports.showNewCommentPopup = showNewCommentPopup; +exports.hideNewCommentPopup = hideNewCommentPopup; diff --git a/src/plugins/md_comments/static/js/preCommentMark.js b/src/plugins/md_comments/static/js/preCommentMark.js new file mode 100644 index 000000000..149bf03a5 --- /dev/null +++ b/src/plugins/md_comments/static/js/preCommentMark.js @@ -0,0 +1,98 @@ +'use strict'; + +exports.MARK_CLASS = 'pre-selected-comment'; + +const PreCommentMarker = function (ace) { + this.ace = ace; + const self = this; + + // do nothing if this feature is not enabled + if (!this.highlightSelectedText()) return; + + // remove any existing marks, as there is no comment being added on plugin initialization + // (we need the timeout to let the plugin be fully initialized before starting to remove + // marked texts) + setTimeout(() => { + self.unmarkSelectedText(); + }, 0); +}; + +// Indicates if MuDoc is configured to highlight text +PreCommentMarker.prototype.highlightSelectedText = function () { + return clientVars.highlightSelectedText; +}; + +PreCommentMarker.prototype.markSelectedText = function () { + // do nothing if this feature is not enabled + if (!this.highlightSelectedText()) return; + + this.ace.callWithAce(doNothing, 'markPreSelectedTextToComment', true); +}; + +PreCommentMarker.prototype.unmarkSelectedText = function () { + // do nothing if this feature is not enabled + if (!this.highlightSelectedText()) return; + + this.ace.callWithAce(doNothing, 'unmarkPreSelectedTextToComment', true); +}; + +PreCommentMarker.prototype.performNonUnduableEvent = function (eventType, callstack, action) { + callstack.startNewEvent('nonundoable'); + action(); + callstack.startNewEvent(eventType); +}; + +PreCommentMarker.prototype.handleMarkText = function (context) { + const editorInfo = context.editorInfo; + const rep = context.rep; + const callstack = context.callstack; + + // first we need to unmark any existing text, otherwise we'll have 2 text ranges marked + this.removeMarks(editorInfo, rep, callstack); + + this.addMark(editorInfo, callstack); +}; + +PreCommentMarker.prototype.handleUnmarkText = function (context) { + const editorInfo = context.editorInfo; + const rep = context.rep; + const callstack = context.callstack; + + this.removeMarks(editorInfo, rep, callstack); +}; + +PreCommentMarker.prototype.addMark = function (editorInfo, callstack) { + const eventType = callstack.editEvent.eventType; + + // we don't want the text marking to be undoable + this.performNonUnduableEvent(eventType, callstack, () => { + editorInfo.ace_setAttributeOnSelection(exports.MARK_CLASS, clientVars.userId); + }); +}; + +PreCommentMarker.prototype.removeMarks = function (editorInfo, rep, callstack) { + const eventType = callstack.editEvent.eventType; + const originalSelStart = rep.selStart; + const originalSelEnd = rep.selEnd; + + // we don't want the text marking to be undoable + this.performNonUnduableEvent(eventType, callstack, () => { + // remove marked text + const padInner = $('iframe[name="ace_outer"]').contents().find('iframe[name="ace_inner"]'); + const selector = `.${exports.MARK_CLASS}`; + const repArr = editorInfo.ace_getRepFromSelector(selector, padInner); + // repArr is an array of reps + $.each(repArr, (index, rep) => { + editorInfo.ace_performSelectionChange(rep[0], rep[1], true); + editorInfo.ace_setAttributeOnSelection(exports.MARK_CLASS, false); + }); + + // make sure selected text is back to original value + editorInfo.ace_performSelectionChange(originalSelStart, originalSelEnd, true); + }); +}; + +// we do nothing on callWithAce; actions will be handled on aceEditEvent +const doNothing = () => {}; + +exports.init = (ace) => new PreCommentMarker(ace); diff --git a/src/plugins/md_comments/static/js/shared.js b/src/plugins/md_comments/static/js/shared.js new file mode 100644 index 000000000..f0897bb4e --- /dev/null +++ b/src/plugins/md_comments/static/js/shared.js @@ -0,0 +1,31 @@ +'use strict'; + +const randomString = require('ep_etherpad-lite/static/js/pad_utils').randomString; + +const collectContentPre = (hookName, context, cb) => { + const comment = /(?:^| )(c-[A-Za-z0-9]*)/.exec(context.cls); + const fakeComment = /(?:^| )(fakecomment-[A-Za-z0-9]*)/.exec(context.cls); + + if (comment && comment[1]) { + context.cc.doAttrib(context.state, `comment::${comment[1]}`); + } + + // a fake comment is a comment copied from this or another pad. To avoid conflicts + // with existing comments, a fake commentId is used, so then we generate a new one + // when the comment is saved + if (fakeComment) { + const mapFakeComments = pad.plugins.ep_comments.getMapfakeComments(); + const fakeCommentId = fakeComment[1]; + const commentId = mapFakeComments[fakeCommentId]; + context.cc.doAttrib(context.state, `comment::${commentId}`); + } + return cb(); +}; + +exports.collectContentPre = collectContentPre; + + +exports.generateCommentId = () => { + const commentId = `c-${randomString(16)}`; + return commentId; +}; diff --git a/src/plugins/md_comments/templates/commentBarButtons.ejs b/src/plugins/md_comments/templates/commentBarButtons.ejs new file mode 100644 index 000000000..9e05e4c39 --- /dev/null +++ b/src/plugins/md_comments/templates/commentBarButtons.ejs @@ -0,0 +1,6 @@ +
  • +
  • + + + +
  • diff --git a/src/plugins/md_comments/templates/commentIcons.html b/src/plugins/md_comments/templates/commentIcons.html new file mode 100644 index 000000000..280c2d190 --- /dev/null +++ b/src/plugins/md_comments/templates/commentIcons.html @@ -0,0 +1,4 @@ + diff --git a/src/plugins/md_comments/templates/comments.html b/src/plugins/md_comments/templates/comments.html new file mode 100644 index 000000000..ba1a88cb8 --- /dev/null +++ b/src/plugins/md_comments/templates/comments.html @@ -0,0 +1,142 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/plugins/md_comments/templates/layout.ejs b/src/plugins/md_comments/templates/layout.ejs new file mode 100644 index 000000000..3ab0eb019 --- /dev/null +++ b/src/plugins/md_comments/templates/layout.ejs @@ -0,0 +1,10 @@ + + + + Comments + + + + <%- body %> + + diff --git a/src/plugins/md_comments/templates/menuButtons.ejs b/src/plugins/md_comments/templates/menuButtons.ejs new file mode 100644 index 000000000..f7a00c438 --- /dev/null +++ b/src/plugins/md_comments/templates/menuButtons.ejs @@ -0,0 +1,3 @@ +
  • + Comment +
  • diff --git a/src/plugins/md_comments/templates/settings.ejs b/src/plugins/md_comments/templates/settings.ejs new file mode 100644 index 000000000..df56526ba --- /dev/null +++ b/src/plugins/md_comments/templates/settings.ejs @@ -0,0 +1,4 @@ +

    + + +

    diff --git a/src/plugins/md_comments/templates/styles.html b/src/plugins/md_comments/templates/styles.html new file mode 100644 index 000000000..b42c798e8 --- /dev/null +++ b/src/plugins/md_comments/templates/styles.html @@ -0,0 +1,2 @@ + + \ No newline at end of file diff --git a/src/plugins/md_font_family/README.md b/src/plugins/md_font_family/README.md new file mode 100644 index 000000000..05414b030 --- /dev/null +++ b/src/plugins/md_font_family/README.md @@ -0,0 +1 @@ +Mu Doc font family plugin \ No newline at end of file diff --git a/src/plugins/md_font_family/ep.json b/src/plugins/md_font_family/ep.json new file mode 100644 index 000000000..382544217 --- /dev/null +++ b/src/plugins/md_font_family/ep.json @@ -0,0 +1,26 @@ +{ + "parts": [ + { + "name": "main", + "client_hooks": { + "postAceInit": "ep_font_family/static/js/index", + "aceRegisterBlockElements": "ep_font_family/static/js/index", + "aceAttribsToClasses": "ep_font_family/static/js/index", + "aceAttribClasses":"ep_font_family/static/js/index", + "collectContentPre": "ep_font_family/static/js/shared", + "collectContentPost": "ep_font_family/static/js/shared", + "aceEditEvent": "ep_font_family/static/js/index", + "aceEditorCSS": "ep_font_family/static/js/index" + }, + "hooks": { + "eejsBlock_editbarMenuLeft": "ep_font_family/index", + "collectContentPre": "ep_font_family/static/js/shared", + "collectContentPost": "ep_font_family/static/js/shared", + "eejsBlock_dd_format":"ep_font_family/index", + "aceAttribClasses":"ep_font_family/index", + "exportHtmlAdditionalTags" : "ep_font_family/index", + "getLineHTMLForExport": "ep_font_family/index" + } + } + ] +} diff --git a/src/plugins/md_font_family/index.js b/src/plugins/md_font_family/index.js new file mode 100644 index 000000000..6a260998a --- /dev/null +++ b/src/plugins/md_font_family/index.js @@ -0,0 +1,71 @@ +'use strict'; + +const fonts = [ + 'fontarial', + 'fontavant-garde', + 'fontbookman', + 'fontcalibri', + 'fontcourier', + 'fontgaramond', + 'fonthelvetica', + 'fontmonospace', + 'fontpalatino', + 'fonttimes-new-roman', +]; + +const eejs = require('ep_etherpad-lite/node/eejs/'); + +/** ****************** +* UI +*/ +exports.eejsBlock_editbarMenuLeft = (hookName, args, cb) => { + args.content += eejs.require('ep_font_family/templates/editbarButtons.ejs'); + return cb(); +}; + +exports.eejsBlock_dd_format = (hookName, args, cb) => { + args.content += eejs.require('ep_font_family/templates/fileMenu.ejs'); + return cb(); +}; + + +/** ****************** +* Editor +*/ + +// Allow to be an attribute +exports.aceAttribClasses = (hookName, attr, cb) => { + for (const i of fonts) { + const font = fonts[i]; + attr[font] = `tag:font${font}`; + } + cb(attr); +}; + +/** ****************** +* Export +*/ + +// Add the props to be supported in export +exports.exportHtmlAdditionalTags = (hook, pad, cb) => { + cb(fonts); +}; + +exports.getLineHTMLForExport = async (hook, context, cb) => { + let lineContent = context.lineContent; + fonts.forEach((font) => { + if (lineContent) { + const fontName = font.substring(4); + lineContent = lineContent.replaceAll(`<${font}`, `\-\&])/g, '\\$&'), (ignore ? 'gi' : 'g')), (typeof (str2) === 'string') ? str2.replace(/\$/g, '$$$$') : str2); +}; diff --git a/src/plugins/md_font_family/locales/de.json b/src/plugins/md_font_family/locales/de.json new file mode 100644 index 000000000..500517b79 --- /dev/null +++ b/src/plugins/md_font_family/locales/de.json @@ -0,0 +1,3 @@ +{ + "ep_font_family.family" : "Schriftart" +} diff --git a/src/plugins/md_font_family/locales/en.json b/src/plugins/md_font_family/locales/en.json new file mode 100644 index 000000000..7f6c689fd --- /dev/null +++ b/src/plugins/md_font_family/locales/en.json @@ -0,0 +1,3 @@ +{ + "ep_font_family.family" : "Font Family" +} diff --git a/src/plugins/md_font_family/locales/fr.json b/src/plugins/md_font_family/locales/fr.json new file mode 100644 index 000000000..065692927 --- /dev/null +++ b/src/plugins/md_font_family/locales/fr.json @@ -0,0 +1,3 @@ +{ + "ep_font_family.family" : "Fontes" +} diff --git a/src/plugins/md_font_family/locales/hu.json b/src/plugins/md_font_family/locales/hu.json new file mode 100644 index 000000000..d81950148 --- /dev/null +++ b/src/plugins/md_font_family/locales/hu.json @@ -0,0 +1,3 @@ +{ + "ep_font_family.family" : "Betűtípuscsalád" +} diff --git a/src/plugins/md_font_family/locales/it.json b/src/plugins/md_font_family/locales/it.json new file mode 100644 index 000000000..082de6dc1 --- /dev/null +++ b/src/plugins/md_font_family/locales/it.json @@ -0,0 +1,3 @@ +{ + "ep_font_family.family" : "Carattere" +} diff --git a/src/plugins/md_font_family/locales/pl.json b/src/plugins/md_font_family/locales/pl.json new file mode 100644 index 000000000..aada5cd0f --- /dev/null +++ b/src/plugins/md_font_family/locales/pl.json @@ -0,0 +1,3 @@ +{ + "ep_font_family.family" : "Czcionka" +} diff --git a/src/plugins/md_font_family/package.json b/src/plugins/md_font_family/package.json new file mode 100644 index 000000000..ba21b6a8e --- /dev/null +++ b/src/plugins/md_font_family/package.json @@ -0,0 +1,68 @@ +{ + "_from": "ep_font_family", + "_id": "ep_font_family@0.0.1", + "_inBundle": false, + "_integrity": "sha512-HC1JN82fHbMvI/3hsfWwgFDW1iF6syj7uS5xmLQN5LlOcz/fsWam32wiz1osHDtNK9knhnl5jRfO1hE5otcOqg==", + "_location": "/ep_font_family", + "_phantomChildren": {}, + "_requested": { + "type": "tag", + "registry": true, + "raw": "ep_font_family", + "name": "ep_font_family", + "escapedName": "ep_font_family", + "rawSpec": "", + "saveSpec": null, + "fetchSpec": "latest" + }, + "_requiredBy": [ + "#USER" + ], + "_resolved": "https://registry.npmjs.org/ep_font_family/-/ep_font_family-0.0.1.tgz", + "_shasum": "ac4d5b67a7c04471029330dcb7e30a18b231a64e", + "_spec": "ep_font_family", + "_where": "/Users/dev/Github/Edumatica/MuDocs/src", + "author": { + "name": "Akhil Naidu", + "email": "kaparapu.akhilnaidu@gmail.com" + }, + "bugs": { + "url": "https://github.com/akhil-naidu/ep_font_family/issues" + }, + "bundleDependencies": false, + "dependencies": {}, + "deprecated": false, + "description": "Add support for different Fonts", + "devDependencies": { + "eslint": "^7.32.0", + "eslint-config-etherpad": "^2.0.2", + "eslint-plugin-cypress": "^2.12.1", + "eslint-plugin-eslint-comments": "^3.2.0", + "eslint-plugin-mocha": "^9.0.0", + "eslint-plugin-node": "^11.1.0", + "eslint-plugin-prefer-arrow": "^1.2.3", + "eslint-plugin-promise": "^5.1.1", + "eslint-plugin-you-dont-need-lodash-underscore": "^6.12.0" + }, + "engines": { + "node": ">=12.13.0" + }, + "eslintConfig": { + "root": true, + "extends": "etherpad/plugin" + }, + "homepage": "https://github.com/akhil-naidu/ep_font_family#readme", + "name": "ep_font_family", + "peerDependencies": { + "ep_etherpad-lite": ">=0.0.2" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/akhil-naidu/ep_font_family.git" + }, + "scripts": { + "lint": "eslint .", + "lint:fix": "eslint --fix ." + }, + "version": "0.0.1" +} diff --git a/src/plugins/md_font_family/static/css/fonts.css b/src/plugins/md_font_family/static/css/fonts.css new file mode 100644 index 000000000..5b2fe1164 --- /dev/null +++ b/src/plugins/md_font_family/static/css/fonts.css @@ -0,0 +1,33 @@ +fontarial{ + font-family: arial; +} +fontavant-garde{ + font-family: Avant Garde,Avantgarde,Century Gothic,CenturyGothic,AppleGothic,sans-serif; +} +fontbookman{ + font-family: bookman, Bookman Old Style; +} +fontcalibri{ + font-family: Calibri,Candara,Segoe,Segoe UI,Optima,Arial,sans-serif; +} +fontcourier{ + font-family: courier; +} +fontgaramond{ + font-family: Garamond,Baskerville,Baskerville Old Face,Hoefler Text,Times New Roman,serif; +} +fonthelvetica{ + font-family: helvetica; +} +fontmonospace{ + font-family: monospace; +} +fontpalatino{ + font-family: palatino; +} +fonttimes-new-roman{ + font-family: times new roman; +} +*{ + font-family:inherit +} diff --git a/src/plugins/md_font_family/static/js/index.js b/src/plugins/md_font_family/static/js/index.js new file mode 100644 index 000000000..dec8f327a --- /dev/null +++ b/src/plugins/md_font_family/static/js/index.js @@ -0,0 +1,107 @@ +'use strict'; + +const fonts = [ + 'fontarial', + 'fontavant-garde', + 'fontbookman', + 'fontcalibri', + 'fontcourier', + 'fontgaramond', + 'fonthelvetica', + 'fontmonospace', + 'fontpalatino', + 'fonttimes-new-roman', +]; + +/** *** +* Basic setup +******/ + +// Bind the event handler to the toolbar buttons +exports.postAceInit = (hook, context) => { + const fontFamily = $('select.family-selection'); + $.each(fonts, (k, font) => { + font = font.substring(4); + let fontString = capitaliseFirstLetter(font); + fontString = fontString.split('-').join(' '); + fontFamily.append($('
    +
    --> diff --git a/src/plugins/md_wordcount/templates/stats_entry.ejs b/src/plugins/md_wordcount/templates/stats_entry.ejs new file mode 100644 index 000000000..f1a49a604 --- /dev/null +++ b/src/plugins/md_wordcount/templates/stats_entry.ejs @@ -0,0 +1,4 @@ +

    + + +

    diff --git a/src/static/css/pad/popup.css b/src/static/css/pad/popup.css index 6de108e40..db927a62e 100644 --- a/src/static/css/pad/popup.css +++ b/src/static/css/pad/popup.css @@ -15,7 +15,7 @@ .popup { position: absolute; - top: 10px; + /* top: 10px; */ right: 30px; visibility: hidden; z-index: 500; diff --git a/src/static/skins/colibris/src/components/chat.css b/src/static/skins/colibris/src/components/chat.css index 4e92342cf..46da94188 100644 --- a/src/static/skins/colibris/src/components/chat.css +++ b/src/static/skins/colibris/src/components/chat.css @@ -23,6 +23,7 @@ color: #485365; color: var(--text-color); right: 30px; + position: fixed; } #chatbox.stickyChat .chat-content { diff --git a/src/static/skins/colibris/src/pad-variants.css b/src/static/skins/colibris/src/pad-variants.css index 1b335d3ab..87919ac48 100644 --- a/src/static/skins/colibris/src/pad-variants.css +++ b/src/static/skins/colibris/src/pad-variants.css @@ -1,12 +1,54 @@ /* =========================== */ /* === Super Light Toolbar === */ /* =========================== */ -.super-light-toolbar .toolbar, .super-light-toolbar .popup-content, .super-light-toolbar #pad_title { +.super-light-toolbar .toolbar,.super-light-toolbar #pad_title { --text-color: var(--super-dark-color); --text-soft-color: var(--dark-color); --border-color: #e4e6e9; --bg-soft-color: var(--light-color); --bg-color: var(--super-light-color); + background-color: #f2f3f4; + justify-content: center; + margin-top: 20px; + margin-bottom: 20px; +} + +#editbar.editor-scrolled { + border-bottom: 0px solid #d2d2d2; +} + +.toolbar ul.menu_left { + padding-left: 10px; + padding-right: 10px; + background-color: white; + border-radius: 8px; +} + +.toolbar ul.menu_right { + flex-shrink: 0; + position: fixed; + bottom: 0; + right: 0; + left: 0; + border-top: 0 solid #ccc; + background-color: white; + padding: 0 5px 5px 5px; +} + +.super-light-toolbar .popup-content{ + --text-color: var(--super-dark-color); + --text-soft-color: var(--dark-color); + --border-color: #e4e6e9; + --bg-soft-color: var(--light-color); + --bg-color: var(--super-light-color); + +} +.popup { + position: absolute; + bottom: 10px; + left: 30px; + visibility: hidden; + z-index: 500; } /* ===================== */ /* === Light Toolbar === */ @@ -42,11 +84,6 @@ --bg-soft-color: var(--super-dark-color); --bg-color: var(--dark-color); } - - - - - /* ============================ */ /* == Super Light Background == */ /* ============================ */ @@ -57,6 +94,7 @@ --border-color: #e4e6e9; --bg-soft-color: var(--light-color); --bg-color: var(--super-light-color); + } .super-light-background body, .full-width-editor.super-light-editor body:not(.comments-active) { --scrollbar-bg: var(--super-light-color); @@ -69,18 +107,60 @@ /* ====================== */ /* == Light Background == */ /* ====================== */ -.light-background #editorcontainerbox, .light-background #sidediv, -.light-background #chatbox, .light-background #outerdocbody, .light-background { +#editorcontainerbox { --text-color: var(--super-dark-color); --text-soft-color: var(--dark-color); --border-color: var(--middle-color); --bg-soft-color: var(--super-light-color); --bg-color: var(--light-color); + margin-bottom: 48px; + margin-top: 0px; + padding-top: 0px; + position: relative; } + +#outerdocbody{ + padding-top: 0; +} + +#editorcontainerbox iframe { + width: 100%; + max-height: 99%; + height: 99%; +} + +.chat-content { + border: 1px solid #f2f3f4; + border-radius: 8px; +} + +.popup#users.chatAndUsers > .popup-content { + border: none; + border-bottom: 0px solid #ccc; + border-left: 0px solid #ccc; + border-right: 0; + border-radius: 0; + box-shadow: none; + height: 200px; + min-width: 0; + padding: 10px; + margin-right: 10px; + margin-bottom: 10px; + border-radius: 8px; +} + +.light-background #chatbox{ + margin-bottom: 10px; + border-radius: 8px; + margin-right: 10px; + overflow: hidden; +} + .light-background body, .full-width-editor.light-editor body:not(.comments-active) { --scrollbar-bg: var(--light-color); --scrollbar-track: var(--super-light-color); --scrollbar-thumb: var(--dark-color); + background-color: #f2f3f4; } .light-background .compact-display-content { background-color: var(--light-color); @@ -142,6 +222,7 @@ /* ======================== */ .super-light-editor #outerdocbody iframe, .super-light-editor #outerdocbody > #innerdocbody { --bg-color: var(--super-light-color); + border-radius: 8px; } .super-light-editor #innerdocbody { --text-color: var(--super-dark-color); @@ -225,4 +306,9 @@ .full-width-editor ::-webkit-scrollbar-track, .full-width-editor ::-webkit-scrollbar-thumb { border-radius: 0px; +} + +.mobile-layout #users.popup { + right: 0rem; + left: auto; } \ No newline at end of file diff --git a/src/static/skins/colibris/src/plugins/comments.css b/src/static/skins/colibris/src/plugins/comments.css index 005890ef4..9b791c434 100644 --- a/src/static/skins/colibris/src/plugins/comments.css +++ b/src/static/skins/colibris/src/plugins/comments.css @@ -8,6 +8,13 @@ background-color: #576273; background-color: var(--text-soft-color); } + +.full-display-content .comment-title-wrapper, +.full-display-content .comment-reply { + padding: 10px; + background-color: #ffffff; +} + .sidebar-comment .suggestion-create { margin-top: 10px; } @@ -29,6 +36,12 @@ .comment-actions-wrapper .comment-edit { margin-right: 5px; } + +.comment-actions-wrapper { + float: right; + display: flex; +} + [type="checkbox"] + label.label-suggestion-checkbox { margin-left: 5px; padding-left: 2.4rem; @@ -38,8 +51,8 @@ box-shadow: none; background-color: #f2f3f4; background-color: var(--bg-soft-color); - border: 1px solid #ffffff; - border: 1px solid var(--bg-color); + border: 1px solid #f2f3f4; + /* border: 1px solid var(--bg-color); */ } .comment-reply { border-top: 1px solid #ffffff; @@ -47,9 +60,15 @@ background-color: inherit; } .comment-reply textarea, .comment-reply input[type="text"] { - background-color: #ffffff; + background-color: #f2f3f4; background-color: var(--bg-color); } + +.comment-reply input[type=text], .comment-reply textarea { + background-color: #f2f3f4; + /* background-color: var(--bg-color); */ +} + .btn.revert-suggestion-btn { padding-left: 0; } @@ -79,6 +98,7 @@ .comment-modal .full-display-content .comment-title-wrapper, .comment-modal .full-display-content .comment-reply { padding: 15px; + background-color: #ffffff; } @@ -105,8 +125,9 @@ } .sidebar-comment .full-display-content { margin-left: 10px; + border: 1px } .compact-display-content { padding-left: 20px; } -} \ No newline at end of file +} diff --git a/src/templates/pad.html b/src/templates/pad.html index bc3cec88e..0d85a6c1c 100644 --- a/src/templates/pad.html +++ b/src/templates/pad.html @@ -308,6 +308,40 @@ <% e.end_block(); %>
    + + + + + + @@ -505,4 +539,4 @@ <% e.end_block(); %> - + \ No newline at end of file