diff --git a/package-lock.json b/package-lock.json index a529296b9..9aea41160 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,9 +8,38 @@ "name": "modus-web-components", "version": "1.0.0", "license": "MIT", + "dependencies": { + "stencil-quill": "^10.0.0" + }, "engines": { "node": ">=16.20.2" } + }, + "node_modules/@stencil/core": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@stencil/core/-/core-2.13.0.tgz", + "integrity": "sha512-EEKHOHgYpg3/iFUKMXTZJjUayRul7sXDwNw0OGgkEOe4t7JWiibDkzUHuruvpbqEydX+z1+ez5K2bMMY76c2wA==", + "peer": true, + "bin": { + "stencil": "bin/stencil" + }, + "engines": { + "node": ">=12.10.0", + "npm": ">=6.0.0" + } + } + }, + "dependencies": { + "@stencil/angular-output-target": { + "version": "https://registry.npmjs.org/@stencil/angular-output-target/-/angular-output-target-0.6.1-dev.11657573317.16e0205c.tgz", + "integrity": "sha512-dSD2d8itnD8xa5vRRoOFjWiSd813L+/vuulolDl22k7urBWdH8pGDlBu5uhatEjZD12d6C0blFBlWpy25YFjBw==", + "requires": {} + }, + "@stencil/core": { + "version": "2.13.0", + "resolved": "https://registry.npmjs.org/@stencil/core/-/core-2.13.0.tgz", + "integrity": "sha512-EEKHOHgYpg3/iFUKMXTZJjUayRul7sXDwNw0OGgkEOe4t7JWiibDkzUHuruvpbqEydX+z1+ez5K2bMMY76c2wA==", + "peer": true } } } diff --git a/package.json b/package.json index 7a6796c9b..4b0c196f0 100644 --- a/package.json +++ b/package.json @@ -22,5 +22,8 @@ }, "engines": { "node": ">=16.20.2" + }, + "dependencies": { + "stencil-quill": "^10.0.0" } } diff --git a/stencil-workspace/package-lock.json b/stencil-workspace/package-lock.json index 48b716197..c2f4512cc 100644 --- a/stencil-workspace/package-lock.json +++ b/stencil-workspace/package-lock.json @@ -11,7 +11,8 @@ "dependencies": { "@popperjs/core": "^2.11.8", "@stencil/core": "^4.12.4", - "@tanstack/table-core": "^8.20.5" + "@tanstack/table-core": "^8.20.5", + "quill": "^2.0.2" }, "devDependencies": { "@stencil/angular-output-target": "^0.9.0", @@ -3327,6 +3328,12 @@ "node": ">=0.10.0" } }, + "node_modules/eventemitter3": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", + "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", + "license": "MIT" + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -3415,6 +3422,12 @@ "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", "dev": true }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "license": "Apache-2.0" + }, "node_modules/fast-glob": { "version": "3.3.1", "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.1.tgz", @@ -5101,6 +5114,24 @@ "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, + "node_modules/lodash-es": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz", + "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", + "license": "MIT" + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==", + "license": "MIT" + }, + "node_modules/lodash.isequal": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", + "integrity": "sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==", + "license": "MIT" + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -5550,6 +5581,12 @@ "node": ">=6" } }, + "node_modules/parchment": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/parchment/-/parchment-3.0.0.tgz", + "integrity": "sha512-HUrJFQ/StvgmXRcQ1ftY6VEZUq3jA2t9ncFN4F84J/vN0/FPpQF+8FKXb3l6fLces6q0uOHj6NJn+2xvZnxO6A==", + "license": "BSD-3-Clause" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -6067,6 +6104,35 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/quill": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/quill/-/quill-2.0.2.tgz", + "integrity": "sha512-QfazNrhMakEdRG57IoYFwffUIr04LWJxbS/ZkidRFXYCQt63c1gK6Z7IHUXMx/Vh25WgPBU42oBaNzQ0K1R/xw==", + "license": "BSD-3-Clause", + "dependencies": { + "eventemitter3": "^5.0.1", + "lodash-es": "^4.17.21", + "parchment": "^3.0.0", + "quill-delta": "^5.1.0" + }, + "engines": { + "npm": ">=8.2.3" + } + }, + "node_modules/quill-delta": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/quill-delta/-/quill-delta-5.1.0.tgz", + "integrity": "sha512-X74oCeRI4/p0ucjb5Ma8adTXd9Scumz367kkMK5V/IatcX6A0vlgLgKbzXWy5nZmCGeNJm2oQX0d2Eqj+ZIlCA==", + "license": "MIT", + "dependencies": { + "fast-diff": "^1.3.0", + "lodash.clonedeep": "^4.5.0", + "lodash.isequal": "^4.5.0" + }, + "engines": { + "node": ">= 12.0.0" + } + }, "node_modules/react-is": { "version": "17.0.2", "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", diff --git a/stencil-workspace/package.json b/stencil-workspace/package.json index 4b926394e..33c58011c 100644 --- a/stencil-workspace/package.json +++ b/stencil-workspace/package.json @@ -43,7 +43,8 @@ "dependencies": { "@popperjs/core": "^2.11.8", "@stencil/core": "^4.12.4", - "@tanstack/table-core": "^8.20.5" + "@tanstack/table-core": "^8.20.5", + "quill": "^2.0.2" }, "devDependencies": { "@stencil/angular-output-target": "^0.9.0", diff --git a/stencil-workspace/src/components/modus-text-editor/modus-text-editor.scss b/stencil-workspace/src/components/modus-text-editor/modus-text-editor.scss new file mode 100644 index 000000000..d633aca17 --- /dev/null +++ b/stencil-workspace/src/components/modus-text-editor/modus-text-editor.scss @@ -0,0 +1,1077 @@ +.ql-container { + border-radius: 0 0 4px 4px; + font-family: Helvetica, Arial, sans-serif; + font-size: 13px; + height: 100%; + min-height: 200px; + margin: 0; + position: relative; +} + +.ql-container.disabled { + opacity: 50%; +} + +.ql-container.ql-disabled .ql-tooltip { + visibility: hidden; +} +.ql-container:not(.ql-disabled) li[data-list='checked'] > .ql-ui, +.ql-container:not(.ql-disabled) li[data-list='unchecked'] > .ql-ui { + cursor: pointer; +} +.ql-clipboard { + left: -100000px; + height: 1px; + overflow-y: hidden; + position: absolute; + top: 50%; +} +.ql-clipboard p { + margin: 0; + padding: 0; +} +.ql-editor { + box-sizing: border-box; + min-height: 200px; + counter-reset: list-0 list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9; + line-height: 1.42; + height: 100%; + outline: none; + overflow-y: auto; + padding: 12px 15px; + tab-size: 4; + -moz-tab-size: 4; + text-align: left; + white-space: pre-wrap; + word-wrap: break-word; +} +.ql-editor > * { + cursor: text; +} +.ql-editor p, +.ql-editor ol, +.ql-editor pre, +.ql-editor blockquote, +.ql-editor h1, +.ql-editor h2, +.ql-editor h3, +.ql-editor h4, +.ql-editor h5, +.ql-editor h6 { + margin: 0; + padding: 0; +} +@supports (counter-set: none) { + .ql-editor p, + .ql-editor h1, + .ql-editor h2, + .ql-editor h3, + .ql-editor h4, + .ql-editor h5, + .ql-editor h6 { + counter-set: list-0 list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9; + } +} +@supports not (counter-set: none) { + .ql-editor p, + .ql-editor h1, + .ql-editor h2, + .ql-editor h3, + .ql-editor h4, + .ql-editor h5, + .ql-editor h6 { + counter-reset: list-0 list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9; + } +} +.ql-editor table { + border-collapse: collapse; +} +.ql-editor td { + border: 1px solid #000; + padding: 2px 5px; +} +.ql-editor ol { + padding-left: 1.5em; +} +.ql-editor li { + list-style-type: none; + padding-left: 1.5em; + position: relative; +} +.ql-editor li > .ql-ui:before { + display: inline-block; + margin-left: -1.5em; + margin-right: 0.3em; + text-align: right; + white-space: nowrap; + width: 1.2em; +} +.ql-editor li[data-list='checked'] > .ql-ui, +.ql-editor li[data-list='unchecked'] > .ql-ui { + color: #777; +} +.ql-editor li[data-list='bullet'] > .ql-ui:before { + content: '\2022'; +} +.ql-editor li[data-list='checked'] > .ql-ui:before { + content: '\2611'; +} +.ql-editor li[data-list='unchecked'] > .ql-ui:before { + content: '\2610'; +} +@supports (counter-set: none) { + .ql-editor li[data-list] { + counter-set: list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9; + } +} +@supports not (counter-set: none) { + .ql-editor li[data-list] { + counter-reset: list-1 list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9; + } +} +.ql-editor li[data-list='ordered'] { + counter-increment: list-0; +} +.ql-editor li[data-list='ordered'] > .ql-ui:before { + content: counter(list-0, decimal) '. '; +} +.ql-editor li[data-list='ordered'].ql-indent-1 { + counter-increment: list-1; +} +.ql-editor li[data-list='ordered'].ql-indent-1 > .ql-ui:before { + content: counter(list-1, lower-alpha) '. '; +} +@supports (counter-set: none) { + .ql-editor li[data-list].ql-indent-1 { + counter-set: list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9; + } +} +@supports not (counter-set: none) { + .ql-editor li[data-list].ql-indent-1 { + counter-reset: list-2 list-3 list-4 list-5 list-6 list-7 list-8 list-9; + } +} +.ql-editor li[data-list='ordered'].ql-indent-2 { + counter-increment: list-2; +} +.ql-editor li[data-list='ordered'].ql-indent-2 > .ql-ui:before { + content: counter(list-2, lower-roman) '. '; +} +@supports (counter-set: none) { + .ql-editor li[data-list].ql-indent-2 { + counter-set: list-3 list-4 list-5 list-6 list-7 list-8 list-9; + } +} +@supports not (counter-set: none) { + .ql-editor li[data-list].ql-indent-2 { + counter-reset: list-3 list-4 list-5 list-6 list-7 list-8 list-9; + } +} +.ql-editor li[data-list='ordered'].ql-indent-3 { + counter-increment: list-3; +} +.ql-editor li[data-list='ordered'].ql-indent-3 > .ql-ui:before { + content: counter(list-3, decimal) '. '; +} +@supports (counter-set: none) { + .ql-editor li[data-list].ql-indent-3 { + counter-set: list-4 list-5 list-6 list-7 list-8 list-9; + } +} +@supports not (counter-set: none) { + .ql-editor li[data-list].ql-indent-3 { + counter-reset: list-4 list-5 list-6 list-7 list-8 list-9; + } +} +.ql-editor li[data-list='ordered'].ql-indent-4 { + counter-increment: list-4; +} +.ql-editor li[data-list='ordered'].ql-indent-4 > .ql-ui:before { + content: counter(list-4, lower-alpha) '. '; +} +@supports (counter-set: none) { + .ql-editor li[data-list].ql-indent-4 { + counter-set: list-5 list-6 list-7 list-8 list-9; + } +} +@supports not (counter-set: none) { + .ql-editor li[data-list].ql-indent-4 { + counter-reset: list-5 list-6 list-7 list-8 list-9; + } +} +.ql-editor li[data-list='ordered'].ql-indent-5 { + counter-increment: list-5; +} +.ql-editor li[data-list='ordered'].ql-indent-5 > .ql-ui:before { + content: counter(list-5, lower-roman) '. '; +} +@supports (counter-set: none) { + .ql-editor li[data-list].ql-indent-5 { + counter-set: list-6 list-7 list-8 list-9; + } +} +@supports not (counter-set: none) { + .ql-editor li[data-list].ql-indent-5 { + counter-reset: list-6 list-7 list-8 list-9; + } +} +.ql-editor li[data-list='ordered'].ql-indent-6 { + counter-increment: list-6; +} +.ql-editor li[data-list='ordered'].ql-indent-6 > .ql-ui:before { + content: counter(list-6, decimal) '. '; +} +@supports (counter-set: none) { + .ql-editor li[data-list].ql-indent-6 { + counter-set: list-7 list-8 list-9; + } +} +@supports not (counter-set: none) { + .ql-editor li[data-list].ql-indent-6 { + counter-reset: list-7 list-8 list-9; + } +} +.ql-editor li[data-list='ordered'].ql-indent-7 { + counter-increment: list-7; +} +.ql-editor li[data-list='ordered'].ql-indent-7 > .ql-ui:before { + content: counter(list-7, lower-alpha) '. '; +} +@supports (counter-set: none) { + .ql-editor li[data-list].ql-indent-7 { + counter-set: list-8 list-9; + } +} +@supports not (counter-set: none) { + .ql-editor li[data-list].ql-indent-7 { + counter-reset: list-8 list-9; + } +} +.ql-editor li[data-list='ordered'].ql-indent-8 { + counter-increment: list-8; +} +.ql-editor li[data-list='ordered'].ql-indent-8 > .ql-ui:before { + content: counter(list-8, lower-roman) '. '; +} +@supports (counter-set: none) { + .ql-editor li[data-list].ql-indent-8 { + counter-set: list-9; + } +} +@supports not (counter-set: none) { + .ql-editor li[data-list].ql-indent-8 { + counter-reset: list-9; + } +} +.ql-editor li[data-list='ordered'].ql-indent-9 { + counter-increment: list-9; +} +.ql-editor li[data-list='ordered'].ql-indent-9 > .ql-ui:before { + content: counter(list-9, decimal) '. '; +} +.ql-editor .ql-indent-1:not(.ql-direction-rtl) { + padding-left: 3em; +} +.ql-editor li.ql-indent-1:not(.ql-direction-rtl) { + padding-left: 4.5em; +} +.ql-editor .ql-indent-1.ql-direction-rtl.ql-align-right { + padding-right: 3em; +} +.ql-editor li.ql-indent-1.ql-direction-rtl.ql-align-right { + padding-right: 4.5em; +} +.ql-editor .ql-indent-2:not(.ql-direction-rtl) { + padding-left: 6em; +} +.ql-editor li.ql-indent-2:not(.ql-direction-rtl) { + padding-left: 7.5em; +} +.ql-editor .ql-indent-2.ql-direction-rtl.ql-align-right { + padding-right: 6em; +} +.ql-editor li.ql-indent-2.ql-direction-rtl.ql-align-right { + padding-right: 7.5em; +} +.ql-editor .ql-indent-3:not(.ql-direction-rtl) { + padding-left: 9em; +} +.ql-editor li.ql-indent-3:not(.ql-direction-rtl) { + padding-left: 10.5em; +} +.ql-editor .ql-indent-3.ql-direction-rtl.ql-align-right { + padding-right: 9em; +} +.ql-editor li.ql-indent-3.ql-direction-rtl.ql-align-right { + padding-right: 10.5em; +} +.ql-editor .ql-indent-4:not(.ql-direction-rtl) { + padding-left: 12em; +} +.ql-editor li.ql-indent-4:not(.ql-direction-rtl) { + padding-left: 13.5em; +} +.ql-editor .ql-indent-4.ql-direction-rtl.ql-align-right { + padding-right: 12em; +} +.ql-editor li.ql-indent-4.ql-direction-rtl.ql-align-right { + padding-right: 13.5em; +} +.ql-editor .ql-indent-5:not(.ql-direction-rtl) { + padding-left: 15em; +} +.ql-editor li.ql-indent-5:not(.ql-direction-rtl) { + padding-left: 16.5em; +} +.ql-editor .ql-indent-5.ql-direction-rtl.ql-align-right { + padding-right: 15em; +} +.ql-editor li.ql-indent-5.ql-direction-rtl.ql-align-right { + padding-right: 16.5em; +} +.ql-editor .ql-indent-6:not(.ql-direction-rtl) { + padding-left: 18em; +} +.ql-editor li.ql-indent-6:not(.ql-direction-rtl) { + padding-left: 19.5em; +} +.ql-editor .ql-indent-6.ql-direction-rtl.ql-align-right { + padding-right: 18em; +} +.ql-editor li.ql-indent-6.ql-direction-rtl.ql-align-right { + padding-right: 19.5em; +} +.ql-editor .ql-indent-7:not(.ql-direction-rtl) { + padding-left: 21em; +} +.ql-editor li.ql-indent-7:not(.ql-direction-rtl) { + padding-left: 22.5em; +} +.ql-editor .ql-indent-7.ql-direction-rtl.ql-align-right { + padding-right: 21em; +} +.ql-editor li.ql-indent-7.ql-direction-rtl.ql-align-right { + padding-right: 22.5em; +} +.ql-editor .ql-indent-8:not(.ql-direction-rtl) { + padding-left: 24em; +} +.ql-editor li.ql-indent-8:not(.ql-direction-rtl) { + padding-left: 25.5em; +} +.ql-editor .ql-indent-8.ql-direction-rtl.ql-align-right { + padding-right: 24em; +} +.ql-editor li.ql-indent-8.ql-direction-rtl.ql-align-right { + padding-right: 25.5em; +} +.ql-editor .ql-indent-9:not(.ql-direction-rtl) { + padding-left: 27em; +} +.ql-editor li.ql-indent-9:not(.ql-direction-rtl) { + padding-left: 28.5em; +} +.ql-editor .ql-indent-9.ql-direction-rtl.ql-align-right { + padding-right: 27em; +} +.ql-editor li.ql-indent-9.ql-direction-rtl.ql-align-right { + padding-right: 28.5em; +} +.ql-editor li.ql-direction-rtl { + padding-right: 1.5em; +} +.ql-editor li.ql-direction-rtl > .ql-ui:before { + margin-left: 0.3em; + margin-right: -1.5em; + text-align: left; +} +.ql-editor table { + table-layout: fixed; + width: 100%; +} +.ql-editor table td { + outline: none; +} +.ql-editor .ql-code-block-container { + font-family: monospace; +} +.ql-editor .ql-video { + display: block; + max-width: 100%; +} +.ql-editor .ql-video.ql-align-center { + margin: 0 auto; +} +.ql-editor .ql-video.ql-align-right { + margin: 0 0 0 auto; +} +.ql-editor .ql-bg-black { + background-color: #000; +} +.ql-editor .ql-bg-red { + background-color: #e60000; +} +.ql-editor .ql-bg-orange { + background-color: #f90; +} +.ql-editor .ql-bg-yellow { + background-color: #ff0; +} +.ql-editor .ql-bg-green { + background-color: #008a00; +} +.ql-editor .ql-bg-blue { + background-color: #06c; +} +.ql-editor .ql-bg-purple { + background-color: #93f; +} +.ql-editor .ql-color-white { + color: #fff; +} +.ql-editor .ql-color-red { + color: #e60000; +} +.ql-editor .ql-color-orange { + color: #f90; +} +.ql-editor .ql-color-yellow { + color: #ff0; +} +.ql-editor .ql-color-green { + color: #008a00; +} +.ql-editor .ql-color-blue { + color: #06c; +} +.ql-editor .ql-color-purple { + color: #93f; +} +.ql-editor .ql-font-serif { + font-family: + Georgia, + Times New Roman, + serif; +} +.ql-editor .ql-font-monospace { + font-family: + Monaco, + Courier New, + monospace; +} +.ql-editor .ql-size-small { + font-size: 0.75em; +} +.ql-editor .ql-size-large { + font-size: 1.5em; +} +.ql-editor .ql-size-huge { + font-size: 2.5em; +} +.ql-editor .ql-direction-rtl { + direction: rtl; + text-align: inherit; +} +.ql-editor .ql-align-center { + text-align: center; +} +.ql-editor .ql-align-justify { + text-align: justify; +} +.ql-editor .ql-align-right { + text-align: right; +} +.ql-editor .ql-ui { + position: absolute; +} +.ql-editor.ql-blank::before { + color: rgba(0, 0, 0, 0.6); + content: attr(data-placeholder); + font-style: italic; + left: 15px; + pointer-events: none; + position: absolute; + right: 15px; +} + +.ql-toolbar { + border-radius: 4px 4px 0 0; +} + +.ql-snow.ql-toolbar:after, +.ql-snow .ql-toolbar:after { + clear: both; + content: ''; + display: table; +} +.ql-snow.ql-toolbar button, +.ql-snow .ql-toolbar button { + background: none; + border: none; + cursor: pointer; + display: flex; + float: left; + height: 24px; + padding: 3px 5px; + width: 28px; + align-items: center; + justify-content: center; +} +.ql-snow.ql-toolbar button svg, +.ql-snow .ql-toolbar button svg { + float: left; + height: 100%; +} +.ql-snow.ql-toolbar button:active:hover, +.ql-snow .ql-toolbar button:active:hover { + outline: none; +} +.ql-snow.ql-toolbar input.ql-image[type='file'], +.ql-snow .ql-toolbar input.ql-image[type='file'] { + display: none; +} +.ql-snow.ql-toolbar button:hover, +.ql-snow .ql-toolbar button:hover, +.ql-snow.ql-toolbar button:focus, +.ql-snow .ql-toolbar button:focus, +.ql-snow.ql-toolbar button.ql-active, +.ql-snow .ql-toolbar button.ql-active, +.ql-snow.ql-toolbar .ql-picker-label:hover, +.ql-snow .ql-toolbar .ql-picker-label:hover, +.ql-snow.ql-toolbar .ql-picker-label.ql-active, +.ql-snow .ql-toolbar .ql-picker-label.ql-active, +.ql-snow.ql-toolbar .ql-picker-item:hover, +.ql-snow .ql-toolbar .ql-picker-item:hover, +.ql-snow.ql-toolbar .ql-picker-item.ql-selected, +.ql-snow .ql-toolbar .ql-picker-item.ql-selected { + color: #06c; +} +.ql-snow.ql-toolbar button:hover .ql-fill, +.ql-snow .ql-toolbar button:hover .ql-fill, +.ql-snow.ql-toolbar button:focus .ql-fill, +.ql-snow .ql-toolbar button:focus .ql-fill, +.ql-snow.ql-toolbar button.ql-active .ql-fill, +.ql-snow .ql-toolbar button.ql-active .ql-fill, +.ql-snow.ql-toolbar .ql-picker-label:hover .ql-fill, +.ql-snow .ql-toolbar .ql-picker-label:hover .ql-fill, +.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-fill, +.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-fill, +.ql-snow.ql-toolbar .ql-picker-item:hover .ql-fill, +.ql-snow .ql-toolbar .ql-picker-item:hover .ql-fill, +.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-fill, +.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-fill, +.ql-snow.ql-toolbar button:hover .ql-stroke.ql-fill, +.ql-snow .ql-toolbar button:hover .ql-stroke.ql-fill, +.ql-snow.ql-toolbar button:focus .ql-stroke.ql-fill, +.ql-snow .ql-toolbar button:focus .ql-stroke.ql-fill, +.ql-snow.ql-toolbar button.ql-active .ql-stroke.ql-fill, +.ql-snow .ql-toolbar button.ql-active .ql-stroke.ql-fill, +.ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill, +.ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke.ql-fill, +.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill, +.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke.ql-fill, +.ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill, +.ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke.ql-fill, +.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill, +.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke.ql-fill { + fill: #06c; +} +.ql-snow.ql-toolbar button:hover .ql-stroke, +.ql-snow .ql-toolbar button:hover .ql-stroke, +.ql-snow.ql-toolbar button:focus .ql-stroke, +.ql-snow .ql-toolbar button:focus .ql-stroke, +.ql-snow.ql-toolbar button.ql-active .ql-stroke, +.ql-snow .ql-toolbar button.ql-active .ql-stroke, +.ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke, +.ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke, +.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke, +.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke, +.ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke, +.ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke, +.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke, +.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke, +.ql-snow.ql-toolbar button:hover .ql-stroke-miter, +.ql-snow .ql-toolbar button:hover .ql-stroke-miter, +.ql-snow.ql-toolbar button:focus .ql-stroke-miter, +.ql-snow .ql-toolbar button:focus .ql-stroke-miter, +.ql-snow.ql-toolbar button.ql-active .ql-stroke-miter, +.ql-snow .ql-toolbar button.ql-active .ql-stroke-miter, +.ql-snow.ql-toolbar .ql-picker-label:hover .ql-stroke-miter, +.ql-snow .ql-toolbar .ql-picker-label:hover .ql-stroke-miter, +.ql-snow.ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter, +.ql-snow .ql-toolbar .ql-picker-label.ql-active .ql-stroke-miter, +.ql-snow.ql-toolbar .ql-picker-item:hover .ql-stroke-miter, +.ql-snow .ql-toolbar .ql-picker-item:hover .ql-stroke-miter, +.ql-snow.ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter, +.ql-snow .ql-toolbar .ql-picker-item.ql-selected .ql-stroke-miter { + stroke: #06c; +} +@media (pointer: coarse) { + .ql-snow.ql-toolbar button:hover:not(.ql-active), + .ql-snow .ql-toolbar button:hover:not(.ql-active) { + color: #444; + } + .ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-fill, + .ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-fill, + .ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill, + .ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke.ql-fill { + fill: #444; + } + .ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke, + .ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke, + .ql-snow.ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter, + .ql-snow .ql-toolbar button:hover:not(.ql-active) .ql-stroke-miter { + stroke: #444; + } +} +.ql-snow { + box-sizing: border-box; +} +.ql-snow * { + box-sizing: border-box; +} +.ql-snow .ql-hidden { + display: none; +} +.ql-snow .ql-out-bottom, +.ql-snow .ql-out-top { + visibility: hidden; +} +.ql-snow .ql-tooltip { + position: absolute; + transform: translateY(10px); +} +.ql-snow .ql-tooltip a { + cursor: pointer; + text-decoration: none; +} +.ql-snow .ql-tooltip.ql-flip { + transform: translateY(-10px); +} +.ql-snow .ql-formats { + display: inline-block; + vertical-align: middle; +} +.ql-snow .ql-formats:after { + clear: both; + content: ''; + display: table; +} +.ql-snow .ql-stroke { + fill: none; + stroke: #444; + stroke-linecap: round; + stroke-linejoin: round; + stroke-width: 2; +} +.ql-snow .ql-stroke-miter { + fill: none; + stroke: #444; + stroke-miterlimit: 10; + stroke-width: 2; +} +.ql-snow .ql-fill, +.ql-snow .ql-stroke.ql-fill { + fill: #444; +} +.ql-snow .ql-empty { + fill: none; +} +.ql-snow .ql-even { + fill-rule: evenodd; +} +.ql-snow .ql-thin, +.ql-snow .ql-stroke.ql-thin { + stroke-width: 1; +} +.ql-snow .ql-transparent { + opacity: 0.4; +} +.ql-snow .ql-direction svg:last-child { + display: none; +} +.ql-snow .ql-direction.ql-active svg:last-child { + display: inline; +} +.ql-snow .ql-direction.ql-active svg:first-child { + display: none; +} +.ql-snow .ql-editor h1 { + font-size: 2em; +} +.ql-snow .ql-editor h2 { + font-size: 1.5em; +} +.ql-snow .ql-editor h3 { + font-size: 1.17em; +} +.ql-snow .ql-editor h4 { + font-size: 1em; +} +.ql-snow .ql-editor h5 { + font-size: 0.83em; +} +.ql-snow .ql-editor h6 { + font-size: 0.67em; +} +.ql-snow .ql-editor a { + text-decoration: underline; +} +.ql-snow .ql-editor blockquote { + border-left: 4px solid #ccc; + margin-bottom: 5px; + margin-top: 5px; + padding-left: 16px; +} +.ql-snow .ql-editor code, +.ql-snow .ql-editor .ql-code-block-container { + background-color: #f0f0f0; + border-radius: 3px; +} +.ql-snow .ql-editor .ql-code-block-container { + margin-bottom: 5px; + margin-top: 5px; + padding: 5px 10px; +} +.ql-snow .ql-editor code { + font-size: 85%; + padding: 2px 4px; +} +.ql-snow .ql-editor .ql-code-block-container { + background-color: #23241f; + color: #f8f8f2; + overflow: visible; +} +.ql-snow .ql-editor img { + max-width: 100%; +} +.ql-snow .ql-picker { + color: #444; + display: inline-block; + font-size: 14px; + font-weight: 500; + height: 24px; + position: relative; + vertical-align: middle; +} +.ql-snow .ql-picker-label { + cursor: pointer; + display: inline-block; + height: 100%; + padding-left: 8px; + padding-right: 2px; + position: relative; + width: 100%; +} +.ql-snow .ql-picker-label::before { + display: inline-block; + line-height: 22px; +} +.ql-snow .ql-picker-options { + background-color: #fff; + display: none; + min-width: 100%; + padding: 4px 8px; + position: absolute; + white-space: nowrap; +} +.ql-snow .ql-picker-options .ql-picker-item { + cursor: pointer; + display: block; + padding-bottom: 5px; + padding-top: 5px; +} +.ql-snow .ql-picker.ql-expanded .ql-picker-label { + color: #ccc; + z-index: 2; +} +.ql-snow .ql-picker.ql-expanded .ql-picker-label .ql-fill { + fill: #ccc; +} +.ql-snow .ql-picker.ql-expanded .ql-picker-label .ql-stroke { + stroke: #ccc; +} +.ql-snow .ql-picker.ql-expanded .ql-picker-options { + display: block; + margin-top: -1px; + top: 100%; + z-index: 1; +} +.ql-snow .ql-color-picker, +.ql-snow .ql-icon-picker { + width: 28px; +} +.ql-snow .ql-color-picker .ql-picker-label, +.ql-snow .ql-icon-picker .ql-picker-label { + padding: 2px 4px; +} +.ql-snow .ql-color-picker .ql-picker-label svg, +.ql-snow .ql-icon-picker .ql-picker-label svg { + right: 4px; +} +.ql-snow .ql-icon-picker .ql-picker-options { + padding: 4px 0; +} +.ql-snow .ql-icon-picker .ql-picker-item { + height: 24px; + width: 24px; + padding: 2px 4px; +} +.ql-snow .ql-color-picker .ql-picker-options { + padding: 3px 5px; + width: 152px; +} +.ql-snow .ql-color-picker .ql-picker-item { + border: 1px solid transparent; + float: left; + height: 16px; + margin: 2px; + padding: 0; + width: 16px; +} +.ql-snow .ql-picker:not(.ql-color-picker):not(.ql-icon-picker) svg { + position: absolute; + margin-top: -9px; + right: 0; + top: 50%; + width: 18px; +} +.ql-snow .ql-picker.ql-header .ql-picker-label[data-label]:not([data-label=''])::before, +.ql-snow .ql-picker.ql-font .ql-picker-label[data-label]:not([data-label=''])::before, +.ql-snow .ql-picker.ql-size .ql-picker-label[data-label]:not([data-label=''])::before, +.ql-snow .ql-picker.ql-header .ql-picker-item[data-label]:not([data-label=''])::before, +.ql-snow .ql-picker.ql-font .ql-picker-item[data-label]:not([data-label=''])::before, +.ql-snow .ql-picker.ql-size .ql-picker-item[data-label]:not([data-label=''])::before { + content: attr(data-label); +} +.ql-snow .ql-picker.ql-header { + width: 98px; +} +.ql-snow .ql-picker.ql-size .ql-picker-label::before, +.ql-snow .ql-picker.ql-size .ql-picker-item::before { + content: '14px'; +} +.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='14px']::before, +.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='14px']::before { + content: '14px'; + font-size: 14px !important; +} +.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='16px']::before, +.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='16px']::before { + content: '16px'; + font-size: 16px !important; +} +.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='18px']::before, +.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='18px']::before { + content: '18px'; + font-size: 18px !important; +} +.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='1']::before, +.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='1']::before { + content: 'Heading 1'; +} +.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='2']::before, +.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='2']::before { + content: 'Heading 2'; +} +.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='3']::before, +.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='3']::before { + content: 'Heading 3'; +} +.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='4']::before, +.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='4']::before { + content: 'Heading 4'; +} +.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='5']::before, +.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='5']::before { + content: 'Heading 5'; +} +.ql-snow .ql-picker.ql-header .ql-picker-label[data-value='6']::before, +.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='6']::before { + content: 'Heading 6'; +} +.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='1']::before { + font-size: 2em; +} +.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='2']::before { + font-size: 1.5em; +} +.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='3']::before { + font-size: 1.17em; +} +.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='4']::before { + font-size: 1em; +} +.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='5']::before { + font-size: 0.83em; +} +.ql-snow .ql-picker.ql-header .ql-picker-item[data-value='6']::before { + font-size: 0.67em; +} +.ql-snow .ql-picker.ql-font { + width: 108px; +} +.ql-snow .ql-picker.ql-font .ql-picker-label::before, +.ql-snow .ql-picker.ql-font .ql-picker-item::before { + content: 'Sans Serif'; +} +.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='serif']::before, +.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='serif']::before { + content: 'Serif'; +} +.ql-snow .ql-picker.ql-font .ql-picker-label[data-value='monospace']::before, +.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='monospace']::before { + content: 'Monospace'; +} +.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='serif']::before { + font-family: + Georgia, + Times New Roman, + serif; +} +.ql-snow .ql-picker.ql-font .ql-picker-item[data-value='monospace']::before { + font-family: + Monaco, + Courier New, + monospace; +} +.ql-snow .ql-picker.ql-size { + width: 98px; +} +.ql-snow .ql-picker.ql-size .ql-picker-label::before, +.ql-snow .ql-picker.ql-size .ql-picker-item::before { + content: 'Normal'; +} +.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='small']::before, +.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='small']::before { + content: 'Small'; +} +.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='large']::before, +.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='large']::before { + content: 'Large'; +} +.ql-snow .ql-picker.ql-size .ql-picker-label[data-value='huge']::before, +.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='huge']::before { + content: 'Huge'; +} +.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='small']::before { + font-size: 10px; +} +.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='large']::before { + font-size: 18px; +} +.ql-snow .ql-picker.ql-size .ql-picker-item[data-value='huge']::before { + font-size: 32px; +} +.ql-snow .ql-color-picker.ql-background .ql-picker-item { + background-color: #fff; +} +.ql-snow .ql-color-picker.ql-color .ql-picker-item { + background-color: #000; +} +.ql-code-block-container { + position: relative; +} +.ql-code-block-container .ql-ui { + right: 5px; + top: 5px; +} +.ql-toolbar.ql-snow { + border: 1px solid #ccc; + box-sizing: border-box; + font-family: 'Helvetica Neue', 'Helvetica', 'Arial', sans-serif; + padding: 8px; +} + +.ql-toolbar.ql-snow .ql-formats:not(:last-child) { + border-right: 1px solid #ccc; +} +.ql-toolbar.ql-snow .ql-picker-label { + border: 1px solid transparent; +} +.ql-toolbar.ql-snow .ql-picker-options { + border: 1px solid transparent; + box-shadow: rgba(0, 0, 0, 0.2) 0 2px 8px; +} +.ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-label { + border-color: #ccc; +} +.ql-toolbar.ql-snow .ql-picker.ql-expanded .ql-picker-options { + border-color: #ccc; +} +.ql-toolbar.ql-snow .ql-color-picker .ql-picker-item.ql-selected, +.ql-toolbar.ql-snow .ql-color-picker .ql-picker-item:hover { + border-color: #000; +} +.ql-toolbar.ql-snow + .ql-container.ql-snow { + border-top: 0; +} +.ql-snow .ql-tooltip { + background-color: #fff; + border: 1px solid #ccc; + box-shadow: 0 0 5px #ddd; + color: #444; + padding: 5px 12px; + white-space: nowrap; +} +.ql-snow .ql-tooltip::before { + content: 'Visit URL:'; + line-height: 26px; + margin-right: 8px; +} +.ql-snow .ql-tooltip input[type='text'] { + display: none; + border: 1px solid #ccc; + font-size: 13px; + height: 26px; + margin: 0; + padding: 3px 5px; + width: 170px; +} +.ql-snow .ql-tooltip a.ql-preview { + display: inline-block; + max-width: 200px; + overflow-x: hidden; + text-overflow: ellipsis; + vertical-align: top; +} +.ql-snow .ql-tooltip a.ql-action::after { + border-right: 1px solid #ccc; + content: 'Edit'; + margin-left: 16px; + padding-right: 8px; +} +.ql-snow .ql-tooltip a.ql-remove::before { + content: 'Remove'; + margin-left: 8px; +} +.ql-snow .ql-tooltip a { + line-height: 26px; +} +.ql-snow .ql-tooltip.ql-editing a.ql-preview, +.ql-snow .ql-tooltip.ql-editing a.ql-remove { + display: none; +} +.ql-snow .ql-tooltip.ql-editing input[type='text'] { + display: inline-block; +} +.ql-snow .ql-tooltip.ql-editing a.ql-action::after { + border-right: 0; + content: 'Save'; + padding-right: 0; +} +.ql-snow .ql-tooltip[data-mode='link']::before { + content: 'Enter link:'; +} +.ql-snow .ql-tooltip[data-mode='formula']::before { + content: 'Enter formula:'; +} +.ql-snow .ql-tooltip[data-mode='video']::before { + content: 'Enter video:'; +} +.ql-snow a { + color: #06c; +} +.ql-container.ql-snow { + border: 1px solid #ccc; +} diff --git a/stencil-workspace/src/components/modus-text-editor/modus-text-editor.tsx b/stencil-workspace/src/components/modus-text-editor/modus-text-editor.tsx new file mode 100644 index 000000000..05e145781 --- /dev/null +++ b/stencil-workspace/src/components/modus-text-editor/modus-text-editor.tsx @@ -0,0 +1,129 @@ +// eslint-disable-next-line @typescript-eslint/no-unused-vars +import { Component, h, Prop, Element, Event, EventEmitter } from '@stencil/core'; +import Quill from 'quill'; +import { Attributor } from 'parchment'; +import { ModusIconMap } from '../../icons/ModusIconMap'; +import { convertIconToSVG } from '../../utils/utils'; +//import { Delta } from 'quill/core'; + +@Component({ + tag: 'modus-text-editor', + styleUrl: 'modus-text-editor.scss', +}) +export class ModusTextEditor { + /** (optional) Content of the editor. */ + @Prop() content: string; + + /** (optional) Disables the editor. */ + @Prop() disabled = false; + + /** (optional) The placeholder text of the editor. */ + @Prop() placeholder = ''; + + /** An event that fires on input value change. */ + @Event() textChange: EventEmitter<{ delta: unknown; oldDelta: unknown; source: string }>; + + /** An event that fires on selection change. */ + @Event() selectionUpdate: EventEmitter<{ range: unknown; oldRange: unknown; source: string }>; + + /** An event that fires on editor change. */ + @Event() editorChange: EventEmitter<{ eventName: string; args: unknown[] }>; + + private quillInstance: Quill; + + private fontSizeArr = ['14px', '16px', '18px']; + + @Element() hostElement: HTMLElement; + + setIcons() { + const icons = Quill.import('ui/icons'); + icons['bold'] = convertIconToSVG(); + icons['italic'] = convertIconToSVG(); + icons['underline'] = convertIconToSVG(); + icons['strike'] = convertIconToSVG(); + icons['align'][''] = convertIconToSVG(); + icons['align']['center'] = convertIconToSVG(); + icons['align']['right'] = convertIconToSVG(); + icons['list']['bullet'] = convertIconToSVG(); + icons['list']['ordered'] = convertIconToSVG(); + icons['link'] = convertIconToSVG(); + } + + setFontSize() { + const fontSize = Quill.import('attributors/style/size') as Attributor; + fontSize.whitelist = this.fontSizeArr; + Quill.register(fontSize, true); + } + + setCaretIcons() { + const fontPicker = this.hostElement.querySelector('.ql-font .ql-picker-label svg'); + const fontSizePicker = this.hostElement.querySelector('.ql-size .ql-picker-label svg'); + + fontPicker.innerHTML = convertIconToSVG(); + fontSizePicker.innerHTML = convertIconToSVG(); + } + + componentDidRender() { + if (!this.quillInstance) { + this.initializeQuillEditor(); + } + } + + initializeQuillEditor() { + const editorContainer = this.hostElement.querySelector('.editor-container') as HTMLElement; + + this.setIcons(); + this.setFontSize(); + + const toolbarOptions = { + container: [ + [{ font: ['serif', 'monospace'] }], + [{ size: this.fontSizeArr }], + ['bold', 'italic', 'underline', 'strike'], + [{ align: '' }, { align: 'center' }, { align: 'right' }], + [{ list: 'bullet' }, { list: 'ordered' }], + ['link'], + ], + }; + + if (editorContainer) { + this.quillInstance = new Quill(editorContainer, { + modules: { + toolbar: toolbarOptions, + }, + placeholder: this?.placeholder, + theme: 'snow', + }); + + if (this.content) { + this.quillInstance.setText(this.content); + } + if (this.disabled) { + this.quillInstance.enable(false); + } + + this.attachQuillEventListeners(); + } + + this.setCaretIcons(); + } + + attachQuillEventListeners() { + this.quillInstance.on('text-change', (delta, oldDelta, source) => { + this.textChange.emit({ delta, oldDelta, source }); + }); + + this.quillInstance.on('selection-change', (range, oldRange, source) => { + this.selectionUpdate.emit({ range, oldRange, source }); + }); + + this.quillInstance.on('editor-change', (eventName, ...args) => { + this.editorChange.emit({ eventName, args }); + }); + } + + render() { + const editorContainerClass = `editor-container ${this.disabled ? 'disabled' : ''}`; + return
; + } +} diff --git a/stencil-workspace/src/components/modus-text-editor/quill-editor.tsx b/stencil-workspace/src/components/modus-text-editor/quill-editor.tsx new file mode 100644 index 000000000..909ee4a6d --- /dev/null +++ b/stencil-workspace/src/components/modus-text-editor/quill-editor.tsx @@ -0,0 +1,352 @@ +import { h, Component, ComponentDidLoad, Element, Event, EventEmitter, Prop, Watch, Host } from '@stencil/core'; + +declare const Quill: any; + +@Component({ + tag: 'quill-editor', + scoped: true, + shadow: false, +}) +export class QuillEditorComponent implements ComponentDidLoad { + @Event() editorInit: EventEmitter; + @Event() editorChange: EventEmitter< + | { + editor: any; + event: 'text-change'; + content: any; + text: string; + html: string; + delta: any; + oldDelta: any; + source: string; + } + | { + editor: any; + event: 'selection-change'; + range: any; + oldRange: any; + source: string; + } + >; + @Event() editorContentChange: EventEmitter<{ + editor: any; + content: any; + text: string; + html: string; + delta: any; + oldDelta: any; + source: string; + }>; + @Event() editorSelectionChange: EventEmitter<{ + editor: any; + range: any; + oldRange: any; + source: string; + }>; + @Event() editorFocus: EventEmitter<{ + editor: any; + source: string; + }>; + @Event() editorBlur: EventEmitter<{ + editor: any; + source: string; + }>; + + @Element() wrapperElement: HTMLElement; + + @Prop() format: 'html' | 'text' | 'json' = 'html'; + @Prop() bounds: HTMLElement | string; + @Prop() content: string; + @Prop() debug = 'warn'; + @Prop() formats: string[]; + @Prop() modules?: string; + @Prop() placeholder = 'Insert text here ...'; + @Prop() readOnly: boolean; + @Prop() styles = '{}'; + @Prop() theme = 'snow'; + @Prop() customToolbarPosition: 'top' | 'bottom' = 'top'; + @Prop() preserveWhitespace = false; + + quillEditor: any; + editorElement: HTMLDivElement | HTMLPreElement; + private defaultModules = { + toolbar: [ + ['bold', 'italic', 'underline', 'strike'], // toggled buttons + ['blockquote', 'code-block'], + + [{ header: 1 }, { header: 2 }], // custom button values + [{ list: 'ordered' }, { list: 'bullet' }], + [{ script: 'sub' }, { script: 'super' }], // superscript/subscript + [{ indent: '-1' }, { indent: '+1' }], // outdent/indent + [{ direction: 'rtl' }], // text direction + + [{ size: ['small', false, 'large', 'huge'] }], // custom dropdown + [{ header: [1, 2, 3, 4, 5, 6, false] }], + + [{ color: [].slice() }, { background: [].slice() }], // dropdown with defaults from theme + [{ font: [].slice() }], + [{ align: [].slice() }], + + ['clean'], // remove formatting button + + ['link', 'image', 'video'], // link and image, video + ], + }; + + selectionChangeEvent: any; + textChangeEvent: any; + editorChangeEvent: any; + + setEditorContent(value: any) { + if (this.format === 'html') { + const contents = this.quillEditor.clipboard.convert(value); + this.quillEditor.setContents(contents, 'api'); + } else if (this.format === 'text') { + this.quillEditor.setText(value, 'api'); + } else if (this.format === 'json') { + try { + this.quillEditor.setContents(JSON.parse(value), 'api'); + } catch (e) { + this.quillEditor.setText(value, 'api'); + } + } else { + this.quillEditor.setText(value, 'api'); + } + } + + getEditorContent() { + const text = this.quillEditor.getText(); + const content = this.quillEditor.getContents(); + + let html: string | null = this.editorElement.children[0].innerHTML; + if (html === '


' || html === '

') { + html = ''; + } + + if (this.format === 'html') { + return html; + } else if (this.format === 'text') { + return text; + } else if (this.format === 'json') { + try { + return JSON.stringify(content); + } catch (e) { + return text; + } + } else { + return text; + } + } + + componentDidLoad() { + this.editorElement = this.preserveWhitespace ? document.createElement('pre') : document.createElement('div'); + this.editorElement.setAttribute('quill-editor', ''); + + const modules: any = this.modules ? JSON.parse(this.modules) : this.defaultModules; + + const toolbarElem = this.wrapperElement.querySelector('[slot="quill-toolbar"]'); + if (toolbarElem) { + modules['toolbar'] = toolbarElem; + if (this.customToolbarPosition === 'bottom') { + this.wrapperElement.prepend(this.editorElement); + } else { + this.wrapperElement.append(this.editorElement); + } + } else { + this.wrapperElement.append(this.editorElement); + } + + this.quillEditor = new Quill(this.editorElement, { + debug: this.debug, + modules: modules, + placeholder: this.placeholder, + readOnly: this.readOnly || false, + theme: this.theme || 'snow', + formats: this.formats, + bounds: this.bounds ? (this.bounds === 'self' ? this.editorElement : this.bounds) : document.body, + }); + + if (this.styles) { + const styles = JSON.parse(this.styles); + Object.keys(styles).forEach((key: string) => { + this.editorElement.style.setProperty(key, styles[key]); + }); + } + + if (this.content) { + this.setEditorContent(this.content); + + this.quillEditor['history'].clear(); + } + + this.editorChangeEvent = this.quillEditor.on( + 'editor-change', + ( + event: 'text-change' | 'selection-change', + current: any | Range | null, + old: any | Range | null, + source: string + ): void => { + // only emit changes emitted by user interactions + + if (event === 'text-change') { + const text = this.quillEditor.getText(); + const content = this.quillEditor.getContents(); + + let html: string | null = this.editorElement.querySelector('.ql-editor')!.innerHTML; + if (html === '


' || html === '

') { + html = null; + } + + this.editorChange.emit({ + content, + delta: current, + editor: this.quillEditor, + event, + html, + oldDelta: old, + source, + text, + }); + } else { + this.editorChange.emit({ + editor: this.quillEditor, + event, + oldRange: old, + range: current, + source, + }); + } + } + ); + + this.selectionChangeEvent = this.quillEditor.on('selection-change', (range: any, oldRange: any, source: string) => { + if (range === null) { + this.editorBlur.emit({ + editor: this.quillEditor, + source, + }); + } else if (oldRange === null) { + this.editorFocus.emit({ + editor: this.quillEditor, + source, + }); + } + + this.editorSelectionChange.emit({ + editor: this.quillEditor, + range, + oldRange, + source, + }); + }); + + this.textChangeEvent = this.quillEditor.on('text-change', (delta: any, oldDelta: any, source: string) => { + const text = this.quillEditor.getText(); + const content = this.quillEditor.getContents(); + + let html: string | null = this.editorElement.querySelector('.ql-editor').innerHTML; + if (html === '


' || html === '

') { + html = null; + } + + this.editorContentChange.emit({ + editor: this.quillEditor, + content, + delta, + html, + oldDelta, + source, + text, + }); + }); + + setTimeout(() => { + this.editorInit.emit(this.quillEditor); + }); + } + + disconnectedCallback() { + if (this.selectionChangeEvent) { + this.selectionChangeEvent.removeListener('selection-change'); + } + if (this.textChangeEvent) { + this.textChangeEvent.removeListener('text-change'); + } + if (this.editorChangeEvent) { + this.editorChangeEvent.removeListener('editor-change'); + } + } + + @Watch('content') + updateContent(newValue: any): void { + if (!this.quillEditor) { + return; + } + const editorContents = this.getEditorContent(); + + if (['text', 'html', 'json'].indexOf(this.format) > -1 && newValue === editorContents) { + return null; + } else { + let changed = false; + try { + const newContentString = JSON.stringify(newValue); + changed = JSON.stringify(editorContents) !== newContentString; + } catch { + return null; + } + + if (!changed) { + return null; + } + } + + this.setEditorContent(newValue); + } + + @Watch('readOnly') + updateReadOnly(newValue: boolean, oldValue: boolean): void { + if (!this.quillEditor) { + return; + } + if (newValue !== oldValue) { + this.quillEditor.enable(!newValue); + } + } + + @Watch('placeholder') + updatePlaceholder(newValue: string, oldValue: string): void { + if (!this.quillEditor) { + return; + } + if (newValue !== oldValue) { + this.quillEditor.root.dataset.placeholder = newValue; + } + } + + @Watch('styles') + updateStyle(newValue: string, oldValue: string): void { + if (!this.editorElement) { + return; + } + + if (oldValue) { + const old = JSON.parse(oldValue); + Object.keys(old).forEach((key: string) => { + this.editorElement.style.setProperty(key, ''); + }); + } + if (newValue) { + const value = JSON.parse(newValue); + Object.keys(value).forEach((key: string) => { + this.editorElement.style.setProperty(key, value[key]); + }); + } + } + + render() { + + + ; + } +} diff --git a/stencil-workspace/src/components/modus-text-editor/readme.md b/stencil-workspace/src/components/modus-text-editor/readme.md new file mode 100644 index 000000000..823db5c32 --- /dev/null +++ b/stencil-workspace/src/components/modus-text-editor/readme.md @@ -0,0 +1,40 @@ +# quill-editor + + + + + + +## Properties + +| Property | Attribute | Description | Type | Default | +| ----------------------- | ------------------------- | ----------- | ---------------------------- | ------------------------ | +| `bounds` | `bounds` | | `HTMLElement \| string` | `undefined` | +| `content` | `content` | | `string` | `undefined` | +| `customToolbarPosition` | `custom-toolbar-position` | | `"bottom" \| "top"` | `'top'` | +| `debug` | `debug` | | `string` | `'warn'` | +| `format` | `format` | | `"html" \| "json" \| "text"` | `'html'` | +| `formats` | -- | | `string[]` | `undefined` | +| `modules` | `modules` | | `string` | `undefined` | +| `placeholder` | `placeholder` | | `string` | `'Insert text here ...'` | +| `preserveWhitespace` | `preserve-whitespace` | | `boolean` | `false` | +| `readOnly` | `read-only` | | `boolean` | `undefined` | +| `styles` | `styles` | | `string` | `'{}'` | +| `theme` | `theme` | | `string` | `'snow'` | + + +## Events + +| Event | Description | Type | +| ----------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| `editorBlur` | | `CustomEvent<{ editor: any; source: string; }>` | +| `editorChange` | | `CustomEvent<{ editor: any; event: "selection-change"; range: any; oldRange: any; source: string; } \| { editor: any; event: "text-change"; content: any; text: string; html: string; delta: any; oldDelta: any; source: string; }>` | +| `editorContentChange` | | `CustomEvent<{ editor: any; content: any; text: string; html: string; delta: any; oldDelta: any; source: string; }>` | +| `editorFocus` | | `CustomEvent<{ editor: any; source: string; }>` | +| `editorInit` | | `CustomEvent` | +| `editorSelectionChange` | | `CustomEvent<{ editor: any; range: any; oldRange: any; source: string; }>` | + + +---------------------------------------------- + + diff --git a/stencil-workspace/src/index.html b/stencil-workspace/src/index.html index 77729b596..1d195059b 100644 --- a/stencil-workspace/src/index.html +++ b/stencil-workspace/src/index.html @@ -16,8 +16,28 @@ -
- -
+ +

Modus Editor

+ + + + diff --git a/stencil-workspace/src/utils/utils.ts b/stencil-workspace/src/utils/utils.ts index 3279f5f3d..d24025200 100644 --- a/stencil-workspace/src/utils/utils.ts +++ b/stencil-workspace/src/utils/utils.ts @@ -16,3 +16,38 @@ let counter = 0; export function generateElementId(): string { return `mwc_id_${counter++}`; } + +export function convertIconToSVG(iconObject) { + let svgString = ` { + svgString += ` ${key}="${iconObject['$attrs$'][key]}"`; + }); + + svgString += `>`; + + if (iconObject['$children$']) { + iconObject['$children$'].forEach((child) => { + let childString = `<${child['$tag$']}`; + + Object.keys(child['$attrs$']).forEach((key) => { + childString += ` ${key}="${child['$attrs$'][key]}"`; + }); + + if (!child['$children$']) { + childString += `/>`; + } else { + childString += `>`; + + childString += convertIconToSVG(child); + childString += ``; + } + + svgString += childString; + }); + } + + svgString += ``; + + return svgString; +} diff --git a/stencil-workspace/storybook/stories/components/modus-text-editor/modus-text-editor-storybook-docs.mdx b/stencil-workspace/storybook/stories/components/modus-text-editor/modus-text-editor-storybook-docs.mdx new file mode 100644 index 000000000..19b5d3470 --- /dev/null +++ b/stencil-workspace/storybook/stories/components/modus-text-editor/modus-text-editor-storybook-docs.mdx @@ -0,0 +1,35 @@ +# Text Editor + +--- + +The [Modus Text Editor](https://modus.trimble.com/components/text-editor/) is a rich-text editor built to offer customizable and easy-to-use content editing tools. It provides common text editing features like bold, italic, underline, and text alignment, along with the ability to insert links, images, and more. + +#### Implementation Details + +- The `modus-text-editor` component offers a toolbar for formatting text, and an editable content area. +- Use the `content` property to get or set the content of the editor. +- Customize the toolbar by passing an array of toolbar options via the `toolbar-options` property. + +### Default + +```html + +``` + +### Properties + +| Property | Attribute | Description | Type | Default | +| ------------- | ------------- | ------------------------------------------- | --------- | ------- | +| `content` | `content` | (optional) Content of the text editor. | `string` | `""` | +| `disabled` | `disabled` | (optional) Disables the editor. | `boolean` | `false` | +| `placeholder` | `placeholder` | (optional) Placeholder text for the editor. | `string` | `""` | + +### Events + +| Event | Description | Type | +| ----------------- | ---------------------------- | -------------------------------------------------------------------- | +| `editorChange` | Fired on editor change. | `CustomEvent<{ eventName: string; args: unknown[] }>` | +| `selectionUpdate` | Fired on selection change. | `CustomEvent<{ range: unknown; oldRange: unknown; source: string }>` | +| `textChange` | Fired on input value change. | `CustomEvent<{ delta: unknown; oldDelta: unknown; source: string }>` | + +### Accessibility