diff --git a/package-lock.json b/package-lock.json index f62f2e3bb5..f20cdb008a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,7 +44,7 @@ "gulp-filter": "^7.0.0", "gulp-imagemin": "^7.1.0", "gulp.spritesmith": "^6.13.0", - "habitica-markdown": "^3.0.0", + "habitica-markdown": "github:HabitRPG/habitica-markdown#fiz/emojis-update", "heapdump": "^0.3.15", "helmet": "^4.6.0", "in-app-purchase": "^1.11.3", @@ -105,6 +105,9 @@ "npm": "^10" } }, + "../habitica-markdown/habitica-markdown": { + "extraneous": true + }, "node_modules/@aashutoshrathi/word-wrap": { "version": "1.2.6", "resolved": "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz", @@ -4167,6 +4170,12 @@ "node": ">=14.0.0" } }, + "node_modules/apidoc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, "node_modules/apidoc/node_modules/bootstrap": { "version": "3.4.1", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-3.4.1.tgz", @@ -4202,6 +4211,15 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/apidoc/node_modules/linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "license": "MIT", + "dependencies": { + "uc.micro": "^1.0.1" + } + }, "node_modules/apidoc/node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -4213,6 +4231,22 @@ "node": ">=10" } }, + "node_modules/apidoc/node_modules/markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, "node_modules/apidoc/node_modules/nodemon": { "version": "2.0.22", "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-2.0.22.tgz", @@ -12497,50 +12531,14 @@ } }, "node_modules/habitica-markdown": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/habitica-markdown/-/habitica-markdown-3.0.0.tgz", - "integrity": "sha512-rw1LJ5Vsjx8sfjNa4e2wFuZf5eqqyb5/kfZXPxqfMMgJCCgIhWStDqY3nIclnpGWpemlKd+qbdh2rLiLgm9kng==", + "version": "4.0.0", + "resolved": "git+ssh://git@github.com/HabitRPG/habitica-markdown.git#204545c1e028f22b937c0a73c5ef250c8973db16", + "license": "GPL-3.0", "dependencies": { - "habitica-markdown-emoji": "1.2.4", - "markdown-it": "10.0.0", - "markdown-it-link-attributes": "3.0.0", - "markdown-it-linkify-images": "^1.1.1" - } - }, - "node_modules/habitica-markdown-emoji": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/habitica-markdown-emoji/-/habitica-markdown-emoji-1.2.4.tgz", - "integrity": "sha512-UV0AxpDToldFQULuhTxC1y4sdNTApaIOh7ZuV/92HCPmCGkv3DAlHtYE67OmCqLVfs26HWAGVJaU3+OEnW3gjg==", - "dependencies": { - "markdown-it-emoji": "^1.1.1" - } - }, - "node_modules/habitica-markdown/node_modules/entities": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz", - "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==" - }, - "node_modules/habitica-markdown/node_modules/linkify-it": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz", - "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==", - "dependencies": { - "uc.micro": "^1.0.1" - } - }, - "node_modules/habitica-markdown/node_modules/markdown-it": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-10.0.0.tgz", - "integrity": "sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg==", - "dependencies": { - "argparse": "^1.0.7", - "entities": "~2.0.0", - "linkify-it": "^2.0.0", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" - }, - "bin": { - "markdown-it": "bin/markdown-it.js" + "markdown-it": "^14.0.0", + "markdown-it-emoji": "^2.0.2", + "markdown-it-link-attributes": "^4.0.1", + "markdown-it-linkify-images": "^3.0.0" } }, "node_modules/handlebars": { @@ -14552,13 +14550,20 @@ "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" }, "node_modules/linkify-it": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", - "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "license": "MIT", "dependencies": { - "uc.micro": "^1.0.1" + "uc.micro": "^2.0.0" } }, + "node_modules/linkify-it/node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" + }, "node_modules/load-json-file": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", @@ -14906,59 +14911,79 @@ } }, "node_modules/markdown-it": { - "version": "12.3.2", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", - "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "license": "MIT", "dependencies": { "argparse": "^2.0.1", - "entities": "~2.1.0", - "linkify-it": "^3.0.1", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" }, "bin": { - "markdown-it": "bin/markdown-it.js" + "markdown-it": "bin/markdown-it.mjs" } }, "node_modules/markdown-it-emoji": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/markdown-it-emoji/-/markdown-it-emoji-1.4.0.tgz", - "integrity": "sha512-QCz3Hkd+r5gDYtS2xsFXmBYrgw6KuWcJZLCEkdfAuwzZbShCmCfta+hwAMq4NX/4xPzkSHduMKgMkkPUJxSXNg==" + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/markdown-it-emoji/-/markdown-it-emoji-2.0.2.tgz", + "integrity": "sha512-zLftSaNrKuYl0kR5zm4gxXjHaOI3FAOEaloKmRA5hijmJZvSjmxcokOLlzycb/HXlUFWzXqpIEoyEMCE4i9MvQ==", + "license": "MIT" }, "node_modules/markdown-it-link-attributes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/markdown-it-link-attributes/-/markdown-it-link-attributes-3.0.0.tgz", - "integrity": "sha512-B34ySxVeo6MuEGSPCWyIYryuXINOvngNZL87Mp7YYfKIf6DcD837+lXA8mo6EBbauKsnGz22ZH0zsbOiQRWTNg==" + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/markdown-it-link-attributes/-/markdown-it-link-attributes-4.0.1.tgz", + "integrity": "sha512-pg5OK0jPLg62H4k7M9mRJLT61gUp9nvG0XveKYHMOOluASo9OEF13WlXrpAp2aj35LbedAy3QOCgQCw0tkLKAQ==", + "license": "MIT" }, "node_modules/markdown-it-linkify-images": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/markdown-it-linkify-images/-/markdown-it-linkify-images-1.1.1.tgz", - "integrity": "sha512-1IEmAaAjIgAwY+tZI0sxDXdy9QKHutj5cN0lH2JBiSZt+2NYKrWRJj0cloQW3OFIfP2MLFA1E+6OLJhXPiLgNw==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/markdown-it-linkify-images/-/markdown-it-linkify-images-3.0.0.tgz", + "integrity": "sha512-Vs5yGJa5MWjFgytzgtn8c1U6RcStj3FZKhhx459U8dYbEE5FTWZ6mMRkYMiDlkFO0j4VCsQT1LT557bY0ETgtg==", + "license": "MIT", "dependencies": { - "markdown-it": "^8.4.2" + "markdown-it": "^13.0.1" } }, + "node_modules/markdown-it-linkify-images/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, "node_modules/markdown-it-linkify-images/node_modules/entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==" + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-3.0.1.tgz", + "integrity": "sha512-WiyBqoomrwMdFG1e0kqvASYfnlb0lp8M5o5Fw2OFq1hNZxxcNk8Ik0Xm7LxzBhuidnZB/UtBqVCgUz3kBOP51Q==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } }, "node_modules/markdown-it-linkify-images/node_modules/linkify-it": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz", - "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-4.0.1.tgz", + "integrity": "sha512-C7bfi1UZmoj8+PQx22XyeXCuBlokoyWQL5pWSP+EI6nzRylyThouddufc2c1NDIcP9k5agmN9fLpA7VNJfIiqw==", + "license": "MIT", "dependencies": { "uc.micro": "^1.0.1" } }, "node_modules/markdown-it-linkify-images/node_modules/markdown-it": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-8.4.2.tgz", - "integrity": "sha512-GcRz3AWTqSUphY3vsUqQSFMbgR38a4Lh3GWlHRh/7MRwz8mcu9n2IO7HOh+bXHrR9kOPDl5RNCaEsrneb+xhHQ==", + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-13.0.2.tgz", + "integrity": "sha512-FtwnEuuK+2yVU7goGn/MJ0WBZMM9ZPgU9spqlFs7/A/pDIUNSOQZhUgOqYCficIuR2QaFnrt8LHqBWsbTAoI5w==", + "license": "MIT", "dependencies": { - "argparse": "^1.0.7", - "entities": "~1.1.1", - "linkify-it": "^2.0.0", + "argparse": "^2.0.1", + "entities": "~3.0.1", + "linkify-it": "^4.0.1", "mdurl": "^1.0.1", "uc.micro": "^1.0.5" }, @@ -14969,7 +14994,32 @@ "node_modules/markdown-it/node_modules/argparse": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==" + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/markdown-it/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/markdown-it/node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "license": "MIT" + }, + "node_modules/markdown-it/node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "license": "MIT" }, "node_modules/matchdep": { "version": "2.0.0", @@ -18049,6 +18099,15 @@ "node": ">=6" } }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/q": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", diff --git a/package.json b/package.json index 4ff6435850..19be89e0e8 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "gulp-filter": "^7.0.0", "gulp-imagemin": "^7.1.0", "gulp.spritesmith": "^6.13.0", - "habitica-markdown": "^3.0.0", + "habitica-markdown": "^4.0.0", "heapdump": "^0.3.15", "helmet": "^4.6.0", "in-app-purchase": "^1.11.3", diff --git a/test/api/v3/integration/dataexport/GET-export_inbox.html.test.js b/test/api/v3/integration/dataexport/GET-export_inbox.html.test.js index b9f9b96fff..d153828688 100644 --- a/test/api/v3/integration/dataexport/GET-export_inbox.html.test.js +++ b/test/api/v3/integration/dataexport/GET-export_inbox.html.test.js @@ -38,7 +38,7 @@ describe('GET /export/inbox.html', () => { it('renders the markdown messages as html', async () => { const res = await user.get('/export/inbox.html'); - expect(res).to.include('img class="habitica-emoji"'); + expect(res).to.include('😄'); expect(res).to.include('

Hello!

'); expect(res).to.include('
  • list 1
  • '); }); @@ -46,7 +46,7 @@ describe('GET /export/inbox.html', () => { it('sorts messages from newest to oldest', async () => { const res = await user.get('/export/inbox.html'); - const emojiPosition = res.indexOf('img class="habitica-emoji"'); + const emojiPosition = res.indexOf('😄'); const headingPosition = res.indexOf('

    Hello!

    '); const listPosition = res.indexOf('
  • list 1
  • '); diff --git a/test/common/libs/habiticaMarkdown.test.js b/test/common/libs/habiticaMarkdown.test.js new file mode 100644 index 0000000000..2ee91da77b --- /dev/null +++ b/test/common/libs/habiticaMarkdown.test.js @@ -0,0 +1,67 @@ +import md from 'habitica-markdown'; + +describe('habiticaMarkdown emoji plugin', () => { + it('renders standard emoji as Unicode', () => { + const result = md.render(':smile:'); + expect(result).to.include('😄'); + expect(result).not.to.include('img'); + }); + + it('renders thumbsup emoji as Unicode', () => { + const result = md.render(':thumbsup:'); + expect(result).to.include('👍'); + }); + + it('renders +1 emoji as Unicode', () => { + const result = md.render(':+1:'); + expect(result).to.include('👍'); + }); + + it('renders melior as an img tag', () => { + const result = md.render(':melior:'); + expect(result).to.include(' { + const result = md.render('[:smile: link](http://example.com)'); + expect(result).to.include(':smile: link'); + expect(result).not.to.include('😄'); + }); + + it('converts emoji outside of links normally', () => { + const result = md.render(':smile: [link](http://example.com)'); + expect(result).to.include('😄'); + expect(result).to.include('link'); + }); + + it('leaves removed custom emoji (bowtie) as literal text', () => { + const result = md.render(':bowtie:'); + expect(result).to.include(':bowtie:'); + expect(result).not.to.include('img'); + }); + + it('leaves unknown shortcodes as literal text', () => { + const result = md.render(':nonexistent_emoji_xyz:'); + expect(result).to.include(':nonexistent_emoji_xyz:'); + }); + + it('renders new emoji not in the old dataset', () => { + const result = md.render(':yawning_face:'); + expect(result).to.include('🥱'); + }); + + it('supports unsafeHTMLRender', () => { + const result = md.unsafeHTMLRender('bold :smile:'); + expect(result).to.include('bold'); + expect(result).to.include('😄'); + }); + + it('supports renderWithMentions', () => { + const result = md.renderWithMentions(':smile: @testuser', { userName: 'testuser' }); + expect(result).to.include('😄'); + expect(result).to.include('at-text'); + expect(result).to.include('at-highlight'); + }); +}); diff --git a/website/client/package.json b/website/client/package.json index 1cff12c077..0703d588b2 100644 --- a/website/client/package.json +++ b/website/client/package.json @@ -28,7 +28,7 @@ "eslint-config-habitrpg": "6.2.0", "eslint-plugin-mocha": "5.3.0", "eslint-plugin-vue": "7.20.0", - "habitica-markdown": "^3.0.0", + "habitica-markdown": "^4.0.0", "hellojs": "^1.20.0", "intro.js": "^7.2.0", "jquery": "^3.7.1", diff --git a/website/client/src/assets/scss/markdown.scss b/website/client/src/assets/scss/markdown.scss index 19f9e45bc1..de59f1237e 100644 --- a/website/client/src/assets/scss/markdown.scss +++ b/website/client/src/assets/scss/markdown.scss @@ -58,6 +58,11 @@ h3.markdown { img { max-width: 100%; } + + .emoji-native { + font-size: 0.85em; + vertical-align: middle; + } blockquote { padding: 0 16px; diff --git a/website/client/src/components/challenges/challengeModal.vue b/website/client/src/components/challenges/challengeModal.vue index abf98b0b2c..1381771bef 100644 --- a/website/client/src/components/challenges/challengeModal.vue +++ b/website/client/src/components/challenges/challengeModal.vue @@ -12,23 +12,39 @@ - + @focus="setActiveField('name')" + @keydown="onFieldKeydown($event)" + @keydown.tab="autoCompleteMixinHandleTab($event)" + @keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)" + @keydown.down="autoCompleteMixinSelectNextAutocomplete($event)" + @keypress.enter="autoCompleteMixinSelectAutocomplete($event)" + @keydown.esc="autoCompleteMixinHandleEscape($event)" + >
    - + @focus="setActiveField('shortName')" + @keydown="onFieldKeydown($event)" + @keydown.tab="autoCompleteMixinHandleTab($event)" + @keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)" + @keydown.down="autoCompleteMixinSelectNextAutocomplete($event)" + @keypress.enter="autoCompleteMixinSelectAutocomplete($event)" + @keydown.esc="autoCompleteMixinHandleEscape($event)" + >
    @@ -55,11 +78,26 @@ class="float-right" > +
    { + if (this.textbox) { + this.textbox.setSelectionRange(newCaret, newCaret); + this.textbox.focus(); + } + }); + }, enableSubmit: throttle(function enableSubmit () { /* Enables the submit button if it was disabled */ if (this.loading) { diff --git a/website/client/src/components/chat/emojiAutoComplete.vue b/website/client/src/components/chat/emojiAutoComplete.vue new file mode 100644 index 0000000000..f58aa8d211 --- /dev/null +++ b/website/client/src/components/chat/emojiAutoComplete.vue @@ -0,0 +1,281 @@ + + + + + diff --git a/website/client/src/components/groups/chat.vue b/website/client/src/components/groups/chat.vue index d1b0e7ae4d..2d242863bc 100644 --- a/website/client/src/components/groups/chat.vue +++ b/website/client/src/components/groups/chat.vue @@ -41,6 +41,14 @@ :chat="group.chat" @select="selectedAutocomplete" /> +
    @@ -90,6 +98,7 @@ import { MAX_MESSAGE_LENGTH } from '@/../../common/script/constants'; import externalLinks from '../../mixins/externalLinks'; import autocomplete from '../chat/autoComplete'; +import emojiAutoComplete from '../chat/emojiAutoComplete'; import communityGuidelines from './communityGuidelines'; import chatMessages from '../chat/chatMessages'; import { mapState } from '@/libs/store'; @@ -102,6 +111,7 @@ export default { }, components: { autocomplete, + emojiAutoComplete, communityGuidelines, chatMessages, }, diff --git a/website/client/src/components/groups/group.vue b/website/client/src/components/groups/group.vue index a70fe8b90d..044acd7c9d 100644 --- a/website/client/src/components/groups/group.vue +++ b/website/client/src/components/groups/group.vue @@ -25,53 +25,61 @@
    -
    -
    -
    - {{ group.memberCount | abbrNum }} -
    - {{ $t('memberList') }} +
    +
    +
    +
    +
    + {{ group.memberCount | abbrNum }} +
    +
    + {{ $t('memberList') }} +
    -
    - {{ group.balance * 4 }} -
    - {{ $t('guildBank') }} +
    +
    +
    + {{ group.balance * 4 }} +
    +
    + {{ $t('guildBank') }} +
    @@ -128,35 +136,57 @@ } .item-with-icon { + display: inline-block; border-radius: 2px; - background-color: #ffffff; + background-color: $white; box-shadow: 0 2px 2px 0 rgba(26, 24, 29, 0.16), 0 1px 4px 0 rgba(26, 24, 29, 0.12); - padding: 1em; - text-align: center; - min-width: 120px; + margin-left: 1em; + width: 120px; height: 76px; - margin-right: 1rem; + text-align: center; + font-size: 20px; + vertical-align: bottom; + overflow: hidden; + position: relative; - &:last-of-type { - margin-left: 0.5rem; + .box-content { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + height: 100%; + width: 100%; } - .svg-icon.shield, .svg-icon.gem { - width: 28px; - height: auto; - margin: 0 auto; + .icon-number-row { + display: flex; + align-items: center; + justify-content: center; + margin-bottom: 0.1em; + + .number { + font-size: 18px; + font-weight: normal; + margin-left: 0.2em; + } + } + + .svg-icon { + width: 24px; + height: 24px; display: inline-block; vertical-align: bottom; - margin-right: 0.5em; } - .number { - font-size: 22px; - font-weight: bold; - } - - .label { - margin-top: .5em; + .details { + font-size: 11px; + color: $gray-200; + width: 100%; + padding: 0 4px; + line-height: 1.1; + word-break: break-word; + max-height: 2.2em; + overflow: visible; } } @@ -215,11 +245,6 @@ .icon-row { margin-top: 1em; justify-content: flex-end; - - .number { - font-size: 22px; - font-weight: bold; - } } .chat-row { diff --git a/website/client/src/components/tasks/modal-controls/checklist.vue b/website/client/src/components/tasks/modal-controls/checklist.vue index a82a6c9a63..35f5370df2 100644 --- a/website/client/src/components/tasks/modal-controls/checklist.vue +++ b/website/client/src/components/tasks/modal-controls/checklist.vue @@ -48,11 +48,19 @@ />
    +
    @@ -105,6 +128,8 @@ import chevronIcon from '@/assets/svg/chevron.svg?raw'; import gripIcon from '@/assets/svg/grip.svg?raw'; import checkbox from '@/components/ui/checkbox'; import lockableLabel from './lockableLabel'; +import emojiAutoComplete from '@/components/chat/emojiAutoComplete'; +import { autoCompleteHelperMixin } from '@/mixins/autoCompleteHelper'; export default { name: 'Checklist', @@ -112,7 +137,9 @@ export default { checkbox, draggable, lockableLabel, + emojiAutoComplete, }, + mixins: [autoCompleteHelperMixin], props: { disabled: { type: Boolean, @@ -133,6 +160,8 @@ export default { showChecklist: true, hasPossibilityOfIMEConversion: true, newChecklistItem: null, + textbox: null, + activeItemIndex: -1, icons: Object.freeze({ positive: positiveIcon, destroy: deleteIcon, @@ -141,6 +170,15 @@ export default { }), }; }, + computed: { + activeFieldText () { + if (this.activeItemIndex === -1) { + return this.newChecklistItem || ''; + } + const item = this.checklist[this.activeItemIndex]; + return item ? item.text || '' : ''; + }, + }, methods: { summaryClass (item) { if (!this.disableEdit) return ''; @@ -179,6 +217,40 @@ export default { this.checklist.splice(i, 1); this.updateChecklist(); }, + setActiveItem (index) { + this.activeItemIndex = index; + if (index === -1) { + this.textbox = this.$refs.newChecklistInput; + } else { + const refArr = this.$refs[`checklistItem-${index}`]; + this.textbox = refArr ? refArr[0] || refArr : null; + } + }, + newChecklistEnterHandler (e) { + const ac = this._getActiveAutocomplete(); + if (ac && ac.selected !== null) { + e.preventDefault(); + ac.makeSelection(); + } else if (ac) { + ac.cancel(); + this.setHasPossibilityOfIMEConversion(false); + } else { + this.setHasPossibilityOfIMEConversion(false); + } + }, + selectedAutocomplete (newText, newCaret) { + if (this.activeItemIndex === -1) { + this.newChecklistItem = newText; + } else { + this.checklist[this.activeItemIndex].text = newText; + } + this.$nextTick(() => { + if (this.textbox) { + this.textbox.setSelectionRange(newCaret, newCaret); + this.textbox.focus(); + } + }); + }, }, }; @@ -187,6 +259,7 @@ export default { @import '@/assets/scss/colors.scss'; .checklist-component { + position: relative; .chevron-flip { transform: translateY(-5px) rotate(180deg); diff --git a/website/client/src/components/tasks/modal-controls/selectMulti.vue b/website/client/src/components/tasks/modal-controls/selectMulti.vue index 922ef7d151..592b28398e 100644 --- a/website/client/src/components/tasks/modal-controls/selectMulti.vue +++ b/website/client/src/components/tasks/modal-controls/selectMulti.vue @@ -9,12 +9,27 @@ @toggle="openOrClose($event)" > -
    +
    +
    @@ -94,6 +109,10 @@ $itemHeight: 2rem; } .select-multi { + .search-input-wrapper { + position: relative; + } + .dropdown-toggle { padding-left: 0.75rem; } @@ -185,6 +204,8 @@ $itemHeight: 2rem; import Vue from 'vue'; import MultiList from '@/components/tasks/modal-controls/multiList'; import markdownDirective from '@/directives/markdown'; +import emojiAutoComplete from '@/components/chat/emojiAutoComplete'; +import { autoCompleteHelperMixin } from '@/mixins/autoCompleteHelper'; export default { directives: { @@ -192,7 +213,9 @@ export default { }, components: { MultiList, + emojiAutoComplete, }, + mixins: [autoCompleteHelperMixin], props: { addNew: { type: Boolean, @@ -221,6 +244,7 @@ export default { wasTagAdded: false, selected: this.selectedItems, search: '', + textbox: null, }; }, computed: { @@ -312,6 +336,38 @@ export default { this.closeSelectPopup(); } }, + setTextbox () { + const ref = this.$refs.searchInput; + this.textbox = ref ? (ref.$el || ref) : null; + }, + searchEnterHandler (e) { + const ac = this._getActiveAutocomplete(); + if (ac && ac.selected !== null) { + e.preventDefault(); + e.stopPropagation(); + ac.makeSelection(); + } else { + if (ac) ac.cancel(); + this.handleSubmit(); + } + }, + searchEscHandler (e) { + const ac = this._getActiveAutocomplete(); + if (ac && ac.searchActive) { + e.preventDefault(); + e.stopPropagation(); + ac.cancel(); + } + }, + selectedAutocomplete (newText, newCaret) { + this.search = newText; + this.$nextTick(() => { + if (this.textbox) { + this.textbox.setSelectionRange(newCaret, newCaret); + this.textbox.focus(); + } + }); + }, handleSubmit () { if (!this.addNew) return; const { search } = this; diff --git a/website/client/src/components/tasks/taskModal.vue b/website/client/src/components/tasks/taskModal.vue index b2f17bf5bb..7117cbf927 100644 --- a/website/client/src/components/tasks/taskModal.vue +++ b/website/client/src/components/tasks/taskModal.vue @@ -70,6 +70,13 @@ spellcheck="true" :disabled="challengeAccessRequired" :placeholder="$t('addATitle')" + @focus="setActiveField('title')" + @keydown="autoCompleteMixinUpdateCarretPosition" + @keydown.tab="autoCompleteMixinHandleTab($event)" + @keydown.up="autoCompleteMixinSelectPreviousAutocomplete($event)" + @keydown.down="autoCompleteMixinSelectNextAutocomplete($event)" + @keypress.enter="titleEnterHandler($event)" + @keydown.esc="autoCompleteMixinHandleEscape($event)" >
    +
    { + this.textbox.setSelectionRange(newCaret, newCaret); + this.textbox.focus(); + }); }, async addTag (name) { const tagResult = await this.createTag({ name }); diff --git a/website/client/src/components/tasks/user.vue b/website/client/src/components/tasks/user.vue index 4de6ff3963..3af37856de 100644 --- a/website/client/src/components/tasks/user.vue +++ b/website/client/src/components/tasks/user.vue @@ -80,9 +80,17 @@ v-html="icons.drag" >
    @@ -134,6 +149,15 @@
    +