feat: editor script setup
continuous-integration/drone/pr Build is passing Details
continuous-integration/drone/push Build is failing Details

This commit is contained in:
Dominik Pschenitschni 2022-10-03 12:18:20 +02:00
parent cbec1f24aa
commit db627ed28a
Signed by: dpschen
GPG Key ID: B257AC0149F43A77
7 changed files with 289 additions and 279 deletions

View File

@ -69,6 +69,7 @@
"@faker-js/faker": "7.5.0",
"@types/dompurify": "2.3.4",
"@types/flexsearch": "0.7.3",
"@types/marked": "4.0.7",
"@types/node": "16.11.64",
"@typescript-eslint/eslint-plugin": "5.39.0",
"@typescript-eslint/parser": "5.39.0",

View File

@ -1,4 +1,4 @@
lockfileVersion: 5.3
lockfileVersion: 5.4
specifiers:
'@4tw/cypress-drag-drop': 2.2.1
@ -17,6 +17,7 @@ specifiers:
'@types/flexsearch': 0.7.3
'@types/is-touch-device': 1.0.0
'@types/lodash.clonedeep': 4.5.7
'@types/marked': 4.0.7
'@types/node': 16.11.64
'@types/sortablejs': 1.15.0
'@typescript-eslint/eslint-plugin': 5.39.0
@ -86,7 +87,7 @@ dependencies:
'@fortawesome/fontawesome-svg-core': 6.2.0
'@fortawesome/free-regular-svg-icons': 6.2.0
'@fortawesome/free-solid-svg-icons': 6.2.0
'@fortawesome/vue-fontawesome': 3.0.1_21e6b971ab1cbc4f6c07645df616013e
'@fortawesome/vue-fontawesome': 3.0.1_ehtls4nlds6e63ahmro7mfqbhy
'@github/hotkey': 2.0.1
'@kyvg/vue3-notification': 2.4.1_vue@3.2.40
'@sentry/tracing': 7.14.1
@ -95,7 +96,7 @@ dependencies:
'@types/lodash.clonedeep': 4.5.7
'@types/sortablejs': 1.15.0
'@vueuse/core': 9.3.0_vue@3.2.40
'@vueuse/router': 9.3.0_vue-router@4.1.5+vue@3.2.40
'@vueuse/router': 9.3.0_c7eza3xvlyb4mo6qeit5ggeo6u
axios: 0.27.2
blurhash: 2.0.3
bulma-css-variables: 0.9.33
@ -112,7 +113,7 @@ dependencies:
lodash.debounce: 4.0.8
marked: 4.1.1
minimist: 1.2.6
pinia: 2.0.22_typescript@4.8.4+vue@3.2.40
pinia: 2.0.22_bfjwoga25wxjazzogo7o372nwq
register-service-worker: 1.7.2
snake-case: 3.0.4
sortablejs: 1.15.0
@ -134,12 +135,13 @@ devDependencies:
'@faker-js/faker': 7.5.0
'@types/dompurify': 2.3.4
'@types/flexsearch': 0.7.3
'@types/marked': 4.0.7
'@types/node': 16.11.64
'@typescript-eslint/eslint-plugin': 5.39.0_be048b79d04b908735dc19ebba1dbd66
'@typescript-eslint/parser': 5.39.0_eslint@8.24.0+typescript@4.8.4
'@typescript-eslint/eslint-plugin': 5.39.0_xyciw6oqjoiiono4dhv3uhn5my
'@typescript-eslint/parser': 5.39.0_ypn2ylkkyfa5i233caldtndbqa
'@vitejs/plugin-legacy': 2.2.0_terser@5.10.0+vite@3.1.4
'@vitejs/plugin-vue': 3.1.2_vite@3.1.4+vue@3.2.40
'@vue/eslint-config-typescript': 11.0.2_ab5ac96ab635ce1fa6bba69a2b0ff7cb
'@vue/eslint-config-typescript': 11.0.2_vnnms2vwgxhb7jv3u2ncwd7xzm
'@vue/test-utils': 2.1.0_vue@3.2.40
'@vue/tsconfig': 0.1.3_@types+node@16.11.64
autoprefixer: 10.4.12_postcss@8.4.17
@ -159,12 +161,12 @@ devDependencies:
sass: 1.55.0
typescript: 4.8.4
vite: 3.1.4_sass@1.55.0+terser@5.10.0
vite-plugin-pwa: 0.13.1_vite@3.1.4
vite-plugin-pwa: 0.13.1_bhe5iaipiq3lmbaxwdxgnnn2gq
vite-svg-loader: 3.6.0
vitest: 0.23.4_ddc85b0b7a78b2cfae025c8e16b7f605
vitest: 0.23.4_3xefwc32pczm7lqclshbnn7wau
vue-tsc: 0.40.13_typescript@4.8.4
wait-on: 6.0.1
workbox-cli: 6.5.4
workbox-cli: 6.5.4_acorn@8.8.0
packages:
@ -1392,7 +1394,7 @@ packages:
peerDependencies:
postcss: ^8.2
dependencies:
'@csstools/selector-specificity': 2.0.2_cd239324a5aeb6e3cee0fb61f6a33448
'@csstools/selector-specificity': 2.0.2_zurzgjffv23ohtxa7nq7nizuja
postcss: 8.4.17
postcss-selector-parser: 6.0.10
dev: true
@ -1445,7 +1447,7 @@ packages:
peerDependencies:
postcss: ^8.2
dependencies:
'@csstools/selector-specificity': 2.0.2_cd239324a5aeb6e3cee0fb61f6a33448
'@csstools/selector-specificity': 2.0.2_zurzgjffv23ohtxa7nq7nizuja
postcss: 8.4.17
postcss-selector-parser: 6.0.10
dev: true
@ -1530,7 +1532,7 @@ packages:
postcss: 8.4.17
dev: true
/@csstools/selector-specificity/2.0.2_cd239324a5aeb6e3cee0fb61f6a33448:
/@csstools/selector-specificity/2.0.2_zurzgjffv23ohtxa7nq7nizuja:
resolution: {integrity: sha512-IkpVW/ehM1hWKln4fCA3NzJU8KwD+kIOvPZA4cqxoJHtE21CCzjyp+Kxbu0i5I4tBNOlXPL9mjwnWlL0VEG4Fg==}
engines: {node: ^12 || ^14 || >=16}
peerDependencies:
@ -1679,7 +1681,7 @@ packages:
'@fortawesome/fontawesome-common-types': 6.2.0
dev: false
/@fortawesome/vue-fontawesome/3.0.1_21e6b971ab1cbc4f6c07645df616013e:
/@fortawesome/vue-fontawesome/3.0.1_ehtls4nlds6e63ahmro7mfqbhy:
resolution: {integrity: sha512-CdXZJoCS+aEPec26ZP7hWWU3SaJlQPZSCGdgpQ2qGl2HUmtUUNrI3zC4XWdn1JUmh3t5OuDeRG1qB4eGRNSD4A==}
peerDependencies:
'@fortawesome/fontawesome-svg-core': ~1 || ~6
@ -1917,7 +1919,7 @@ packages:
strip-ansi: 7.0.1
supports-color: 9.2.1
tmp-promise: 3.0.3
ts-node: 10.8.1_56922d2c3e8466316d745c2e9b343672
ts-node: 10.8.1_k2jc2lb6qrtdc3lulqxjwnbwoi
typescript: 4.8.4
update-notifier: 5.1.0
uuid: 8.3.2
@ -2605,7 +2607,7 @@ packages:
resolution: {integrity: sha512-92FRmppjjqz29VMJ2dn+xdyXZBrMlE42AV6Kq6BwjWV7CNUW1hs2FtxSNLQE+gJhaZ6AAmYuO9y8dshhcBl7vA==}
dev: false
/@rollup/plugin-babel/5.3.0_@babel+core@7.17.2+rollup@2.79.1:
/@rollup/plugin-babel/5.3.0_pf2mys4p2khuj2gysypj3zzjia:
resolution: {integrity: sha512-9uIC8HZOnVLrLHxayq/PTzw+uS25E14KPUBh5ktF+18Mjo5yK0ToMMx6epY0uEgkjwJw0aBW4x2horYXh8juWw==}
engines: {node: '>= 10.0.0'}
peerDependencies:
@ -2989,7 +2991,6 @@ packages:
/@types/marked/4.0.7:
resolution: {integrity: sha512-eEAhnz21CwvKVW+YvRvcTuFKNU9CV1qH+opcgVK3pIMI6YZzDm6gc8o2vHjldFk6MGKt5pueSB7IOpvpx5Qekw==}
dev: false
/@types/minimatch/3.0.5:
resolution: {integrity: sha512-Klz949h02Gz2uZCMGwDUSDS1YBlTdDDgbWHi+81l29tQALUtvz4rAYi5uoVhE5Lagoq6DeqAUlbrHvW/mXDgdQ==}
@ -3104,7 +3105,7 @@ packages:
dev: true
optional: true
/@typescript-eslint/eslint-plugin/5.39.0_be048b79d04b908735dc19ebba1dbd66:
/@typescript-eslint/eslint-plugin/5.39.0_xyciw6oqjoiiono4dhv3uhn5my:
resolution: {integrity: sha512-xVfKOkBm5iWMNGKQ2fwX5GVgBuHmZBO1tCRwXmY5oAIsPscfwm2UADDuNB8ZVYCtpQvJK4xpjrK7jEhcJ0zY9A==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
@ -3115,10 +3116,10 @@ packages:
typescript:
optional: true
dependencies:
'@typescript-eslint/parser': 5.39.0_eslint@8.24.0+typescript@4.8.4
'@typescript-eslint/parser': 5.39.0_ypn2ylkkyfa5i233caldtndbqa
'@typescript-eslint/scope-manager': 5.39.0
'@typescript-eslint/type-utils': 5.39.0_eslint@8.24.0+typescript@4.8.4
'@typescript-eslint/utils': 5.39.0_eslint@8.24.0+typescript@4.8.4
'@typescript-eslint/type-utils': 5.39.0_ypn2ylkkyfa5i233caldtndbqa
'@typescript-eslint/utils': 5.39.0_ypn2ylkkyfa5i233caldtndbqa
debug: 4.3.4
eslint: 8.24.0
ignore: 5.2.0
@ -3130,7 +3131,7 @@ packages:
- supports-color
dev: true
/@typescript-eslint/parser/5.39.0_eslint@8.24.0+typescript@4.8.4:
/@typescript-eslint/parser/5.39.0_ypn2ylkkyfa5i233caldtndbqa:
resolution: {integrity: sha512-PhxLjrZnHShe431sBAGHaNe6BDdxAASDySgsBCGxcBecVCi8NQWxQZMcizNA4g0pN51bBAn/FUfkWG3SDVcGlA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
@ -3158,7 +3159,7 @@ packages:
'@typescript-eslint/visitor-keys': 5.39.0
dev: true
/@typescript-eslint/type-utils/5.39.0_eslint@8.24.0+typescript@4.8.4:
/@typescript-eslint/type-utils/5.39.0_ypn2ylkkyfa5i233caldtndbqa:
resolution: {integrity: sha512-KJHJkOothljQWzR3t/GunL0TPKY+fGJtnpl+pX+sJ0YiKTz3q2Zr87SGTmFqsCMFrLt5E0+o+S6eQY0FAXj9uA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
@ -3169,7 +3170,7 @@ packages:
optional: true
dependencies:
'@typescript-eslint/typescript-estree': 5.39.0_typescript@4.8.4
'@typescript-eslint/utils': 5.39.0_eslint@8.24.0+typescript@4.8.4
'@typescript-eslint/utils': 5.39.0_ypn2ylkkyfa5i233caldtndbqa
debug: 4.3.4
eslint: 8.24.0
tsutils: 3.21.0_typescript@4.8.4
@ -3183,7 +3184,7 @@ packages:
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
dev: true
/@typescript-eslint/typescript-estree/5.39.0_f1deb5be19df0fe0ff039530117daddf:
/@typescript-eslint/typescript-estree/5.39.0_6hpllpqz34h6b7ydsuybc7nn34:
resolution: {integrity: sha512-qLFQP0f398sdnogJoLtd43pUgB18Q50QSA+BTE5h3sUxySzbWDpTSdgt4UyxNSozY/oDK2ta6HVAzvGgq8JYnA==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
@ -3225,7 +3226,7 @@ packages:
- supports-color
dev: true
/@typescript-eslint/utils/5.39.0_eslint@8.24.0+typescript@4.8.4:
/@typescript-eslint/utils/5.39.0_ypn2ylkkyfa5i233caldtndbqa:
resolution: {integrity: sha512-+DnY5jkpOpgj+EBtYPyHRjXampJfC0yUZZzfzLuUWVZvCuKqSdJVC8UhdWipIw7VKNTfwfAPiOWzYkAwuIhiAg==}
engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0}
peerDependencies:
@ -3303,7 +3304,7 @@ packages:
magic-string: 0.26.3
regenerator-runtime: 0.13.9
systemjs: 6.12.6
terser: 5.10.0
terser: 5.10.0_acorn@8.8.0
vite: 3.1.4_sass@1.55.0+terser@5.10.0
dev: true
@ -3432,7 +3433,7 @@ packages:
resolution: {integrity: sha512-OEgAMeQXvCoJ+1x8WyQuVZzFo0wcyCmUR3baRVLmKBo1LmYZWMlRiXlux5jd0fqVJu6PfDbOrZItVqUEzLobeQ==}
dev: false
/@vue/eslint-config-typescript/11.0.2_ab5ac96ab635ce1fa6bba69a2b0ff7cb:
/@vue/eslint-config-typescript/11.0.2_vnnms2vwgxhb7jv3u2ncwd7xzm:
resolution: {integrity: sha512-EiKud1NqlWmSapBFkeSrE994qpKx7/27uCGnhdqzllYDpQZroyX/O6bwjEpeuyKamvLbsGdO6PMR2faIf+zFnw==}
engines: {node: ^14.17.0 || >=16.0.0}
peerDependencies:
@ -3443,8 +3444,8 @@ packages:
typescript:
optional: true
dependencies:
'@typescript-eslint/eslint-plugin': 5.39.0_be048b79d04b908735dc19ebba1dbd66
'@typescript-eslint/parser': 5.39.0_eslint@8.24.0+typescript@4.8.4
'@typescript-eslint/eslint-plugin': 5.39.0_xyciw6oqjoiiono4dhv3uhn5my
'@typescript-eslint/parser': 5.39.0_ypn2ylkkyfa5i233caldtndbqa
eslint: 8.24.0
eslint-plugin-vue: 9.6.0_eslint@8.24.0
typescript: 4.8.4
@ -3551,7 +3552,7 @@ packages:
resolution: {integrity: sha512-GnnfjbzIPJIh9ngL9s9oGU1+Hx/h5/KFqTfJykzh/1xjaHkedV9g0MASpdmPZIP+ynNhKAcEfA6g5i8KXwtoMA==}
dev: false
/@vueuse/router/9.3.0_vue-router@4.1.5+vue@3.2.40:
/@vueuse/router/9.3.0_c7eza3xvlyb4mo6qeit5ggeo6u:
resolution: {integrity: sha512-UFN2MFciprH21oYsAgNHeDJ4Bd86HpRm9gximSN8j6h4fc2aa62fvfhprfHqdTxYAcgcGkMwcc9TO75jOvr8gg==}
peerDependencies:
vue-router: '>=4.0.0-rc.1'
@ -5572,7 +5573,7 @@ packages:
resolution: {integrity: sha512-lR78AugfUSBojwlSRZBeEqQ1l8LI7rbxOl1qTUnGLcjZQDjZmrZCb7R46rK8U8B5WzFvJrxa7fEBA8FoD/n5fA==}
engines: {node: ^12.20.0 || ^14.14.0 || >=16.0.0}
dependencies:
'@typescript-eslint/typescript-estree': 5.39.0_f1deb5be19df0fe0ff039530117daddf
'@typescript-eslint/typescript-estree': 5.39.0_6hpllpqz34h6b7ydsuybc7nn34
ast-module-types: 3.0.0
node-source-walk: 5.0.0
typescript: 4.8.4
@ -10088,7 +10089,7 @@ packages:
engines: {node: '>=6'}
dev: true
/pinia/2.0.22_typescript@4.8.4+vue@3.2.40:
/pinia/2.0.22_bfjwoga25wxjazzogo7o372nwq:
resolution: {integrity: sha512-u+b8/BC+tmvo3ACbYO2w5NfxHWFOjvvw9DQnyT0dW8aUMCPRQT5QnfZ5R5W2MzZBMTeZRMQI7V/QFbafmM9QHw==}
peerDependencies:
'@vue/composition-api': ^1.4.0
@ -10331,7 +10332,7 @@ packages:
peerDependencies:
postcss: ^8.2
dependencies:
'@csstools/selector-specificity': 2.0.2_cd239324a5aeb6e3cee0fb61f6a33448
'@csstools/selector-specificity': 2.0.2_zurzgjffv23ohtxa7nq7nizuja
postcss: 8.4.17
postcss-selector-parser: 6.0.10
dev: true
@ -11047,7 +11048,7 @@ packages:
glob: 7.2.0
dev: true
/rollup-plugin-terser/7.0.2_rollup@2.79.1:
/rollup-plugin-terser/7.0.2_acorn@8.8.0+rollup@2.79.1:
resolution: {integrity: sha512-w3iIaU4OxcF52UUXiZNsNeuXIMDvFrr+ZXK6bFZ0Q60qyVfq4uLptoS4bbq3paG3x216eQllFZX7zt6TIImguQ==}
peerDependencies:
rollup: ^2.0.0
@ -11056,7 +11057,9 @@ packages:
jest-worker: 26.6.2
rollup: 2.79.1
serialize-javascript: 4.0.0
terser: 5.10.0
terser: 5.10.0_acorn@8.8.0
transitivePeerDependencies:
- acorn
dev: true
/rollup-plugin-visualizer/5.8.2_rollup@2.79.1:
@ -11938,10 +11941,12 @@ packages:
supports-hyperlinks: 2.2.0
dev: true
/terser/5.10.0:
/terser/5.10.0_acorn@8.8.0:
resolution: {integrity: sha512-AMmF99DMfEDiRJfxfY5jj5wNH/bYO09cniSqhfoyxc8sFoYIgkJy86G04UoZU5VjlpnplVu0K6Tx6E9b5+DlHA==}
engines: {node: '>=10'}
hasBin: true
peerDependencies:
acorn: ^8.5.0
peerDependenciesMeta:
acorn:
optional: true
@ -12147,7 +12152,7 @@ packages:
resolution: {integrity: sha512-XrHUvV5HpdLmIj4uVMxHggLbFSZYIn7HEWsqePZcI50pco+MPqJ50wMGY794X7AOOhxOBAjbkqfAbEe/QMp2Lw==}
dev: true
/ts-node/10.8.1_56922d2c3e8466316d745c2e9b343672:
/ts-node/10.8.1_k2jc2lb6qrtdc3lulqxjwnbwoi:
resolution: {integrity: sha512-Wwsnao4DQoJsN034wePSg5nZiw4YKXf56mPIAeD6wVmiv+RytNSWqc2f3fKvcUoV+Yn2+yocD71VOfQHbmVX4g==}
hasBin: true
peerDependencies:
@ -12545,20 +12550,21 @@ packages:
extsprintf: 1.3.0
dev: true
/vite-plugin-pwa/0.13.1_vite@3.1.4:
/vite-plugin-pwa/0.13.1_bhe5iaipiq3lmbaxwdxgnnn2gq:
resolution: {integrity: sha512-NR3dIa+o2hzlzo4lF4Gu0cYvoMjSw2DdRc6Epw1yjmCqWaGuN86WK9JqZie4arNlE1ZuWT3CLiMdiX5wcmmUmg==}
peerDependencies:
vite: ^3.1.0
workbox-build: ^6.5.4
workbox-window: ^6.5.4
dependencies:
debug: 4.3.4
fast-glob: 3.2.11
pretty-bytes: 6.0.0
rollup: 2.79.1
vite: 3.1.4_sass@1.55.0+terser@5.10.0
workbox-build: 6.5.4
workbox-build: 6.5.4_acorn@8.8.0
workbox-window: 6.5.4
transitivePeerDependencies:
- '@types/babel__core'
- supports-color
dev: true
@ -12593,12 +12599,12 @@ packages:
resolve: 1.22.1
rollup: 2.78.0
sass: 1.55.0
terser: 5.10.0
terser: 5.10.0_acorn@8.8.0
optionalDependencies:
fsevents: 2.3.2
dev: true
/vitest/0.23.4_ddc85b0b7a78b2cfae025c8e16b7f605:
/vitest/0.23.4_3xefwc32pczm7lqclshbnn7wau:
resolution: {integrity: sha512-iukBNWqQAv8EKDBUNntspLp9SfpaVFbmzmM0sNcnTxASQZMzRw3PsM6DMlsHiI+I6GeO5/sYDg3ecpC+SNFLrQ==}
engines: {node: '>=v14.16.0'}
hasBin: true
@ -12912,7 +12918,7 @@ packages:
workbox-core: 6.5.4
dev: true
/workbox-build/6.5.4:
/workbox-build/6.5.4_acorn@8.8.0:
resolution: {integrity: sha512-kgRevLXEYvUW9WS4XoziYqZ8Q9j/2ziJYEtTrjdz5/L/cTUa2XfyMP2i7c3p34lgqJ03+mTiz13SdFef2POwbA==}
engines: {node: '>=10.0.0'}
dependencies:
@ -12920,7 +12926,7 @@ packages:
'@babel/core': 7.17.2
'@babel/preset-env': 7.16.11_@babel+core@7.17.2
'@babel/runtime': 7.17.2
'@rollup/plugin-babel': 5.3.0_@babel+core@7.17.2+rollup@2.79.1
'@rollup/plugin-babel': 5.3.0_pf2mys4p2khuj2gysypj3zzjia
'@rollup/plugin-node-resolve': 11.2.1_rollup@2.79.1
'@rollup/plugin-replace': 2.4.2_rollup@2.79.1
'@surma/rollup-plugin-off-main-thread': 2.2.3
@ -12932,7 +12938,7 @@ packages:
lodash: 4.17.21
pretty-bytes: 5.6.0
rollup: 2.79.1
rollup-plugin-terser: 7.0.2_rollup@2.79.1
rollup-plugin-terser: 7.0.2_acorn@8.8.0+rollup@2.79.1
source-map: 0.8.0-beta.0
stringify-object: 3.3.0
strip-comments: 2.0.1
@ -12955,6 +12961,7 @@ packages:
workbox-window: 6.5.4
transitivePeerDependencies:
- '@types/babel__core'
- acorn
- supports-color
dev: true
@ -12964,7 +12971,7 @@ packages:
workbox-core: 6.5.4
dev: true
/workbox-cli/6.5.4:
/workbox-cli/6.5.4_acorn@8.8.0:
resolution: {integrity: sha512-+Cc0jYh25MofhCROZqfQkpYSAGvykyrUVekuuPaLFbJ8qxX/zzX8hRRpglfwxDwokAjz8S20oEph4s+MyQc+Yw==}
engines: {node: '>=10.0.0'}
hasBin: true
@ -12981,9 +12988,10 @@ packages:
stringify-object: 3.3.0
upath: 1.2.0
update-notifier: 4.1.3
workbox-build: 6.5.4
workbox-build: 6.5.4_acorn@8.8.0
transitivePeerDependencies:
- '@types/babel__core'
- acorn
- supports-color
dev: true

View File

@ -4,7 +4,7 @@
<vue-easymde
:configs="config"
@change="bubble"
@change="() => bubble()"
@update:modelValue="handleInput"
class="content"
v-if="isEditActive"
@ -66,245 +66,245 @@
</div>
</template>
<script lang="ts">
import {defineComponent} from 'vue'
<script setup lang="ts">
import {computed, nextTick, onMounted, ref, toRefs, watch} from 'vue'
import VueEasymde from './vue-easymde.vue'
import {marked} from 'marked'
import DOMPurify from 'dompurify'
import {setupMarkdownRenderer} from '@/helpers/markdownRenderer'
import {createEasyMDEConfig} from './editorConfig'
import AttachmentModel from '../../models/attachment'
import AttachmentService from '../../services/attachment'
import {findCheckboxesInText} from '../../helpers/checklistFromText'
import AttachmentModel from '@/models/attachment'
import AttachmentService from '@/services/attachment'
import {setupMarkdownRenderer} from '@/helpers/markdownRenderer'
import {findCheckboxesInText} from '@/helpers/checklistFromText'
import {createRandomID} from '@/helpers/randomId'
import BaseButton from '@/components/base/BaseButton.vue'
import ButtonLink from '@/components/misc/ButtonLink.vue'
import type { IAttachment } from '@/modelTypes/IAttachment'
import type { ITask } from '@/modelTypes/ITask'
export default defineComponent({
name: 'editor',
components: {
VueEasymde,
BaseButton,
ButtonLink,
const props = defineProps({
modelValue: {
type: String,
default: '',
},
props: {
modelValue: {
type: String,
default: '',
},
placeholder: {
type: String,
default: '',
},
uploadEnabled: {
type: Boolean,
default: false,
},
uploadCallback: {
type: Function,
},
hasPreview: {
type: Boolean,
default: true,
},
previewIsDefault: {
type: Boolean,
default: true,
},
isEditEnabled: {
default: true,
},
bottomActions: {
default: () => [],
},
emptyText: {
type: String,
default: '',
},
showSave: {
type: Boolean,
default: false,
},
// If a key is passed the editor will go in "edit" mode when the key is pressed.
// Disabled if an empty string is passed.
editShortcut: {
type: String,
default: '',
},
placeholder: {
type: String,
default: '',
},
emits: ['update:modelValue'],
computed: {
showPreviewText() {
return this.isPreviewActive && this.text === '' && this.emptyText !== ''
},
showEditButton() {
return !this.isEditActive && this.text !== ''
},
uploadEnabled: {
type: Boolean,
default: false,
},
data() {
return {
text: '',
changeTimeout: null,
isEditActive: false,
isPreviewActive: true,
preview: '',
attachmentService: null,
loadedAttachments: {},
config: createEasyMDEConfig({
placeholder: this.placeholder,
uploadImage: this.uploadEnabled,
imageUploadFunction: this.uploadCallback,
}),
checkboxId: createRandomID(),
}
uploadCallback: {
type: Function,
},
watch: {
modelValue(modelValue) {
this.text = modelValue
this.$nextTick(this.renderPreview)
},
text(newVal, oldVal) {
// Only bubble the new value if it actually changed, but not if the component just got mounted and the text changed from the outside.
if (oldVal === '' && this.text === this.modelValue) {
return
}
this.bubble()
},
hasPreview: {
type: Boolean,
default: true,
},
mounted() {
if (this.modelValue !== '') {
this.text = this.modelValue
}
if (this.previewIsDefault && this.hasPreview) {
this.$nextTick(this.renderPreview)
return
}
this.isPreviewActive = false
this.isEditActive = true
previewIsDefault: {
type: Boolean,
default: true,
},
methods: {
// This gets triggered when only pasting content into the editor.
// A change event would not get generated by that, an input event does.
// Therefore, we're using this handler to catch paste events.
// But because this also gets triggered when typing into the editor, we give
// it a higher timeout to make the timouts cancel each other in that case so
// that in the end, only one change event is triggered to the outside per change.
handleInput(val) {
// Don't bubble if the text is up to date
if (val === this.text) {
return
}
this.text = val
this.bubble(1000)
},
bubble(timeout = 500) {
if (this.changeTimeout !== null) {
clearTimeout(this.changeTimeout)
}
this.changeTimeout = setTimeout(() => {
this.$emit('update:modelValue', this.text)
}, timeout)
},
replaceAt(str, index, replacement) {
return str.substr(0, index) + replacement + str.substr(index + replacement.length)
},
findNthIndex(str, n) {
const checkboxes = findCheckboxesInText(str)
return checkboxes[n]
},
renderPreview() {
setupMarkdownRenderer(this.checkboxId)
this.preview = DOMPurify.sanitize(marked(this.text), {ADD_ATTR: ['target']})
// Since the render function is synchronous, we can't do async http requests in it.
// Therefore, we can't resolve the blob url at (markdown) compile time.
// To work around this, we modify the url after rendering it in the vue component.
// We're doing the whole thing in the next tick to ensure the image elements are available in the
// dom tree. If we're calling this right after setting this.preview it could be the images were
// not already made available.
// Some docs at https://stackoverflow.com/q/62865160/10924593
this.$nextTick(async () => {
const attachmentImage = document.getElementsByClassName('attachment-image')
if (attachmentImage) {
for (const img of attachmentImage) {
// The url is something like /tasks/<id>/attachments/<id>
const parts = img.dataset.src.substr(window.API_URL.length + 1).split('/')
const taskId = parseInt(parts[1])
const attachmentId = parseInt(parts[3])
const cacheKey = `${taskId}-${attachmentId}`
if (typeof this.loadedAttachments[cacheKey] !== 'undefined') {
img.src = this.loadedAttachments[cacheKey]
continue
}
const attachment = new AttachmentModel({taskId: taskId, id: attachmentId})
if (this.attachmentService === null) {
this.attachmentService = new AttachmentService()
}
const url = await this.attachmentService.getBlobUrl(attachment)
img.src = url
this.loadedAttachments[cacheKey] = url
}
}
const textCheckbox = document.getElementsByClassName(`text-checkbox-${this.checkboxId}`)
if (textCheckbox) {
for (const check of textCheckbox) {
check.removeEventListener('change', this.handleCheckboxClick)
check.addEventListener('change', this.handleCheckboxClick)
check.parentElement.classList.add('has-checkbox')
}
}
})
},
handleCheckboxClick(e) {
// Find the original markdown checkbox this is targeting
const checked = e.target.checked
const numMarkdownCheck = parseInt(e.target.dataset.checkboxNum)
const index = this.findNthIndex(this.text, numMarkdownCheck)
if (index < 0 || typeof index === 'undefined') {
console.debug('no index found')
return
}
console.debug(index, this.text.substr(index, 9))
const listPrefix = this.text.substr(index, 1)
if (checked) {
this.text = this.replaceAt(this.text, index, `${listPrefix} [x] `)
} else {
this.text = this.replaceAt(this.text, index, `${listPrefix} [ ] `)
}
this.bubble()
this.renderPreview()
},
toggleEdit() {
if (this.isEditActive) {
this.isPreviewActive = true
this.isEditActive = false
this.renderPreview()
this.bubble(0) // save instantly
} else {
this.isPreviewActive = false
this.isEditActive = true
}
},
isEditEnabled: {
default: true,
},
bottomActions: {
default: () => [],
},
emptyText: {
type: String,
default: '',
},
showSave: {
type: Boolean,
default: false,
},
// If a key is passed the editor will go in "edit" mode when the key is pressed.
// Disabled if an empty string is passed.
editShortcut: {
type: String,
default: '',
},
})
const emit = defineEmits(['update:modelValue'])
const text = ref('')
const changeTimeout = ref<ReturnType<typeof setTimeout> | null>(null)
const isEditActive = ref(false)
const isPreviewActive = ref(true)
const showPreviewText = computed(() => isPreviewActive.value && text.value === '' && props.emptyText !== '')
const showEditButton = computed(() => !isEditActive.value && text.value !== '')
const preview = ref('')
const attachmentService = new AttachmentService()
type CacheKey = `${ITask['id']}-${IAttachment['id']}`
const loadedAttachments = ref<{[key: CacheKey]: string}>({})
const config = ref(createEasyMDEConfig({
placeholder: props.placeholder,
uploadImage: props.uploadEnabled,
imageUploadFunction: props.uploadCallback,
}))
const checkboxId = ref(createRandomID())
const {modelValue} = toRefs(props)
watch(
modelValue,
async (value) => {
text.value = value
await nextTick()
renderPreview()
},
)
watch(
text,
(newVal, oldVal) => {
// Only bubble the new value if it actually changed, but not if the component just got mounted and the text changed from the outside.
if (oldVal === '' && text.value === modelValue.value) {
return
}
bubble()
},
)
onMounted(() => {
if (modelValue.value !== '') {
text.value = modelValue.value
}
if (props.previewIsDefault && props.hasPreview) {
nextTick(() => renderPreview())
return
}
isPreviewActive.value = false
isEditActive.value = true
})
// This gets triggered when only pasting content into the editor.
// A change event would not get generated by that, an input event does.
// Therefore, we're using this handler to catch paste events.
// But because this also gets triggered when typing into the editor, we give
// it a higher timeout to make the timouts cancel each other in that case so
// that in the end, only one change event is triggered to the outside per change.
function handleInput(val: string) {
// Don't bubble if the text is up to date
if (val === text.value) {
return
}
text.value = val
bubble(1000)
}
function bubble(timeout = 500) {
if (changeTimeout.value !== null) {
clearTimeout(changeTimeout.value)
}
changeTimeout.value = setTimeout(() => {
emit('update:modelValue', text.value)
}, timeout)
}
function replaceAt(str: string, index: number, replacement: string) {
return str.slice(0, index) + replacement + str.slice(index + replacement.length)
}
function findNthIndex(str: string, n: number) {
const checkboxes = findCheckboxesInText(str)
return checkboxes[n]
}
function renderPreview() {
setupMarkdownRenderer(checkboxId.value)
preview.value = DOMPurify.sanitize(marked(text.value), {ADD_ATTR: ['target']})
// Since the render function is synchronous, we can't do async http requests in it.
// Therefore, we can't resolve the blob url at (markdown) compile time.
// To work around this, we modify the url after rendering it in the vue component.
// We're doing the whole thing in the next tick to ensure the image elements are available in the
// dom tree. If we're calling this right after setting this.preview it could be the images were
// not already made available.
// Some docs at https://stackoverflow.com/q/62865160/10924593
nextTick().then(async () => {
const attachmentImage = document.querySelectorAll<HTMLImageElement>('.attachment-image')
if (attachmentImage) {
Array.from(attachmentImage).forEach(async (img) => {
// The url is something like /tasks/<id>/attachments/<id>
const parts = img.dataset.src?.slice(window.API_URL.length + 1).split('/')
const taskId = Number(parts[1])
const attachmentId = Number(parts[3])
const cacheKey: CacheKey = `${taskId}-${attachmentId}`
if (typeof loadedAttachments.value[cacheKey] !== 'undefined') {
img.src = loadedAttachments.value[cacheKey]
return
}
const attachment = new AttachmentModel({taskId: taskId, id: attachmentId})
const url = await attachmentService.getBlobUrl(attachment)
img.src = url
loadedAttachments.value[cacheKey] = url
})
}
const textCheckbox = document.querySelectorAll<HTMLInputElement>(`.text-checkbox-${checkboxId.value}`)
if (textCheckbox) {
Array.from(textCheckbox).forEach(check => {
check.removeEventListener('change', handleCheckboxClick)
check.addEventListener('change', handleCheckboxClick)
check.parentElement?.classList.add('has-checkbox')
})
}
})
}
function handleCheckboxClick(e: Event) {
// Find the original markdown checkbox this is targeting
const checked = (e.target as HTMLInputElement).checked
const numMarkdownCheck = Number((e.target as HTMLInputElement).dataset.checkboxNum)
const index = findNthIndex(text.value, numMarkdownCheck)
if (index < 0 || typeof index === 'undefined') {
console.debug('no index found')
return
}
console.debug(index, text.value.slice(index, 9))
const listPrefix = text.value.slice(index, 1)
text.value = replaceAt(text.value, index, `${listPrefix} ${checked ? '[x]' : '[ ]'} `)
bubble()
renderPreview()
}
function toggleEdit() {
if (isEditActive.value) {
isPreviewActive.value = true
isEditActive.value = false
renderPreview()
bubble(0) // save instantly
} else {
isPreviewActive.value = false
isEditActive.value = true
}
}
</script>
<style lang="scss">

View File

@ -8,33 +8,34 @@ export function setupMarkdownRenderer(checkboxId: string) {
let checkboxNum = -1
marked.use({
renderer: {
image: (src, title, text) => {
image(src: string, title: string, text: string) {
title = title ? ` title="${title}` : ''
// If the url starts with the api url, the image is likely an attachment and
// we'll need to download and parse it properly.
if (src.substr(0, window.API_URL.length + 7) === `${window.API_URL}/tasks/`) {
if (src.slice(0, window.API_URL.length + 7) === `${window.API_URL}/tasks/`) {
return `<img data-src="${src}" alt="${text}" ${title} class="attachment-image"/>`
}
return `<img src="${src}" alt="${text}" ${title}/>`
},
checkbox: (checked) => {
checkbox(checked: boolean) {
let checkedString = ''
if (checked) {
checked = ' checked="checked"'
checkedString = 'checked'
}
checkboxNum++
return `<input type="checkbox" data-checkbox-num="${checkboxNum}" ${checked} class="text-checkbox-${checkboxId}"/>`
return `<input type="checkbox" data-checkbox-num="${checkboxNum}" ${checkedString} class="text-checkbox-${checkboxId}"/>`
},
link: (href, title, text) => {
link(href: string, title: string, text: string) {
const isLocal = href.startsWith(`${location.protocol}//${location.hostname}`)
const html = linkRenderer.call(renderer, href, title, text)
return isLocal ? html : html.replace(/^<a /, '<a target="_blank" rel="noreferrer noopener nofollow" ')
},
},
highlight: function (code, language) {
highlight(code, language) {
const validLanguage = hljs.getLanguage(language) ? language : 'plaintext'
return hljs.highlight(code, {language: validLanguage}).value
},

View File

@ -1,5 +1,5 @@
const DEFAULT_ID_LENGTH = 9
export function createRandomID(idLength = DEFAULT_ID_LENGTH) {
return Math.random().toString(36).substr(2, idLength)
return Math.random().toString(36).slice(2, idLength)
}

View File

@ -285,7 +285,7 @@ const getDateFromWeekday = (text: string): dateFoundResult => {
// matched string comes with a space at the end (last part of the regex).
let foundText = results[0]
if (foundText.endsWith(' ')) {
foundText = foundText.substr(0, foundText.length - 1)
foundText = foundText.slice(0, foundText.length - 1)
}
return {

View File

@ -34,8 +34,8 @@ if (apiUrlFromStorage !== null) {
}
// Make sure the api url does not contain a / at the end
if (window.API_URL.substr(window.API_URL.length - 1, window.API_URL.length) === '/') {
window.API_URL = window.API_URL.substr(0, window.API_URL.length - 1)
if (window.API_URL.slice(window.API_URL.length - 1, window.API_URL.length) === '/') {
window.API_URL = window.API_URL.slice(0, window.API_URL.length - 1)
}
const app = createApp(App)