From 48c045e952352436804b3bcea9fe22e4cba5e33f Mon Sep 17 00:00:00 2001 From: garrettmills Date: Thu, 22 Apr 2021 10:49:31 -0500 Subject: [PATCH] Add real-time collab support to code editor --- package.json | 3 + pnpm-lock.yaml | 410 ++++++++++++++++++ .../components/editor/code/code.component.ts | 230 +++++++++- src/app/service/editor.service.ts | 4 + src/environments/environment.prod.ts | 1 + src/environments/environment.ts | 1 + 6 files changed, 647 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 992036d..7841b0a 100644 --- a/package.json +++ b/package.json @@ -22,6 +22,9 @@ "@angular/router": "~10.1.5", "@angular/service-worker": "~10.1.5", "@circlon/angular-tree-component": "^10.0.0", + "@ckeditor/ckeditor5-angular": "^2.0.1", + "@ckeditor/ckeditor5-build-decoupled-document": "^27.0.0", + "@convergencelabs/monaco-collab-ext": "^0.3.2", "@fortawesome/fontawesome-free": "^5.15.1", "@ionic-native/core": "^5.0.0", "@ionic-native/splash-screen": "^5.0.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3ee0cc2..20cd819 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ dependencies: '@angular/router': 10.1.6_2d3b53e7e463c932a6e73e0760b7a0a2 '@angular/service-worker': 10.1.6_04f723395c0c28a9d9b8a4ace7178ad2 '@circlon/angular-tree-component': 10.0.2_04f723395c0c28a9d9b8a4ace7178ad2 + '@ckeditor/ckeditor5-angular': 2.0.1_11b9a698fa893a2ce33633beb1aae14b + '@ckeditor/ckeditor5-build-decoupled-document': 27.0.0 + '@convergencelabs/monaco-collab-ext': 0.3.2 '@fortawesome/fontawesome-free': 5.15.2 '@ionic-native/core': 5.31.1_rxjs@6.6.3 '@ionic-native/splash-screen': 5.31.1_fba2ce20a1445ab520b44b573ed379a0 @@ -1421,6 +1424,378 @@ packages: '@angular/core': '>=10.0.0 <11.0.0' resolution: integrity: sha512-N8KyIQ89fGEO8OKYYgFtY/PbPhHqs4DK5kNPmpJt4KPssvQMF1gP6m+RGVDKlDVH933aOPsrHQax87Fk6dI8gA== + /@ckeditor/ckeditor5-adapter-ckfinder/27.0.0: + dependencies: + ckeditor5: 27.0.0 + dev: false + engines: + node: '>=12.0.0' + npm: '>=5.7.1' + resolution: + integrity: sha512-2JzedLntHRw2ChQ54tB9pk1XFH4VzFPJLMJXhE3CP2+ANiIKZZTHodcQ7R2BOosIpySQOEppjfi1emZ6AgcLRg== + /@ckeditor/ckeditor5-alignment/27.0.0: + dependencies: + ckeditor5: 27.0.0 + dev: false + engines: + node: '>=12.0.0' + npm: '>=5.7.1' + resolution: + integrity: sha512-MEXslAyeLmRwbRicOhIKmdvfreXkorwYEYMTDhJaOO3gll/mqDBbn0ijNsCOm4QgXUT6qqK2sOhxGRL3cDuMcQ== + /@ckeditor/ckeditor5-angular/2.0.1_11b9a698fa893a2ce33633beb1aae14b: + dependencies: + '@angular/common': 10.1.6_@angular+core@10.1.6+rxjs@6.6.3 + '@angular/core': 10.1.6_rxjs@6.6.3+zone.js@0.10.3 + '@angular/forms': 10.1.6_2d3b53e7e463c932a6e73e0760b7a0a2 + '@ckeditor/ckeditor5-watchdog': 23.1.0 + tslib: 2.2.0 + dev: false + peerDependencies: + '@angular/common': '>=9.0.0' + '@angular/core': '>=9.0.0' + '@angular/forms': '>=9.0.0' + resolution: + integrity: sha512-mK7HI3DAHiU1UqWq+9BPZi7n5hqO3jZpMNBymOeCRzraln4eQ+6/Ne3lgUyea0XawEdRmtjD64SIzH8MotPjNQ== + /@ckeditor/ckeditor5-autoformat/27.0.0: + dependencies: + ckeditor5: 27.0.0 + dev: false + engines: + node: '>=12.0.0' + npm: '>=5.7.1' + resolution: + integrity: sha512-SirBT1Reu2I9yXSLrcL4IZDecux0pNLfYdZVfz1PIXr99LOYvF1iqyWdo0dLEj5xEGf77OPoiIIhnOQJA8fZzA== + /@ckeditor/ckeditor5-basic-styles/27.0.0: + dependencies: + ckeditor5: 27.0.0 + dev: false + engines: + node: '>=12.0.0' + npm: '>=5.7.1' + resolution: + integrity: sha512-idmD5huRKDFRe1ToyIj5RrnVde00fnEteyVnUWLZPYZr+Y2aye1KFWthmycqKIdNhCXy60ydnhXjy0tVuwHlmw== + /@ckeditor/ckeditor5-block-quote/27.0.0: + dependencies: + ckeditor5: 27.0.0 + dev: false + engines: + node: '>=12.0.0' + npm: '>=5.7.1' + resolution: + integrity: sha512-sUSkhEnKDeAjB8EXvrt3zI+pYdnUg228t2Uoe6A0VCeaSynHn+YF9bHL8XSdEMaD1UdDcdRhtNhJL+d+cEdOqA== + /@ckeditor/ckeditor5-build-decoupled-document/27.0.0: + dependencies: + '@ckeditor/ckeditor5-adapter-ckfinder': 27.0.0 + '@ckeditor/ckeditor5-alignment': 27.0.0 + '@ckeditor/ckeditor5-autoformat': 27.0.0 + '@ckeditor/ckeditor5-basic-styles': 27.0.0 + '@ckeditor/ckeditor5-block-quote': 27.0.0 + '@ckeditor/ckeditor5-ckfinder': 27.0.0 + '@ckeditor/ckeditor5-cloud-services': 27.0.0 + '@ckeditor/ckeditor5-easy-image': 27.0.0 + '@ckeditor/ckeditor5-editor-decoupled': 27.0.0 + '@ckeditor/ckeditor5-essentials': 27.0.0 + '@ckeditor/ckeditor5-font': 27.0.0 + '@ckeditor/ckeditor5-heading': 27.0.0 + '@ckeditor/ckeditor5-image': 27.0.0 + '@ckeditor/ckeditor5-indent': 27.0.0 + '@ckeditor/ckeditor5-link': 27.0.0 + '@ckeditor/ckeditor5-list': 27.0.0 + '@ckeditor/ckeditor5-media-embed': 27.0.0 + '@ckeditor/ckeditor5-paragraph': 27.0.0 + '@ckeditor/ckeditor5-paste-from-office': 27.0.0 + '@ckeditor/ckeditor5-table': 27.0.0 + '@ckeditor/ckeditor5-typing': 27.0.0 + dev: false + engines: + node: '>=12.0.0' + npm: '>=5.7.1' + resolution: + integrity: sha512-2xVXA1eZ99kMwjfrcWtXYLXGSDObmyQOaXWuzYXWaBgGY5mYL4xJtaFP4Jn/IZYShFqu9Wc43nnh3EjnxegfHw== + /@ckeditor/ckeditor5-ckfinder/27.0.0: + dependencies: + ckeditor5: 27.0.0 + dev: false + engines: + node: '>=12.0.0' + npm: '>=5.7.1' + resolution: + integrity: sha512-TyuMYTUv4eiy5dbaxChgn3kaptZWFlnTWOx3CIUPcpURt+h40HFRSANfmOPeKag4COXJaF/0MnmRe/QPJvaNbw== + /@ckeditor/ckeditor5-clipboard/27.0.0: + dependencies: + '@ckeditor/ckeditor5-core': 27.0.0 + '@ckeditor/ckeditor5-engine': 27.0.0 + '@ckeditor/ckeditor5-utils': 27.0.0 + '@ckeditor/ckeditor5-widget': 27.0.0 + lodash-es: 4.17.21 + dev: false + engines: + node: '>=12.0.0' + npm: '>=5.7.1' + resolution: + integrity: sha512-oNLLjPWFoTIqdf6BOtKBDu0qUQiIia5TM6acxi5DKx5YdXnwg1GozxYa4x590EhUVIDFe6i0JuZbkALQ2tBI5g== + /@ckeditor/ckeditor5-cloud-services/27.0.0: + dependencies: + ckeditor5: 27.0.0 + dev: false + engines: + node: '>=12.0.0' + npm: '>=5.7.1' + resolution: + integrity: sha512-IXc8rOAZoITNyl5c46M9/eVusKQYWXsFojxBvI9WlpxtxIRhXCge2grDyEg/rOeLBt8eJxJxIkuNDbwqMXcAKA== + /@ckeditor/ckeditor5-core/27.0.0: + dependencies: + '@ckeditor/ckeditor5-engine': 27.0.0 + '@ckeditor/ckeditor5-ui': 27.0.0 + '@ckeditor/ckeditor5-utils': 27.0.0 + lodash-es: 4.17.21 + dev: false + engines: + node: '>=12.0.0' + npm: '>=5.7.1' + resolution: + integrity: sha512-0dtV2eREe3mh1vqG8x4cIYCwFthzA5aOvRdaIQSj6T6JuBR9lO2IiniOpR0CAsiIgpAM9vmggah+Knngbe2gEA== + /@ckeditor/ckeditor5-easy-image/27.0.0: + dependencies: + ckeditor5: 27.0.0 + dev: false + engines: + node: '>=12.0.0' + npm: '>=5.7.1' + resolution: + integrity: sha512-RqPJI5pkqaSgDcSCEm47eokteTgniLZFD4j/Y7F7v5tTvUoinn5LaKGv8DttquA4OPiF9kq2h3ZadqRGTpUuYQ== + /@ckeditor/ckeditor5-editor-decoupled/27.0.0: + dependencies: + ckeditor5: 27.0.0 + lodash-es: 4.17.21 + dev: false + engines: + node: '>=12.0.0' + npm: '>=5.7.1' + resolution: + integrity: sha512-aOa2zM64cSyJoigoHeWqAVFyqtrgUey4vaTU7uAvCW/JiIaJwFKyrRUIyrMLRE355AJH7Y7vNWUCmgV+mXMN1w== + /@ckeditor/ckeditor5-engine/27.0.0: + dependencies: + '@ckeditor/ckeditor5-utils': 27.0.0 + lodash-es: 4.17.21 + dev: false + engines: + node: '>=12.0.0' + npm: '>=5.7.1' + resolution: + integrity: sha512-pOOf155t4HY9PFqZbXcEZf/M/foDze6ht60v9QNfBXQ62JNFCbfVm3R8EeFvJ7eQ1xuONx1CmvNbw3he/Ow2IQ== + /@ckeditor/ckeditor5-enter/27.0.0: + dependencies: + '@ckeditor/ckeditor5-core': 27.0.0 + '@ckeditor/ckeditor5-engine': 27.0.0 + '@ckeditor/ckeditor5-utils': 27.0.0 + dev: false + engines: + node: '>=12.0.0' + npm: '>=5.7.1' + resolution: + integrity: sha512-SBfztQEvtpWBf9JZ51ERwjiHBjGfVosOzyMIxLz010HXwswuuPY9NA2nLkYf534Vyu4Y4iPhMv5lgy80XyP/TQ== + /@ckeditor/ckeditor5-essentials/27.0.0: + dependencies: + ckeditor5: 27.0.0 + dev: false + engines: + node: '>=12.0.0' + npm: '>=5.7.1' + resolution: + integrity: sha512-yKHiPrCq9t1KysPIoHgth84jYpI4eAeNPrW87/Maauve/Vxir4JyYAwK6hfO6yNmty6h0LAu4M06orABDMh9DA== + /@ckeditor/ckeditor5-font/27.0.0: + dependencies: + ckeditor5: 27.0.0 + dev: false + engines: + node: '>=12.0.0' + npm: '>=5.7.1' + resolution: + integrity: sha512-VLmELvzjRMq/pDTfpzQ8dtDuIWmWYe6Y3PuUtHILWyq7BZy8/CKF4Pgd8bhateKVUXezcBFOSxZcIGSPoCGnoA== + /@ckeditor/ckeditor5-heading/27.0.0: + dependencies: + ckeditor5: 27.0.0 + dev: false + engines: + node: '>=12.0.0' + npm: '>=5.7.1' + resolution: + integrity: sha512-f+tIYVc05GxM4Y4FIt6NicvkMpF3pfMCnEksYN2A5eir9p7Tpfn5yvgcdDyCBpD69WoHP9IPQNYuqbfVp/7jMA== + /@ckeditor/ckeditor5-image/27.0.0: + dependencies: + '@ckeditor/ckeditor5-ui': 27.0.0 + ckeditor5: 27.0.0 + dev: false + engines: + node: '>=12.0.0' + npm: '>=5.7.1' + resolution: + integrity: sha512-N/t/UrGl4eFIpbgtzGIzZDDl1HDBWYpDRFrpieR34QH2A4ZHCN6pO2688pXQlWVfcIacFmJeaNwE0hj/6TZb2g== + /@ckeditor/ckeditor5-indent/27.0.0: + dependencies: + ckeditor5: 27.0.0 + dev: false + engines: + node: '>=12.0.0' + npm: '>=5.7.1' + resolution: + integrity: sha512-JKfuLis9zstH+w5tNbYrDe/4nOjZJOgzjjlrlHFLy4pk8I9trDS0mUga89FB9gGL/dBJvmmTU2s5skHlzHJLNA== + /@ckeditor/ckeditor5-link/27.0.0: + dependencies: + '@ckeditor/ckeditor5-ui': 27.0.0 + ckeditor5: 27.0.0 + lodash-es: 4.17.21 + dev: false + engines: + node: '>=12.0.0' + npm: '>=5.7.1' + resolution: + integrity: sha512-KK9aRG0bjUGRc1sf5PA7j5xD19Gcr/miA2Zwv/SSNq35sJhPoHchfK9t+BOEZvJU/wY53IeUPcf4M6yZ2J6KLw== + /@ckeditor/ckeditor5-list/27.0.0: + dependencies: + ckeditor5: 27.0.0 + dev: false + engines: + node: '>=12.0.0' + npm: '>=5.7.1' + resolution: + integrity: sha512-mIU06LeVahGhQ93wYKwEZcxA+YP8a/OzA3hDBwluqR+9hWGPSttDeD0t+JDaX4HCefVj0f9BvZvkWwRBZeB4nw== + /@ckeditor/ckeditor5-media-embed/27.0.0: + dependencies: + '@ckeditor/ckeditor5-ui': 27.0.0 + ckeditor5: 27.0.0 + dev: false + engines: + node: '>=12.0.0' + npm: '>=5.7.1' + resolution: + integrity: sha512-j1W0qKy/+v4E33z8nZgcAtBiftvxNQUWU9VlaJODNbX+qGV4ZucLHeA1FOzV3AQlwrZGcN7+mDQ91uuM5GUHcw== + /@ckeditor/ckeditor5-paragraph/27.0.0: + dependencies: + '@ckeditor/ckeditor5-core': 27.0.0 + '@ckeditor/ckeditor5-ui': 27.0.0 + '@ckeditor/ckeditor5-utils': 27.0.0 + dev: false + engines: + node: '>=12.0.0' + npm: '>=5.7.1' + resolution: + integrity: sha512-CzPSzYed4Hx7R7y8jXcaNUX0ZR8vAdPI3bNG625HaL084gA09cfLjprAEIRVXfFFYMRTUNMUrZesKCIhVS3E4w== + /@ckeditor/ckeditor5-paste-from-office/27.0.0: + dependencies: + ckeditor5: 27.0.0 + dev: false + engines: + node: '>=12.0.0' + npm: '>=5.7.1' + resolution: + integrity: sha512-ywvw9pJiLd3aYbnu/vPwmet9/dPa8B7kRljoxz9CiHBf/qN3QMXMd1vAtuVxN2ZtV4HIe36Opw4zINMcz7jiRQ== + /@ckeditor/ckeditor5-select-all/27.0.0: + dependencies: + '@ckeditor/ckeditor5-core': 27.0.0 + '@ckeditor/ckeditor5-ui': 27.0.0 + '@ckeditor/ckeditor5-utils': 27.0.0 + dev: false + engines: + node: '>=12.0.0' + npm: '>=5.7.1' + resolution: + integrity: sha512-JhWQMZYX96sJcqBt1qf7nLUn1IPEgKgffXKeiwwtTlgcQJeMrWMEkolybtlff0bedk+yqzzDlm9MHPUiFZNyiw== + /@ckeditor/ckeditor5-table/27.0.0: + dependencies: + ckeditor5: 27.0.0 + lodash-es: 4.17.21 + dev: false + engines: + node: '>=12.0.0' + npm: '>=5.7.1' + resolution: + integrity: sha512-SM1hStls9vi72WYY2Dm6uP7h5Vbf2M42WtaY7ez2Pwp3+LhGDeBECE2nH9ZovWOiZIsGluV7d0v1WwdZBzr/rg== + /@ckeditor/ckeditor5-typing/27.0.0: + dependencies: + '@ckeditor/ckeditor5-core': 27.0.0 + '@ckeditor/ckeditor5-engine': 27.0.0 + '@ckeditor/ckeditor5-utils': 27.0.0 + lodash-es: 4.17.21 + dev: false + engines: + node: '>=12.0.0' + npm: '>=5.7.1' + resolution: + integrity: sha512-VKM2eC0eINqpOSz8k6G9Qu0xwCUnhFaYyT9tG64XRifQSGm0+ED4ygkI0o++cwERYYXB8kLKMloC2rHNK9HnQQ== + /@ckeditor/ckeditor5-ui/27.0.0: + dependencies: + '@ckeditor/ckeditor5-utils': 27.0.0 + ckeditor5: 27.0.0 + lodash-es: 4.17.21 + dev: false + engines: + node: '>=12.0.0' + npm: '>=5.7.1' + resolution: + integrity: sha512-r+29zRUSBMEBUDRAN+rAxi/xPLxozEj9gLUHSvU0+uTiungq0X/hLj676J+JrIohiDe/DcuvZ/OwDi4n6v8i+g== + /@ckeditor/ckeditor5-undo/27.0.0: + dependencies: + '@ckeditor/ckeditor5-core': 27.0.0 + '@ckeditor/ckeditor5-engine': 27.0.0 + '@ckeditor/ckeditor5-ui': 27.0.0 + dev: false + engines: + node: '>=12.0.0' + npm: '>=5.7.1' + resolution: + integrity: sha512-FLe6MnC5j0L1BQIrRa4gob8W8GxUXQ8uMG+DvLik07TpaU5PXb/CzUIJz7vZgxhbj7tKd8L9AmfcGza9g0S3kA== + /@ckeditor/ckeditor5-upload/27.0.0: + dependencies: + '@ckeditor/ckeditor5-core': 27.0.0 + '@ckeditor/ckeditor5-ui': 27.0.0 + '@ckeditor/ckeditor5-utils': 27.0.0 + dev: false + engines: + node: '>=12.0.0' + npm: '>=5.7.1' + resolution: + integrity: sha512-ZlUpnK2jnMpcjYOz+TFsYQRG5WzXRjGgmsXDNG5gi0mCVwUovErvVFqzEHlXQR1TpKIsxgf5I0uPKPzMXpDOfg== + /@ckeditor/ckeditor5-utils/27.0.0: + dependencies: + lodash-es: 4.17.21 + dev: false + engines: + node: '>=12.0.0' + npm: '>=5.7.1' + resolution: + integrity: sha512-GYscRP6M73og0DRASbUmelL6SHP77F7XaoI3rZrHIXA+ykOp83v9b6I/5CiJdSvB8t/oHVq/ZoJ6s6CrkVFMEQ== + /@ckeditor/ckeditor5-watchdog/23.1.0: + dependencies: + lodash-es: 4.17.21 + dev: false + engines: + node: '>=12.0.0' + npm: '>=5.7.1' + resolution: + integrity: sha512-LQSWhsNPDsFPvxU+8O40pu/rPE2U7s4WobPcJm/6qKcM+WkTGdDG4rVuo5sackua3MulUeLGlfZSqFtX6Zsuag== + /@ckeditor/ckeditor5-widget/27.0.0: + dependencies: + '@ckeditor/ckeditor5-core': 27.0.0 + '@ckeditor/ckeditor5-engine': 27.0.0 + '@ckeditor/ckeditor5-enter': 27.0.0 + '@ckeditor/ckeditor5-typing': 27.0.0 + '@ckeditor/ckeditor5-ui': 27.0.0 + '@ckeditor/ckeditor5-utils': 27.0.0 + lodash-es: 4.17.21 + dev: false + engines: + node: '>=12.0.0' + npm: '>=5.7.1' + resolution: + integrity: sha512-kgu7EKO7jC4L27etNlbBpk76CKSZJqD9BgST0el+bPvF/cITItSOCVQttKgW/6peu0ROL6dWPjAhnLBjxNi3oA== + /@convergencelabs/monaco-collab-ext/0.3.2: + dependencies: + monaco-editor: 0.23.0 + dev: false + resolution: + integrity: sha512-yW5WI+pulIh4/fY5p2A9V8YkghQEfB6t33xqVAXkztjSG7MPBd9izlEtK13t0htVHk2vWj0JYhfWT853Gs60ow== /@fortawesome/fontawesome-free/5.15.2: dev: false engines: @@ -3075,6 +3450,26 @@ packages: webpack: '>=4.0.1' resolution: integrity: sha512-7p4Kn/gffhQaavNfyDFg7LS5S/UT1JAjyGd4UqR2+jzoYF02eDkj0Ec3+48TsIa4zghjLY87nQHIh/ecK9qLdw== + /ckeditor5/27.0.0: + dependencies: + '@ckeditor/ckeditor5-clipboard': 27.0.0 + '@ckeditor/ckeditor5-core': 27.0.0 + '@ckeditor/ckeditor5-engine': 27.0.0 + '@ckeditor/ckeditor5-enter': 27.0.0 + '@ckeditor/ckeditor5-paragraph': 27.0.0 + '@ckeditor/ckeditor5-select-all': 27.0.0 + '@ckeditor/ckeditor5-typing': 27.0.0 + '@ckeditor/ckeditor5-ui': 27.0.0 + '@ckeditor/ckeditor5-undo': 27.0.0 + '@ckeditor/ckeditor5-upload': 27.0.0 + '@ckeditor/ckeditor5-utils': 27.0.0 + '@ckeditor/ckeditor5-widget': 27.0.0 + dev: false + engines: + node: '>=12.0.0' + npm: '>=5.7.1' + resolution: + integrity: sha512-KjAjA/K5sNZ2UOaD3qxYiEv23TUdsZdNgDSMUiMjaFNv7bkoPZ4p8fEM4squzzB6VupLSjDw31WmTPF7Sf0KrA== /class-utils/0.3.6: dependencies: arr-union: 3.1.0 @@ -6620,6 +7015,10 @@ packages: dev: false resolution: integrity: sha512-JD1COMZsq8maT6mnuz1UMV0jvYD0E0aUsSOdrr1/nAG3dhqQXwRRgeW0cSqH1U43INKcqxaiVIQNOUDld7gRDA== + /lodash-es/4.17.21: + dev: false + resolution: + integrity: sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== /lodash._baseassign/3.2.0: dependencies: lodash._basecopy: 3.0.1 @@ -7104,6 +7503,10 @@ packages: dev: false resolution: integrity: sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ== + /monaco-editor/0.23.0: + dev: false + resolution: + integrity: sha512-q+CP5zMR/aFiMTE9QlIavGyGicKnG2v/H8qVvybLzeFsARM8f6G9fL0sMST2tyVYCwDKkGamZUI6647A0jR/Lg== /move-concurrently/1.0.1: dependencies: aproba: 1.2.0 @@ -10335,6 +10738,10 @@ packages: /tslib/2.1.0: resolution: integrity: sha512-hcVC3wYEziELGGmEEXue7D75zbwIIVUMWAVbHItGPx0ziyXxrOMQx4rQEVEV45Ut/1IotuEvwqPopzIOkDMf0A== + /tslib/2.2.0: + dev: false + resolution: + integrity: sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w== /tslint/5.15.0_typescript@4.0.7: dependencies: babel-code-frame: 6.26.0 @@ -11205,6 +11612,9 @@ specifiers: '@angular/router': ~10.1.5 '@angular/service-worker': ~10.1.5 '@circlon/angular-tree-component': ^10.0.0 + '@ckeditor/ckeditor5-angular': ^2.0.1 + '@ckeditor/ckeditor5-build-decoupled-document': ^27.0.0 + '@convergencelabs/monaco-collab-ext': ^0.3.2 '@fortawesome/fontawesome-free': ^5.15.1 '@ionic-native/core': ^5.0.0 '@ionic-native/splash-screen': ^5.0.0 diff --git a/src/app/components/editor/code/code.component.ts b/src/app/components/editor/code/code.component.ts index 63de0fe..3f5c1fc 100644 --- a/src/app/components/editor/code/code.component.ts +++ b/src/app/components/editor/code/code.component.ts @@ -1,16 +1,50 @@ -import {Component, ElementRef, Input, OnInit, ViewChild} from '@angular/core'; +import {Component, ElementRef, Input, OnDestroy, OnInit, ViewChild} from '@angular/core'; import {v4} from 'uuid'; import {ApiService, ResourceNotAvailableOfflineError} from '../../../service/api.service'; import {EditorNodeContract} from '../../nodes/EditorNode.contract'; import {EditorService} from '../../../service/editor.service'; import {EditorComponent} from 'ngx-monaco-editor'; +import * as MonacoCollabExt from '@convergencelabs/monaco-collab-ext'; +import {FlitterSocketConnection, FlitterSocketServerClientTransaction} from '../../../flitter-socket'; +import {debug} from '../../../utility'; +import {environment} from "../../../../environments/environment"; + +export interface RemoteUser { + uuid: string; + uid: string; + display: string; + color: string; + cursor?: any; + selection?: any; +} + +export interface RemoteInsertOperation { + type: 'insert'; + index: number; + text: string; +} + +export interface RemoteReplaceOperation { + type: 'replace'; + index: number; + text: string; + length: number; +} + +export interface RemoteDeleteOperation { + type: 'delete'; + index: number; + length: number; +} + +export type RemoteOperation = RemoteInsertOperation | RemoteReplaceOperation | RemoteDeleteOperation; @Component({ selector: 'editor-code', templateUrl: './code.component.html', styleUrls: ['./code.component.scss'], }) -export class CodeComponent extends EditorNodeContract implements OnInit { +export class CodeComponent extends EditorNodeContract implements OnInit, OnDestroy { @Input() nodeId: string; @Input() editorUUID?: string; @ViewChild('theEditor') theEditor: EditorComponent; @@ -21,6 +55,15 @@ export class CodeComponent extends EditorNodeContract implements OnInit { public notAvailableOffline = false; public containerHeight = 540; + protected cursorManager?: MonacoCollabExt.RemoteCursorManager; + protected selectionManager?: MonacoCollabExt.RemoteSelectionManager; + protected contentManager?: MonacoCollabExt.EditorContentManager; + + protected remoteUsers: RemoteUser[] = []; + protected localUser?: RemoteUser; + protected socket?: FlitterSocketConnection; + protected editorGroupID!: string; + public editorOptions = { theme: this.isDark() ? 'vs-dark' : 'vs', language: 'javascript', @@ -199,9 +242,36 @@ export class CodeComponent extends EditorNodeContract implements OnInit { ngOnInit() { this.editorService = this.editorService.getEditor(this.editorUUID); + this.editorService.registerNodeEditor(this.nodeId, this).then(() => { this.editorOptions.readOnly = !this.editorService.canEdit(); }); + + const url = `${environment.websocketBase}/api/v1/socket/code/.websocket`; + debug(`Editor socket URL: ${url}`); + + if ( !this.editorService.isVersion() ) { + const socket = new FlitterSocketConnection(url); + socket.controller(this); + + socket.on_open().then(() => { + debug('Connected to code editor socket', socket); + socket.asyncRequest('subscribe', { resource_id: this.node.Value.Value }).then(([transaction, _, data]) => { + debug('Subscribed to editor group:', data); + if ( data.editor_group_id ) { + this.editorGroupID = data.editor_group_id; + this.socket = socket; + this.localUser = data.local_user; + } + }); + }); + } + } + + ngOnDestroy() { + if ( this.socket ) { + this.socket.socket.close(); + } } onMonacoEditorInit(editor) { @@ -218,8 +288,164 @@ export class CodeComponent extends EditorNodeContract implements OnInit { } }; + editor.onDidChangeCursorPosition(event => { + this.socket?.asyncRequest('update_cursor', { + position: event.position, + uuid: this.localUser?.uuid, + editor_group_id: this.editorGroupID, + }); + }); + + editor.onDidChangeCursorSelection(event => { + this.socket?.asyncRequest('update_selection', { + startPosition: { + lineNumber: event.selection.startLineNumber, + column: event.selection.startColumn, + }, + endPosition: { + lineNumber: event.selection.endLineNumber, + column: event.selection.endColumn, + }, + uuid: this.localUser?.uuid, + editor_group_id: this.editorGroupID, + }); + }); + editor.onDidContentSizeChange(updateHeight); updateHeight(); + + this.cursorManager = new MonacoCollabExt.RemoteCursorManager({ + editor, + tooltips: true, + tooltipDuration: 2, + }); + + this.selectionManager = new MonacoCollabExt.RemoteSelectionManager({ editor }); + + this.contentManager = new MonacoCollabExt.EditorContentManager({ + editor, + onInsert: (index, text) => { + if ( this.readonly ) { + return; + } + + this.socket?.asyncRequest('apply', { + editor_group_id: this.editorGroupID, + operations: [ + { + type: 'insert', + index, + text, + }, + ], + }); + }, + onReplace: (index, length, text) => { + if ( this.readonly ) { + return; + } + + this.socket?.asyncRequest('apply', { + editor_group_id: this.editorGroupID, + operations: [ + { + type: 'replace', + index, + text, + length, + }, + ], + }); + }, + onDelete: (index, length) => { + if ( this.readonly ) { + return; + } + + this.socket?.asyncRequest('apply', { + editor_group_id: this.editorGroupID, + operations: [ + { + type: 'delete', + index, + length, + }, + ], + }); + }, + }); + } + + applyOperation(op: RemoteOperation) { + if ( op.type === 'insert' ) { + this.contentManager?.insert(op.index, op.text); + } else if ( op.type === 'replace' ) { + this.contentManager?.replace(op.index, op.length, op.text); + } else if ( op.type === 'delete' ) { + this.contentManager?.delete(op.index, op.length); + } + } + + async applyRemoteOperation(transaction: FlitterSocketServerClientTransaction, connection: FlitterSocketConnection) { + const ops: RemoteOperation[] = transaction.incoming.operations || []; + for ( const op of ops ) { + this.applyOperation(op); + } + } + + async updateCursorPosition(transaction: FlitterSocketServerClientTransaction, connection: FlitterSocketConnection) { + const position = transaction.incoming.position; + const uuid = transaction.incoming.uuid; + + for ( const user of this.remoteUsers ) { + if ( user.uuid === uuid ) { + user.cursor?.setPosition(position); + } + } + } + + async updateSelection(transaction: FlitterSocketServerClientTransaction, connection: FlitterSocketConnection) { + const startPosition = transaction.incoming.startPosition; + const endPosition = transaction.incoming.endPosition; + const uuid = transaction.incoming.uuid; + + for ( const user of this.remoteUsers ) { + if ( user.uuid === uuid ) { + if ( startPosition && endPosition ) { + user.selection?.setPositions(startPosition, endPosition); + user.selection?.show(); + } else { + user.selection?.hide(); + } + } + } + } + + async setEditorGroupUsers(transaction: FlitterSocketServerClientTransaction, connection: FlitterSocketConnection) { + const remoteUsers: RemoteUser[] = Array.isArray(transaction.incoming?.users) ? transaction.incoming.users : []; + console.log('set editor group users', remoteUsers, transaction); + for ( const user of this.remoteUsers ) { + this.cursorManager?.removeCursor(user.uuid); + user.cursor?.dispose(); + delete user.cursor; + + this.selectionManager?.removeSelection(user.uuid); + user.selection?.dispose(); + delete user.selection; + } + + while ( !this.cursorManager ) { + await new Promise(r => setTimeout(r, 500)); + } + + this.remoteUsers = remoteUsers; + for ( const user of this.remoteUsers ) { + user.cursor = this.cursorManager?.addCursor(user.uuid, user.color, user.display); + user.cursor?.setOffset(0); + user.cursor?.show(); + + user.selection = this.selectionManager?.addSelection(user.uuid, user.color); + } } public onEditorModelChange($event) { diff --git a/src/app/service/editor.service.ts b/src/app/service/editor.service.ts index 223932f..3ac1a8c 100644 --- a/src/app/service/editor.service.ts +++ b/src/app/service/editor.service.ts @@ -135,6 +135,10 @@ export class EditorService { await this.startEditing(this.currentPageId); } + isVersion() { + return !!this.currentPageVersion; + } + async startEditing(pageId: string, version?: number) { if ( this.currentPage ) { await this.stopEditing(); diff --git a/src/environments/environment.prod.ts b/src/environments/environment.prod.ts index 9d99ada..fd18e3d 100644 --- a/src/environments/environment.prod.ts +++ b/src/environments/environment.prod.ts @@ -8,4 +8,5 @@ export const environment = { starshipUrl: '/auth/starship_oauth/login', appBase: '/i/', outputDebug: false, + websocketBase: '', }; diff --git a/src/environments/environment.ts b/src/environments/environment.ts index b62e0b6..cfe9aea 100644 --- a/src/environments/environment.ts +++ b/src/environments/environment.ts @@ -12,6 +12,7 @@ export const environment = { starshipUrl: '/link_api/auth/starship_oauth/login', appBase: '/', outputDebug: true, + websocketBase: 'ws://localhost:8000' }; /*