├── .eslintrc.json ├── .github ├── CODEOWNERS ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── PULL_REQUEST_TEMPLATE │ └── pull_request_template.md ├── dependabot.yml └── workflows │ └── codeql.yml ├── .gitignore ├── .storybook ├── main.js └── preview.js ├── .vscode └── settings.json ├── CHANGELOG.md ├── CONTRIBUTING.md ├── Jenkinsfile ├── LICENSE.txt ├── README.md ├── SECURITY.md ├── package.json ├── rollup.build.js ├── src ├── demo │ ├── Demo.vue │ ├── demo.ts │ ├── index.html │ └── views │ │ ├── ContentTab.vue │ │ ├── Controlled.vue │ │ ├── EditorTab.vue │ │ ├── GetEditor.vue │ │ ├── Home.vue │ │ ├── Iframe.vue │ │ ├── Inline.vue │ │ ├── KeepAlive.vue │ │ ├── Refreshable.vue │ │ └── Tagged.vue ├── main │ └── ts │ │ ├── ScriptLoader.ts │ │ ├── TinyMCE.ts │ │ ├── Utils.ts │ │ ├── components │ │ ├── Editor.ts │ │ └── EditorPropTypes.ts │ │ └── index.ts ├── stories │ └── Editor.stories.tsx └── test │ └── ts │ ├── alien │ ├── Loader.ts │ └── TestHelper.ts │ ├── atomic │ └── UtilsTest.ts │ └── browser │ ├── InitTest.ts │ └── LoadTinyTest.ts ├── stories └── index.js ├── tsconfig.browser.json ├── tsconfig.cjs.json ├── tsconfig.es2015.json ├── tsconfig.json ├── vite.config.js └── yarn.lock /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "overrides": [ 3 | { 4 | "files": [ 5 | "src/**/*.ts", 6 | "src/**/*.tsx" 7 | ], 8 | "excludedFiles": [ 9 | "src/demo/demo.ts" 10 | ], 11 | "plugins": ["@typescript-eslint"], 12 | "extends": "plugin:@tinymce/standard", 13 | "parserOptions": { 14 | "project": "tsconfig.json", 15 | "sourceType": "module" 16 | }, 17 | "rules": { 18 | "@tinymce/prefer-fun": "off" 19 | } 20 | }, 21 | { 22 | "files": [ 23 | "**/*.js" 24 | ], 25 | "env": { 26 | "es6": true, 27 | "node": true, 28 | "browser": true 29 | }, 30 | "extends": "eslint:recommended", 31 | "parser": "espree", 32 | "parserOptions": { 33 | "ecmaVersion": 2020, 34 | "sourceType": "module" 35 | }, 36 | "rules": { 37 | "indent": [ "error", 2, { "SwitchCase": 1 } ], 38 | "no-shadow": "error", 39 | "no-unused-vars": [ "error", { "argsIgnorePattern": "^_" } ], 40 | "object-curly-spacing": [ "error", "always", { "arraysInObjects": false, "objectsInObjects": false } ], 41 | "quotes": [ "error", "single" ], 42 | "semi": "error" 43 | } 44 | } 45 | ] 46 | } -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @tinymce/tinymce-reviewers -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a bug report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **What is the current behavior?** 11 | 12 | **Please provide the steps to reproduce and if possible a minimal demo of the problem via [codesandbox.io](https://codesandbox.io/p/sandbox/tinymce-vue-shtq6h) or similar.** 13 | 14 | **What is the expected behavior?** 15 | 16 | **Which versions of TinyMCE, and which browser / OS are affected by this issue? Did this work in previous versions of TinyMCE or tinymce-vue?** 17 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest a feature for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE/pull_request_template.md: -------------------------------------------------------------------------------- 1 | **Before submitting a pull request** please do the following: 2 | 3 | 1. Fork [the repository](https://github.com/tinymce/tinymce-vue) and create your branch from `master` 4 | 2. Make sure to sign the CLA. -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "github-actions" 9 | directory: "/" 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | name: "CodeQL" 2 | 3 | ## Github workflow code scanning 4 | # Configure this file to setup code scanning for the repository 5 | # Code scanning uses Github actions minutes. To learn more: https://docs.github.com/en/billing/managing-billing-for-github-actions/about-billing-for-github-actions 6 | 7 | on: 8 | push: 9 | branches: [ "main" ] 10 | ## Filter based on files changed: vue and tsx for integrations 11 | # paths: [ "**.tsx?", "**.js", "**.vue" ] 12 | pull_request: 13 | branches: [ "main" ] 14 | ## Filter based on files changed: vue and tsx for integrations 15 | # paths: [ "**.tsx?", "**.js", "**.vue" ] 16 | ## Schedule cron running 17 | # schedule: 18 | # - cron: "0 0 1 * *" 19 | 20 | jobs: 21 | analyze: 22 | name: Analyze 23 | runs-on: ubuntu-latest 24 | permissions: 25 | actions: read 26 | contents: read 27 | security-events: write 28 | 29 | strategy: 30 | fail-fast: false 31 | matrix: 32 | language: [ javascript ] 33 | 34 | steps: 35 | - name: Checkout 36 | uses: actions/checkout@v3 37 | 38 | - name: Initialize CodeQL 39 | uses: github/codeql-action/init@v3 40 | with: 41 | languages: ${{ matrix.language }} 42 | queries: +security-and-quality 43 | 44 | - name: Autobuild 45 | uses: github/codeql-action/autobuild@v3 46 | 47 | - name: Perform CodeQL Analysis 48 | uses: github/codeql-action/analyze@v3 49 | with: 50 | category: "/language:${{ matrix.language }}" 51 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | lib 3 | .DS_Store 4 | storybook-static 5 | .rpt2_cache 6 | tinymce-tinymce-vue-*.tgz 7 | .idea 8 | .cache 9 | scratch -------------------------------------------------------------------------------- /.storybook/main.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | core: { 3 | builder: "webpack5" 4 | }, 5 | "stories": [ 6 | "../src/**/*.stories.mdx", 7 | "../src/**/*.stories.@(js|jsx|ts|tsx)" 8 | ], 9 | "addons": [ 10 | "@storybook/addon-links", 11 | { 12 | name: '@storybook/addon-essentials', 13 | options: { 14 | docs: false 15 | } 16 | } 17 | ] 18 | } -------------------------------------------------------------------------------- /.storybook/preview.js: -------------------------------------------------------------------------------- 1 | export const parameters = { 2 | actions: { argTypesRegex: "^on[A-Z].*" }, 3 | controls: { 4 | matchers: { 5 | color: /(background|color)$/i, 6 | date: /Date$/, 7 | }, 8 | }, 9 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "asynctest" 4 | ], 5 | "typescript.tsdk": "node_modules/typescript/lib" 6 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## Unreleased 9 | 10 | ### Added 11 | - New `readonly` prop that can be used to toggle the editor's `readonly` mode. #TINY-11908 12 | 13 | ### Changed 14 | - `disabled` prop is now mapped to the editor's `disabled` option. #TINY-11908 15 | 16 | ## 6.1.0 - 2024-10-22 17 | 18 | ### Changed 19 | - Moved tinymce dependency to be a optional peer dependency. 20 | 21 | ### Fixed 22 | - Assigned the `licenseKey` prop to TinyMCE's `license_key` init prop. Community PR acknowledgement: Calneideck. 23 | 24 | ## 6.0.0 - 2024-06-05 25 | 26 | ### Added 27 | - Added missing events: `onInput`, `onCommentChange`, `onCompositionEnd`, `onCompositionStart`, `onCompositionUpdate`. 28 | 29 | ### Changed 30 | - Default cloud channel to '7' 31 | 32 | ## 5.1.0 - 2023-04-05 33 | 34 | ### Added 35 | - Exposed method `getEditor()` that return the current editor 36 | 37 | ## 5.0.1 - 2022-10-24 38 | 39 | ### Changed 40 | - Use target element instead of selector for Editor configuration 41 | 42 | ### Fixed 43 | - Updated dependencies 44 | - Updated CI library to latest 45 | 46 | ## 5.0.0 - 2022-04-08 47 | 48 | ### Changed 49 | - License changed to MIT 50 | - Default cloud channel to '6' 51 | 52 | ## 4.0.7 - 2022-03-09 53 | 54 | ### Changed 55 | - Storybook examples 56 | 57 | ## 4.0.6 - 2022-02-17 58 | 59 | ### Added 60 | - Exposed method `rerender(initObject)` to change the editor configuration 61 | - Watcher for tag name 62 | 63 | ## 4.0.5 - 2021-11-22 64 | 65 | ### Added 66 | - Correct proptypes 67 | 68 | ### Fixed 69 | - Update dependencies 70 | 71 | ## 4.0.2 - 2021-11-05 72 | 73 | ### Fixed 74 | - Update dependencies 75 | 76 | ## 4.0.1 - 2021-11-05 77 | 78 | ### Added 79 | - Adopt beehive-flow release process 80 | 81 | ## 4.0.0 - 2020-11-05 82 | 83 | ### Added 84 | - Vue 3 support 85 | 86 | ## 3.2.4 - 2020-10-16 87 | 88 | ### Fixed 89 | - Fixed handling of inline template event bindings 90 | 91 | ## 3.2.3 - 2020-09-16 92 | 93 | ### Changed 94 | - Update dependencies 95 | - Changed `keyup` to `input` for the events triggering sending out content to `v-model`. 96 | 97 | ## 3.2.2 - 2020-05-22 98 | 99 | ### Fixed 100 | - Fixed v-model `outputFormat` resetting the editor content on every change 101 | 102 | ## 3.2.1 - 2020-04-30 103 | 104 | ### Fixed 105 | - Upgraded jquery in dev dependencies in response to security alert. 106 | 107 | ## 3.2.0 - 2020-02-24 108 | 109 | ### Added 110 | - Added new `tinymceScriptSrc` prop for specifying an external version of TinyMCE to lazy load 111 | 112 | ## 3.1.0 - 2020-01-31 113 | 114 | ### Added 115 | - Added new `outputFormat` prop for specifying the format of the content emitted via the `input` event 116 | 117 | ## 3.0.1 - 2019-08-19 118 | 119 | ### Fixed 120 | - Fixed incorrect module paths 121 | 122 | ## 3.0.0 - 2019-08-16 123 | 124 | ### Added 125 | - Changed referrer policy to origin to allow cloud caching 126 | 127 | ### Changed 128 | - Removed Vue as a dependency and added vue@^2.4.3 as a peer dependency 129 | 130 | ## 2.1.0 - 2019-06-05 131 | 132 | ### Changed 133 | - Changed the CDN URL to use `cdn.tiny.cloud` 134 | 135 | ## 2.0.0 - 2019-02-11 136 | 137 | ### Changed 138 | - Changed default cloudChannel to `'5'`. 139 | 140 | ## 1.1.2 - 2019-01-09 141 | 142 | ### Added 143 | - Updated changelog to show how you have to add `.default` to commonjs require. 144 | 145 | ## 1.1.1 - 2019-01-09 146 | 147 | ### Changed 148 | - Improved uuid function. Patch contributed by fureweb-com. 149 | 150 | ## 1.1.0 - 2018-10-01 151 | 152 | ### Added 153 | - Added functionality to bind to `disabled` property to set editor into readonly state. 154 | 155 | ## 1.0.9 - 2018-09-03 156 | 157 | ### Fixed 158 | - Fixed broken links in readme. 159 | 160 | ## 1.0.8 - 2018-05-10 161 | 162 | ### Added 163 | - Added `undo` and `redo` to the events triggering sending out content to `v-model`. 164 | 165 | ## 1.0.7 - 2018-04-26 166 | 167 | ### Fixed 168 | - Added null check before removing editor to check that tinymce is actually available. 169 | 170 | ## 1.0.6 - 2018-04-11 171 | 172 | ### Removed 173 | - Removed `cloudChannel` prop validation. 174 | 175 | ## 1.0.5 - 2018-04-06 176 | 177 | ### Removed 178 | - Removed onPreInit shorthand as it never worked. 179 | 180 | ## 1.0.4 - 2018-04-06 181 | 182 | ### Fixed 183 | - Fixed bug with onInit never firing. 184 | 185 | ## 1.0.3 - 2018-04-03 186 | 187 | ### Fixed 188 | - Fixed bug with value watcher getting out of sync. 189 | 190 | ## 1.0.2 - 2018-02-16 191 | 192 | ### Fixed 193 | - Fixed bug where is wasn't possible to set inline in the init object, only on the shorthand. 194 | 195 | ## 1.0.1 - 2018-02-08 196 | 197 | ### Fixed 198 | - Fixed binding timing issues by moving the binding to after the editor has initialized. 199 | 200 | ## 1.0.0 - 2018-01-16 201 | 202 | ### Added 203 | - Initial release 204 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | This project follows the [beehive-flow](https://github.com/tinymce/beehive-flow/) branching process ("Basic process - release from main branch"). 4 | Please read the [beehive-flow readme](https://github.com/tinymce/beehive-flow/blob/main/README.md) for more information. 5 | This mainly affects branching and merging for Tiny staff. 6 | 7 | External contributors are free to submit PRs against the `main` branch. 8 | Note that contributions will require signing of our Contributor License Agreement. 9 | -------------------------------------------------------------------------------- /Jenkinsfile: -------------------------------------------------------------------------------- 1 | #!groovy 2 | @Library('waluigi@release/7') _ 3 | 4 | mixedBeehiveFlow( 5 | container: [ resourceRequestMemory: '3Gi', resourceLimitMemory: '3Gi' ], 6 | headlessContainer: [ selenium: [ image: tinyAws.getPullThroughCacheImage("selenium/standalone-chrome", "127.0") ] ], 7 | testPrefix: 'Tiny-Vue', 8 | testDirs: [ "src/test/ts/atomic", "src/test/ts/browser" ], 9 | platforms: [ 10 | [ browser: 'chrome', headless: true ], 11 | [ browser: 'firefox', provider: 'aws' ], 12 | [ browser: 'safari', provider: 'lambdatest' ] 13 | ], 14 | publishContainer: [ 15 | resourceRequestMemory: '4Gi', 16 | resourceLimitMemory: '4Gi' 17 | ], 18 | customSteps: { 19 | stage("update storybook") { 20 | def status = beehiveFlowStatus() 21 | if (status.branchState == 'releaseReady' && status.isLatest) { 22 | tinyGit.withGitHubSSHCredentials { 23 | exec('yarn deploy-storybook') 24 | } 25 | } else { 26 | echo "Skipping as is not latest release" 27 | } 28 | } 29 | }, 30 | preparePublish:{ 31 | yarnInstall() 32 | sh "yarn build" 33 | } 34 | ) -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Ephox Corporation DBA Tiny Technologies, Inc. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Official TinyMCE Vue component 2 | 3 | ## About 4 | 5 | This package is a thin wrapper around [TinyMCE](https://github.com/tinymce/tinymce) to make it easier to use in a Vue application. 6 | 7 | * If you need detailed documentation on TinyMCE, see: [TinyMCE Documentation](https://www.tiny.cloud/docs/tinymce/7/). 8 | * For the TinyMCE Vue Quick Start, see: [TinyMCE Documentation - Vue Integration](https://www.tiny.cloud/docs/tinymce/7/vue-cloud). 9 | * For the TinyMCE Vue Technical Reference, see: [TinyMCE Documentation - TinyMCE Vue Technical Reference](https://www.tiny.cloud/docs/tinymce/7/vue-ref). 10 | * For our quick demos, check out the TinyMCE Vue [Storybook](https://tinymce.github.io/tinymce-vue/). 11 | 12 | 13 | ### Support 14 | 15 | Version 7.0 is intended to support the tinymce version 7.6 and above. 16 | Version 4.0 is intended to support Vue 3. For Vue 2.x and below please use previous versions of the wrapper. 17 | 18 | ### Issues 19 | 20 | Have you found an issue with `tinymce-vue` or do you have a feature request? Open up an [issue](https://github.com/tinymce/tinymce-vue/issues) and let us know or submit a [pull request](https://github.com/tinymce/tinymce-vue/pulls). *Note: for issues related to TinyMCE please visit the [TinyMCE repository](https://github.com/tinymce/tinymce).* 21 | -------------------------------------------------------------------------------- /SECURITY.md: -------------------------------------------------------------------------------- 1 | # Security Policy 2 | 3 | ## Reporting a Vulnerability 4 | 5 | For details on how to report security issues to Tiny, refer to the [Reporting TinyMCE security issues documentation](https://www.tiny.cloud/docs/tinymce/6/security/#reportingtinymcesecurityissues). -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@tinymce/tinymce-vue", 3 | "version": "6.2.0-rc", 4 | "description": "Official TinyMCE Vue 3 Component", 5 | "private": false, 6 | "repository": { 7 | "url": "https://github.com/tinymce/tinymce-vue" 8 | }, 9 | "main": "lib/cjs/main/ts/index.js", 10 | "module": "lib/es2015/main/ts/index.js", 11 | "scripts": { 12 | "test": "bedrock-auto -b chrome-headless -f src/test/ts/**/*Test.ts", 13 | "test-manual": "bedrock -f src/test/ts/**/*Test.ts", 14 | "clean": "rimraf lib", 15 | "lint": "eslint src/{main,test}/**/*.ts", 16 | "build": "yarn run clean && yarn run lint && tsc -p ./tsconfig.es2015.json && tsc -p ./tsconfig.cjs.json && node rollup.build.js", 17 | "storybook": "start-storybook -p 6006", 18 | "demo": "vite", 19 | "build-storybook": "build-storybook", 20 | "deploy-storybook": "yarn build-storybook && gh-pages -d ./storybook-static -u 'tiny-bot '" 21 | }, 22 | "keywords": [ 23 | "tinymce", 24 | "vue", 25 | "component" 26 | ], 27 | "author": "Ephox Inc", 28 | "license": "MIT", 29 | "files": [ 30 | "lib/*/**", 31 | "README.md", 32 | "CHANGELOG.md", 33 | "LICENSE.txt" 34 | ], 35 | "peerDependencies": { 36 | "tinymce": "^7.0.0 || ^6.0.0 || ^5.5.1", 37 | "vue": "^3.0.0" 38 | }, 39 | "peerDependenciesMeta": { 40 | "tinymce": { 41 | "optional": true 42 | } 43 | }, 44 | "devDependencies": { 45 | "@babel/core": "^7.20.2", 46 | "@ephox/agar": "^8.0.1", 47 | "@ephox/bedrock-client": "^14.1.1", 48 | "@ephox/bedrock-server": "^14.1.4", 49 | "@ephox/katamari": "^9.1.6", 50 | "@ephox/sugar": "^9.3.1", 51 | "@storybook/addon-actions": "^6.5.13", 52 | "@storybook/addon-essentials": "^6.5.13", 53 | "@storybook/addon-links": "^6.5.13", 54 | "@storybook/addon-notes": "^5.3.21", 55 | "@storybook/builder-webpack5": "^6.5.16", 56 | "@storybook/manager-webpack5": "^6.5.16", 57 | "@storybook/vue3": "^6.5.13", 58 | "@tinymce/beehive-flow": "^0.19.0", 59 | "@tinymce/eslint-plugin": "^2.3.1", 60 | "@tinymce/miniature": "^6.0.0", 61 | "@types/node": "^20.14.0", 62 | "@vitejs/plugin-vue": "^5.0.5", 63 | "@vue/compiler-sfc": "^3.4.27", 64 | "babel-loader": "^8.2.3", 65 | "babel-preset-vue": "^2.0.2", 66 | "babel-register": "^6.26.0", 67 | "css-loader": "^6.2.0", 68 | "file-loader": "^6.0.0", 69 | "gh-pages": "^6.1.1", 70 | "react": "^16.14.0", 71 | "react-dom": "^16.14.0", 72 | "regenerator-runtime": "^0.13.9", 73 | "rimraf": "^5.0.7", 74 | "rollup": "^4.18.0", 75 | "rollup-plugin-node-resolve": "^5.2.0", 76 | "rollup-plugin-typescript2": "^0.34.1", 77 | "rollup-plugin-uglify": "^6.0.0", 78 | "tinymce": "^7", 79 | "tinymce-4": "npm:tinymce@^4", 80 | "tinymce-5": "npm:tinymce@^5", 81 | "tinymce-6": "npm:tinymce@^6", 82 | "tinymce-7": "npm:tinymce@^7", 83 | "ts-loader": "^9.5.1", 84 | "ts-node": "^10.9.2", 85 | "tsconfig-paths-webpack-plugin": "^4.1.0", 86 | "typescript": "^5.4.5", 87 | "vite": "^5.2.12", 88 | "vue": "^3.4.27", 89 | "vue-cli-plugin-storybook": "^2.1.0", 90 | "vue-loader": "^17.4.2", 91 | "vue-router": "^4.3.2", 92 | "vue-template-compiler": "^2.7.16", 93 | "webpack": "^5.75.0" 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /rollup.build.js: -------------------------------------------------------------------------------- 1 | const rollup = require('rollup'); 2 | const typescript = require('rollup-plugin-typescript2'); 3 | const { uglify } = require('rollup-plugin-uglify'); 4 | 5 | const browserBuildOptions = { 6 | file: 'lib/browser/tinymce-vue.js', 7 | format: 'iife', 8 | name: 'Editor', 9 | globals: { 10 | vue: 'Vue' 11 | } 12 | }; 13 | 14 | const build = async (input, output) => { 15 | const bundle = await rollup.rollup(input); 16 | await bundle.write(output); 17 | }; 18 | 19 | [ 20 | browserBuildOptions, 21 | { ...browserBuildOptions, 22 | file: 'lib/browser/tinymce-vue.min.js' 23 | } 24 | ].forEach((opts) => build({ 25 | input: './src/main/ts/index.ts', 26 | plugins: [ 27 | typescript({ 28 | tsconfig: './tsconfig.browser.json' 29 | }), 30 | opts.file.endsWith('min.js') ? uglify() : {} 31 | ] 32 | }, opts).then( 33 | () => console.log(`bundled: ${opts.file}`)) 34 | ); -------------------------------------------------------------------------------- /src/demo/Demo.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 23 | -------------------------------------------------------------------------------- /src/demo/demo.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue'; 2 | import { createWebHistory, createRouter } from 'vue-router'; 3 | import Demo from '/Demo.vue'; 4 | 5 | import Home from '/views/Home.vue'; 6 | import Iframe from '/views/Iframe.vue'; 7 | import Inline from '/views/Inline.vue'; 8 | import Controlled from '/views/Controlled.vue'; 9 | import Keepalive from '/views/KeepAlive.vue'; 10 | import Refreshable from '/views/Refreshable.vue'; 11 | import Tagged from '/views/Tagged.vue'; 12 | import GetEditor from '/views/GetEditor.vue'; 13 | 14 | const routes = [ 15 | { 16 | path: '/', 17 | name: 'Home', 18 | component: Home 19 | }, 20 | { 21 | path: '/iframe', 22 | name: 'Iframe', 23 | component: Iframe 24 | }, 25 | { 26 | path: '/inline', 27 | name: 'Inline', 28 | component: Inline 29 | }, 30 | { 31 | path: '/controlled', 32 | name: 'Controlled', 33 | component: Controlled 34 | }, 35 | { 36 | path: '/keepalive', 37 | name: 'Keepalive', 38 | component: Keepalive 39 | }, 40 | { 41 | path: '/refreshable', 42 | name: 'Refreshable', 43 | component: Refreshable 44 | }, 45 | { 46 | path: '/tagged', 47 | name: 'Tagged', 48 | component: Tagged 49 | }, 50 | { 51 | path: '/get-editor', 52 | name: 'GetEditor', 53 | component: GetEditor 54 | } 55 | ]; 56 | 57 | const router = createRouter({ 58 | history: createWebHistory(), 59 | routes 60 | }); 61 | 62 | createApp(Demo).use(router).mount('#app'); -------------------------------------------------------------------------------- /src/demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | TinyMCE-vue demo page 5 | 6 | 40 | 41 |
42 | 43 | 44 | -------------------------------------------------------------------------------- /src/demo/views/ContentTab.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /src/demo/views/Controlled.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | -------------------------------------------------------------------------------- /src/demo/views/EditorTab.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 32 | -------------------------------------------------------------------------------- /src/demo/views/GetEditor.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 42 | -------------------------------------------------------------------------------- /src/demo/views/Home.vue: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/demo/views/Iframe.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /src/demo/views/Inline.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | -------------------------------------------------------------------------------- /src/demo/views/KeepAlive.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | -------------------------------------------------------------------------------- /src/demo/views/Refreshable.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | -------------------------------------------------------------------------------- /src/demo/views/Tagged.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | -------------------------------------------------------------------------------- /src/main/ts/ScriptLoader.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2018-present, Ephox, Inc. 3 | * 4 | * This source code is licensed under the Apache 2 license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | 9 | import { uuid } from './Utils'; 10 | 11 | export type CallbackFn = () => void; 12 | export interface IStateObj { 13 | listeners: CallbackFn[]; 14 | scriptId: string; 15 | scriptLoaded: boolean; 16 | } 17 | 18 | const createState = (): IStateObj => ({ 19 | listeners: [], 20 | scriptId: uuid('tiny-script'), 21 | scriptLoaded: false 22 | }); 23 | 24 | interface ScriptLoader { 25 | load: (doc: Document, url: string, callback: CallbackFn) => void; 26 | reinitialize: () => void; 27 | } 28 | 29 | const CreateScriptLoader = (): ScriptLoader => { 30 | let state: IStateObj = createState(); 31 | 32 | const injectScriptTag = (scriptId: string, doc: Document, url: string, callback: CallbackFn) => { 33 | const scriptTag = doc.createElement('script'); 34 | scriptTag.referrerPolicy = 'origin'; 35 | scriptTag.type = 'application/javascript'; 36 | scriptTag.id = scriptId; 37 | scriptTag.src = url; 38 | 39 | const handler = () => { 40 | scriptTag.removeEventListener('load', handler); 41 | callback(); 42 | }; 43 | scriptTag.addEventListener('load', handler); 44 | if (doc.head) { 45 | doc.head.appendChild(scriptTag); 46 | } 47 | }; 48 | 49 | const load = (doc: Document, url: string, callback: CallbackFn) => { 50 | if (state.scriptLoaded) { 51 | callback(); 52 | } else { 53 | state.listeners.push(callback); 54 | if (!doc.getElementById(state.scriptId)) { 55 | injectScriptTag(state.scriptId, doc, url, () => { 56 | state.listeners.forEach((fn) => fn()); 57 | state.scriptLoaded = true; 58 | }); 59 | } 60 | } 61 | }; 62 | 63 | // Only to be used by tests. 64 | const reinitialize = () => { 65 | state = createState(); 66 | }; 67 | 68 | return { 69 | load, 70 | reinitialize 71 | }; 72 | }; 73 | 74 | const ScriptLoader = CreateScriptLoader(); 75 | 76 | export { 77 | ScriptLoader 78 | }; -------------------------------------------------------------------------------- /src/main/ts/TinyMCE.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2018-present, Ephox, Inc. 3 | * 4 | * This source code is licensed under the Apache 2 license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | 9 | const getGlobal = (): any => (typeof window !== 'undefined' ? window : global); 10 | 11 | const getTinymce = () => { 12 | const global = getGlobal(); 13 | 14 | return global && global.tinymce ? global.tinymce : null; 15 | }; 16 | 17 | export { getTinymce }; 18 | -------------------------------------------------------------------------------- /src/main/ts/Utils.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2018-present, Ephox, Inc. 3 | * 4 | * This source code is licensed under the Apache 2 license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | 9 | import { Ref, watch, SetupContext } from 'vue'; 10 | import { IPropTypes } from './components/EditorPropTypes'; 11 | import type { Editor as TinyMCEEditor, EditorEvent } from 'tinymce'; 12 | 13 | const validEvents = [ 14 | 'onActivate', 15 | 'onAddUndo', 16 | 'onBeforeAddUndo', 17 | 'onBeforeExecCommand', 18 | 'onBeforeGetContent', 19 | 'onBeforeRenderUI', 20 | 'onBeforeSetContent', 21 | 'onBeforePaste', 22 | 'onBlur', 23 | 'onChange', 24 | 'onClearUndos', 25 | 'onClick', 26 | 'onContextMenu', 27 | 'onCommentChange', 28 | 'onCompositionEnd', 29 | 'onCompositionStart', 30 | 'onCompositionUpdate', 31 | 'onCopy', 32 | 'onCut', 33 | 'onDblclick', 34 | 'onDeactivate', 35 | 'onDirty', 36 | 'onDrag', 37 | 'onDragDrop', 38 | 'onDragEnd', 39 | 'onDragGesture', 40 | 'onDragOver', 41 | 'onDrop', 42 | 'onExecCommand', 43 | 'onFocus', 44 | 'onFocusIn', 45 | 'onFocusOut', 46 | 'onGetContent', 47 | 'onHide', 48 | 'onInit', 49 | 'onInput', 50 | 'onKeyDown', 51 | 'onKeyPress', 52 | 'onKeyUp', 53 | 'onLoadContent', 54 | 'onMouseDown', 55 | 'onMouseEnter', 56 | 'onMouseLeave', 57 | 'onMouseMove', 58 | 'onMouseOut', 59 | 'onMouseOver', 60 | 'onMouseUp', 61 | 'onNodeChange', 62 | 'onObjectResizeStart', 63 | 'onObjectResized', 64 | 'onObjectSelected', 65 | 'onPaste', 66 | 'onPostProcess', 67 | 'onPostRender', 68 | 'onPreProcess', 69 | 'onProgressState', 70 | 'onRedo', 71 | 'onRemove', 72 | 'onReset', 73 | 'onSaveContent', 74 | 'onSelectionChange', 75 | 'onSetAttrib', 76 | 'onSetContent', 77 | 'onShow', 78 | 'onSubmit', 79 | 'onUndo', 80 | 'onVisualAid' 81 | ]; 82 | 83 | const isValidKey = (key: string) => 84 | validEvents.map((event) => event.toLowerCase()).indexOf(key.toLowerCase()) !== -1; 85 | 86 | const bindHandlers = (initEvent: EditorEvent, listeners: Record, editor: TinyMCEEditor): void => { 87 | Object.keys(listeners) 88 | .filter(isValidKey) 89 | .forEach((key: string) => { 90 | const handler = listeners[key]; 91 | if (typeof handler === 'function') { 92 | if (key === 'onInit') { 93 | handler(initEvent, editor); 94 | } else { 95 | editor.on(key.substring(2), (e: EditorEvent) => handler(e, editor)); 96 | } 97 | } 98 | }); 99 | }; 100 | 101 | const bindModelHandlers = (props: IPropTypes, ctx: SetupContext, editor: TinyMCEEditor, modelValue: Ref) => { 102 | const modelEvents = props.modelEvents ? props.modelEvents : null; 103 | const normalizedEvents = Array.isArray(modelEvents) ? modelEvents.join(' ') : modelEvents; 104 | 105 | watch(modelValue, (val: string, prevVal: string) => { 106 | if (editor && typeof val === 'string' && val !== prevVal && val !== editor.getContent({ format: props.outputFormat })) { 107 | editor.setContent(val); 108 | } 109 | }); 110 | 111 | editor.on(normalizedEvents ? normalizedEvents : 'change input undo redo', () => { 112 | ctx.emit('update:modelValue', editor.getContent({ format: props.outputFormat })); 113 | }); 114 | }; 115 | 116 | const initEditor = ( 117 | initEvent: EditorEvent, 118 | props: IPropTypes, 119 | ctx: SetupContext, 120 | editor: TinyMCEEditor, 121 | modelValue: Ref, 122 | content: () => string) => { 123 | editor.setContent(content()); 124 | if (ctx.attrs['onUpdate:modelValue']) { 125 | bindModelHandlers(props, ctx, editor, modelValue); 126 | } 127 | bindHandlers(initEvent, ctx.attrs, editor); 128 | }; 129 | 130 | let unique = 0; 131 | 132 | const uuid = (prefix: string): string => { 133 | const time = Date.now(); 134 | const random = Math.floor(Math.random() * 1000000000); 135 | 136 | unique++; 137 | 138 | return prefix + '_' + random + unique + String(time); 139 | }; 140 | 141 | const isTextarea = (element: Element | null): element is HTMLTextAreaElement => 142 | element !== null && element.tagName.toLowerCase() === 'textarea'; 143 | 144 | const normalizePluginArray = (plugins?: string | string[]): string[] => { 145 | if (typeof plugins === 'undefined' || plugins === '') { 146 | return []; 147 | } 148 | 149 | return Array.isArray(plugins) ? plugins : plugins.split(' '); 150 | }; 151 | 152 | const mergePlugins = (initPlugins: string | string[] | undefined, inputPlugins?: string | string[]) => 153 | normalizePluginArray(initPlugins).concat(normalizePluginArray(inputPlugins)); 154 | 155 | const isNullOrUndefined = (value: any): value is null | undefined => 156 | value === null || value === undefined; 157 | 158 | const isDisabledOptionSupported = (editor: TinyMCEEditor): boolean => 159 | typeof editor.options?.set === 'function' && editor.options.isRegistered('disabled'); 160 | 161 | export { 162 | bindHandlers, 163 | bindModelHandlers, 164 | initEditor, 165 | isValidKey, 166 | uuid, 167 | isTextarea, 168 | mergePlugins, 169 | isNullOrUndefined, 170 | isDisabledOptionSupported 171 | }; 172 | -------------------------------------------------------------------------------- /src/main/ts/components/Editor.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2018-present, Ephox, Inc. 3 | * 4 | * This source code is licensed under the Apache 2 license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | 9 | import { ScriptLoader } from '../ScriptLoader'; 10 | import { getTinymce } from '../TinyMCE'; 11 | import { isTextarea, mergePlugins, uuid, isNullOrUndefined, initEditor, isDisabledOptionSupported } from '../Utils'; 12 | import { editorProps, IPropTypes } from './EditorPropTypes'; 13 | import { h, defineComponent, onMounted, ref, Ref, toRefs, nextTick, watch, onBeforeUnmount, onActivated, onDeactivated } from 'vue'; 14 | import type { Editor as TinyMCEEditor, EditorEvent, TinyMCE } from 'tinymce'; 15 | 16 | type EditorOptions = Parameters[0]; 17 | 18 | const renderInline = (ce: any, id: string, elementRef: Ref, tagName?: string) => 19 | ce(tagName ? tagName : 'div', { 20 | id, 21 | ref: elementRef 22 | }); 23 | 24 | const renderIframe = (ce: any, id: string, elementRef: Ref) => 25 | ce('textarea', { 26 | id, 27 | visibility: 'hidden', 28 | ref: elementRef 29 | }); 30 | 31 | const defaultInitValues = { selector: undefined, target: undefined }; 32 | 33 | const setMode = (editor: TinyMCEEditor, mode: 'readonly' | 'design') => { 34 | if (typeof editor.mode?.set === 'function') { 35 | editor.mode.set(mode); 36 | } else { 37 | // TinyMCE v4 38 | (editor as any).setMode(mode); 39 | } 40 | }; 41 | 42 | export const Editor = defineComponent({ 43 | props: editorProps, 44 | setup: (props: IPropTypes, ctx) => { 45 | let conf = props.init ? { ...props.init, ...defaultInitValues } : { ...defaultInitValues }; 46 | const { disabled, readonly, modelValue, tagName } = toRefs(props); 47 | const element: Ref = ref(null); 48 | let vueEditor: TinyMCEEditor | null = null; 49 | const elementId: string = props.id || uuid('tiny-vue'); 50 | const inlineEditor: boolean = (props.init && props.init.inline) || props.inline; 51 | const modelBind = !!ctx.attrs['onUpdate:modelValue']; 52 | let mounting = true; 53 | const initialValue: string = props.initialValue ? props.initialValue : ''; 54 | let cache = ''; 55 | 56 | const getContent = (isMounting: boolean): () => string => modelBind ? 57 | () => (modelValue?.value ? modelValue.value : '') : 58 | () => isMounting ? initialValue : cache; 59 | 60 | const initWrapper = (): void => { 61 | const content = getContent(mounting); 62 | const finalInit = { 63 | ...conf, 64 | disabled: props.disabled, 65 | readonly: props.readonly, 66 | target: element.value, 67 | plugins: mergePlugins(conf.plugins, props.plugins), 68 | toolbar: props.toolbar || (conf.toolbar), 69 | inline: inlineEditor, 70 | license_key: props.licenseKey, 71 | setup: (editor: TinyMCEEditor) => { 72 | vueEditor = editor; 73 | 74 | if (!isDisabledOptionSupported(vueEditor) && props.disabled === true) { 75 | setMode(vueEditor, 'readonly'); 76 | } 77 | 78 | editor.on('init', (e: EditorEvent) => initEditor(e, props, ctx, editor, modelValue, content)); 79 | if (typeof conf.setup === 'function') { 80 | conf.setup(editor); 81 | } 82 | } 83 | }; 84 | if (isTextarea(element.value)) { 85 | element.value.style.visibility = ''; 86 | } 87 | getTinymce().init(finalInit); 88 | mounting = false; 89 | }; 90 | watch(readonly, (isReadonly) => { 91 | if (vueEditor !== null) { 92 | setMode(vueEditor, isReadonly ? 'readonly' : 'design'); 93 | } 94 | }); 95 | watch(disabled, (isDisabled) => { 96 | if (vueEditor !== null) { 97 | if (isDisabledOptionSupported(vueEditor)) { 98 | vueEditor.options.set('disabled', isDisabled); 99 | } else { 100 | setMode(vueEditor, isDisabled ? 'readonly' : 'design'); 101 | } 102 | } 103 | }); 104 | watch(tagName, (_) => { 105 | if (vueEditor) { 106 | if (!modelBind) { 107 | cache = vueEditor.getContent(); 108 | } 109 | getTinymce()?.remove(vueEditor); 110 | nextTick(() => initWrapper()); 111 | } 112 | }); 113 | onMounted(() => { 114 | if (getTinymce() !== null) { 115 | initWrapper(); 116 | } else if (element.value && element.value.ownerDocument) { 117 | const channel = props.cloudChannel ? props.cloudChannel : '7'; 118 | const apiKey = props.apiKey ? props.apiKey : 'no-api-key'; 119 | const scriptSrc: string = isNullOrUndefined(props.tinymceScriptSrc) ? 120 | `https://cdn.tiny.cloud/1/${apiKey}/tinymce/${channel}/tinymce.min.js` : 121 | props.tinymceScriptSrc; 122 | ScriptLoader.load( 123 | element.value.ownerDocument, 124 | scriptSrc, 125 | initWrapper 126 | ); 127 | } 128 | }); 129 | onBeforeUnmount(() => { 130 | if (getTinymce() !== null) { 131 | getTinymce().remove(vueEditor); 132 | } 133 | }); 134 | if (!inlineEditor) { 135 | onActivated(() => { 136 | if (!mounting) { 137 | initWrapper(); 138 | } 139 | }); 140 | onDeactivated(() => { 141 | if (vueEditor) { 142 | if (!modelBind) { 143 | cache = vueEditor.getContent(); 144 | } 145 | getTinymce()?.remove(vueEditor); 146 | } 147 | }); 148 | } 149 | const rerender = (init: EditorOptions) => { 150 | if (vueEditor) { 151 | cache = vueEditor.getContent(); 152 | getTinymce()?.remove(vueEditor); 153 | conf = { ...conf, ...init, ...defaultInitValues }; 154 | nextTick(() => initWrapper()); 155 | } 156 | }; 157 | ctx.expose({ 158 | rerender, 159 | getEditor: () => vueEditor 160 | }); 161 | return () => inlineEditor ? 162 | renderInline(h, elementId, element, props.tagName) : 163 | renderIframe(h, elementId, element); 164 | } 165 | }); 166 | -------------------------------------------------------------------------------- /src/main/ts/components/EditorPropTypes.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2018-present, Ephox, Inc. 3 | * 4 | * This source code is licensed under the Apache 2 license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | import type { TinyMCE } from 'tinymce'; 9 | 10 | type EditorOptions = Parameters[0]; 11 | 12 | export type CopyProps = { [P in keyof T]: any }; 13 | 14 | export interface IPropTypes { 15 | apiKey: string; 16 | licenseKey: string; 17 | cloudChannel: string; 18 | id: string; 19 | init: EditorOptions & { selector?: undefined; target?: undefined }; 20 | initialValue: string; 21 | outputFormat: 'html' | 'text'; 22 | inline: boolean; 23 | modelEvents: string[] | string; 24 | plugins: string[] | string; 25 | tagName: string; 26 | toolbar: string[] | string; 27 | modelValue: string; 28 | disabled: boolean; 29 | readonly: boolean; 30 | tinymceScriptSrc: string; 31 | } 32 | 33 | export const editorProps: CopyProps = { 34 | apiKey: String, 35 | licenseKey: String, 36 | cloudChannel: String, 37 | id: String, 38 | init: Object, 39 | initialValue: String, 40 | inline: Boolean, 41 | modelEvents: [ String, Array ], 42 | plugins: [ String, Array ], 43 | tagName: String, 44 | toolbar: [ String, Array ], 45 | modelValue: String, 46 | disabled: Boolean, 47 | readonly: Boolean, 48 | tinymceScriptSrc: String, 49 | outputFormat: { 50 | type: String, 51 | validator: (prop: string) => prop === 'html' || prop === 'text' 52 | }, 53 | }; 54 | -------------------------------------------------------------------------------- /src/main/ts/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2018-present, Ephox, Inc. 3 | * 4 | * This source code is licensed under the Apache 2 license found in the 5 | * LICENSE file in the root directory of this source tree. 6 | * 7 | */ 8 | 9 | import { Editor } from './components/Editor'; 10 | 11 | export default Editor; 12 | -------------------------------------------------------------------------------- /src/stories/Editor.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Story } from '@storybook/vue3'; 2 | import { onBeforeMount, ref } from 'vue'; 3 | import { ScriptLoader } from '../main/ts/ScriptLoader'; 4 | 5 | import type { EditorEvent, Editor as TinyMCEEditor } from 'tinymce'; 6 | import { Editor } from '../main/ts/components/Editor'; 7 | 8 | const apiKey = 'qagffr3pkuv17a8on1afax661irst1hbr4e6tbv888sz91jc'; 9 | const content = ` 10 |

11 | TinyMCE provides a full-featured rich text editing experience, and a featherweight download. 12 |

13 |

14 | No matter what you're building, TinyMCE has got you covered. 15 |

`; 16 | 17 | let lastChannel = '5'; 18 | const getConf = (stringConf: string) => { 19 | let conf = {}; 20 | console.log('parsing: ', stringConf); 21 | try { 22 | conf = Function('"use strict";return (' + stringConf + ')')(); 23 | } catch (err) { 24 | console.error('failed to parse configuration: ', err); 25 | } 26 | return conf; 27 | } 28 | 29 | const removeTiny = () => { 30 | delete (window as any).tinymce; 31 | delete (window as any).tinyMCE; 32 | }; 33 | 34 | const loadTiny = (currentArgs: any) => { 35 | const channel = currentArgs.channel || lastChannel; // Storybook is not handling the default for select well 36 | if (channel !== lastChannel) { 37 | removeTiny(); 38 | ScriptLoader.reinitialize(); 39 | ScriptLoader.load(document, `https://cdn.tiny.cloud/1/${apiKey}/tinymce/${channel}/tinymce.min.js`, () => { 40 | console.log('script ready'); 41 | }); 42 | lastChannel = channel; 43 | } 44 | }; 45 | 46 | 47 | export default { 48 | title: 'Editor', 49 | component: Editor, 50 | argTypes: { 51 | channel: { 52 | table: { 53 | defaultValue: {summary: '5'} 54 | }, 55 | defaultValue: '7', 56 | options: ['5', '5-dev', '5-testing', '6-testing', '6-stable', '7-dev', '7-testing', '7-stable', '7.3', '7.4', '7.6'], 57 | control: { type: 'select'} 58 | }, 59 | disabled: { 60 | defaultValue: false, 61 | control: 'boolean' 62 | }, 63 | readonly: { 64 | defaultValue: false, 65 | control: 'boolean' 66 | }, 67 | conf: { 68 | defaultValue: '{height: 300}', 69 | control: { type: 'text' } 70 | } 71 | }, 72 | parameters: { 73 | previewTabs: { 74 | docs: { hidden: true } 75 | }, 76 | controls: { 77 | hideNoControlsWarning: true 78 | } 79 | } 80 | }; 81 | 82 | export const Iframe: Story = (args) => ({ 83 | components: {Editor}, 84 | setup() { 85 | onBeforeMount(() => { 86 | loadTiny(args); 87 | }); 88 | const cc = args.channel || lastChannel; 89 | const conf = getConf(args.conf); 90 | return { 91 | apiKey, 92 | content, 93 | cloudChannel: cc, 94 | conf 95 | } 96 | }, 97 | template: '

Ready

' 98 | }); 99 | 100 | export const Inline: Story = (args) => ({ 101 | components: { Editor }, 102 | setup() { 103 | onBeforeMount(() => { 104 | loadTiny(args); 105 | }); 106 | const cc = args.channel || lastChannel; 107 | const conf = getConf(args.conf); 108 | return { 109 | apiKey, 110 | content, 111 | cloudChannel: cc, 112 | conf 113 | } 114 | }, 115 | template: ` 116 |
117 | 123 |
` 124 | }); 125 | 126 | export const Controlled: Story = (args) => ({ 127 | components: { Editor }, 128 | setup() { 129 | onBeforeMount(() => { 130 | loadTiny(args); 131 | }); 132 | const cc = args.channel || lastChannel; 133 | const conf = getConf(args.conf); 134 | const log = (e: EditorEvent, editor: TinyMCEEditor) => {console.log(e);}; 135 | const controlledContent = ref(content); 136 | return { 137 | apiKey, 138 | content: controlledContent, 139 | cloudChannel: cc, 140 | conf, 141 | log 142 | } 143 | }, 144 | template: ` 145 |
146 | 152 | 157 |
158 |
` 159 | }); 160 | 161 | export const Disable: Story = (args) => ({ 162 | components: { Editor }, 163 | setup() { 164 | onBeforeMount(() => { 165 | loadTiny(args); 166 | }); 167 | const cc = args.channel || lastChannel; 168 | const conf = getConf(args.conf); 169 | const disabled = ref(args.disabled); 170 | const toggleDisabled = (_) => { 171 | disabled.value = !disabled.value; 172 | } 173 | 174 | return { 175 | apiKey, 176 | content, 177 | cloudChannel: cc, 178 | conf, 179 | disabled, 180 | toggleDisabled 181 | } 182 | }, 183 | template: ` 184 |
185 | 186 | 192 |
` 193 | }); 194 | 195 | export const Readonly: Story = (args) => ({ 196 | components: { Editor }, 197 | setup() { 198 | onBeforeMount(() => { 199 | loadTiny(args); 200 | }); 201 | const cc = args.channel || lastChannel; 202 | const conf = getConf(args.conf); 203 | const readonly = ref(args.readonly); 204 | const toggleReadonly = (_) => { 205 | readonly.value = !readonly.value; 206 | } 207 | return { 208 | apiKey, 209 | content, 210 | cloudChannel: cc, 211 | conf, 212 | readonly, 213 | toggleReadonly 214 | } 215 | }, 216 | template: ` 217 |
218 | 219 | 225 |
` 226 | }); 227 | -------------------------------------------------------------------------------- /src/test/ts/alien/Loader.ts: -------------------------------------------------------------------------------- 1 | import { Fun } from '@ephox/katamari'; 2 | import { Attribute, SugarBody, SugarElement, Insert, Remove, SelectorFind } from '@ephox/sugar'; 3 | import Editor from 'src/main/ts/index'; 4 | 5 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 6 | // @ts-ignore 7 | import { createApp } from 'vue/dist/vue.esm-bundler.js'; // Use runtime compiler 8 | 9 | export interface Context { 10 | editor: any; 11 | vm: any; 12 | } 13 | 14 | const getRoot = () => SelectorFind.descendant(SugarBody.body(), '#root').getOrThunk(() => { 15 | const root = SugarElement.fromTag('div'); 16 | Attribute.set(root, 'id', 'root'); 17 | Insert.append(SugarBody.body(), root); 18 | return root; 19 | }); 20 | 21 | // eslint-disable-next-line max-len 22 | const pRender = (data: Record = {}, template: string = ``): Promise> => new Promise((resolve) => { 23 | const root = getRoot(); 24 | const mountPoint = SugarElement.fromTag('div'); 25 | Insert.append(root, mountPoint); 26 | 27 | const originalInit = data.init || {}; 28 | const originalSetup = originalInit.setup || Fun.noop; 29 | 30 | const vm = createApp({ 31 | template, 32 | components: { 33 | Editor 34 | }, 35 | data: () => ({ 36 | ...data, 37 | outputFormat: 'text', 38 | init: { 39 | ...originalInit, 40 | setup: (editor: any) => { 41 | originalSetup(editor); 42 | editor.on('SkinLoaded', () => { 43 | setTimeout(() => { 44 | resolve({ editor, vm }); 45 | }, 0); 46 | }); 47 | } 48 | } 49 | }), 50 | }).mount(mountPoint.dom); 51 | }); 52 | 53 | const remove = () => { 54 | Remove.remove(getRoot()); 55 | }; 56 | 57 | export { pRender, remove, getRoot }; -------------------------------------------------------------------------------- /src/test/ts/alien/TestHelper.ts: -------------------------------------------------------------------------------- 1 | import { Global, Strings, Arr } from '@ephox/katamari'; 2 | import { SugarElement, Attribute, SelectorFilter, Remove } from '@ephox/sugar'; 3 | import { ScriptLoader } from '../../../main/ts/ScriptLoader'; 4 | 5 | const VALID_API_KEY = 'qagffr3pkuv17a8on1afax661irst1hbr4e6tbv888sz91jc'; 6 | 7 | // Function to clean up and remove TinyMCE-related scripts and links from the document 8 | const cleanupGlobalTinymce = () => { 9 | ScriptLoader.reinitialize(); 10 | // This deletes global references to TinyMCE, to ensure a clean slate for each initialization when tests are switching to a different editor versions. 11 | delete Global.tinymce; 12 | delete Global.tinyMCE; 13 | // Helper function to check if an element has a TinyMCE-related URI in a specific attribute 14 | const hasTinymceUri = (attrName: string) => (elm: SugarElement) => 15 | Attribute.getOpt(elm, attrName).exists((src) => Strings.contains(src, 'tinymce')); 16 | // Find all script and link elements that have a TinyMCE-related URI 17 | const elements = Arr.flatten([ 18 | Arr.filter(SelectorFilter.all('script'), hasTinymceUri('src')), 19 | Arr.filter(SelectorFilter.all('link'), hasTinymceUri('href')), 20 | ]); 21 | Arr.each(elements, Remove.remove); 22 | }; 23 | 24 | export { 25 | VALID_API_KEY, 26 | cleanupGlobalTinymce, 27 | }; -------------------------------------------------------------------------------- /src/test/ts/atomic/UtilsTest.ts: -------------------------------------------------------------------------------- 1 | import { Assertions } from '@ephox/agar'; 2 | import { describe, it } from '@ephox/bedrock-client'; 3 | import { Arr } from '@ephox/katamari'; 4 | import { isValidKey } from 'src/main/ts/Utils'; 5 | 6 | describe('UtilsTest', () => { 7 | const checkValidKey = (key: string, expected: boolean) => { 8 | const actual = isValidKey(key); 9 | Assertions.assertEq('Key should be valid in both camelCase and lowercase', expected, actual); 10 | }; 11 | 12 | // eslint-disable-next-line max-len 13 | // v-on event listeners inside DOM templates will be automatically transformed to lowercase (due to HTML’s case-insensitivity), so v-on:myEvent would become v-on:myevent. ref: https://eslint.vuejs.org/rules/custom-event-name-casing 14 | 15 | describe('Valid event name tests', () => { 16 | const validKeys = [ 17 | { key: 'onKeyUp', description: 'camelCase event name "onKeyUp"' }, 18 | { key: 'onkeyup', description: 'lowercase event name "onkeyup"' } 19 | ]; 20 | 21 | Arr.each(validKeys, ({ key, description }) => { 22 | it(`should validate ${description}`, () => { 23 | checkValidKey(key, true); 24 | }); 25 | }); 26 | }); 27 | 28 | it('should invalidate unknown event name "onDisable"', () => { 29 | checkValidKey('onDisable', false); 30 | }); 31 | }); -------------------------------------------------------------------------------- /src/test/ts/browser/InitTest.ts: -------------------------------------------------------------------------------- 1 | import { Assertions, Keyboard, Keys } from '@ephox/agar'; 2 | import { pRender, remove } from '../alien/Loader'; 3 | import { VersionLoader } from '@tinymce/miniature'; 4 | import { SugarElement } from '@ephox/sugar'; 5 | import { describe, it, afterEach, before, context, after } from '@ephox/bedrock-client'; 6 | import { cleanupGlobalTinymce, VALID_API_KEY } from '../alien/TestHelper'; 7 | import { Arr } from '@ephox/katamari'; 8 | 9 | describe('Editor Component Initialization Tests', () => { 10 | // eslint-disable-next-line @typescript-eslint/require-await 11 | const pFakeType = async (str: string, vmContext: any) => { 12 | vmContext.editor.getBody().innerHTML = '

' + str + '

'; 13 | Keyboard.keystroke(Keys.space(), {}, SugarElement.fromDom(vmContext.editor.getBody()) as SugarElement); 14 | }; 15 | 16 | Arr.each([ '4', '5', '6', '7' as const ], (version) => { 17 | context(`Version: ${version}`, () => { 18 | 19 | before(async () => { 20 | await VersionLoader.pLoadVersion(version); 21 | }); 22 | 23 | after(() => { 24 | cleanupGlobalTinymce(); 25 | }); 26 | 27 | afterEach(() => { 28 | remove(); 29 | }); 30 | 31 | it('should not be inline by default', async () => { 32 | const vmContext = await pRender({}, ` 33 | `); 36 | Assertions.assertEq('Editor should not be inline', false, vmContext.editor.inline); 37 | }); 38 | 39 | it('should be inline with inline attribute in template', async () => { 40 | const vmContext = await pRender({}, ` 41 | `); 45 | Assertions.assertEq('Editor should be inline', true, vmContext.editor.inline); 46 | }); 47 | 48 | it('should be inline with inline option in init', async () => { 49 | const vmContext = await pRender({ init: { inline: true }}); 50 | Assertions.assertEq('Editor should be inline', true, vmContext.editor.inline); 51 | }); 52 | 53 | it('should handle one-way binding with output-format="text"', async () => { 54 | const vmContext = await pRender({ 55 | content: undefined, 56 | }, ` 57 | 63 | `); 64 | await pFakeType('A', vmContext); 65 | Assertions.assertEq('Content emitted should be of format="text"', 'A', vmContext.vm.content); 66 | }); 67 | 68 | it('should handle one-way binding with output-format="html"', async () => { 69 | const vmContext = await pRender({ 70 | content: undefined, 71 | }, ` 72 | 78 | `); 79 | await pFakeType('A', vmContext); 80 | Assertions.assertEq('Content emitted should be of format="html"', '

A

', vmContext.vm.content); 81 | }); 82 | }); 83 | }); 84 | }); 85 | -------------------------------------------------------------------------------- /src/test/ts/browser/LoadTinyTest.ts: -------------------------------------------------------------------------------- 1 | import { Assertions } from '@ephox/agar'; 2 | import { beforeEach, context, describe, it } from '@ephox/bedrock-client'; 3 | import { Global } from '@ephox/katamari'; 4 | import { pRender, remove } from '../alien/Loader'; 5 | import { cleanupGlobalTinymce, VALID_API_KEY } from '../alien/TestHelper'; 6 | 7 | describe('LoadTinyTest', () => { 8 | 9 | const AssertTinymceVersion = (version: '4' | '5' | '6' | '7') => { 10 | Assertions.assertEq(`Loaded version of TinyMCE should be ${version}`, version, Global.tinymce.majorVersion); 11 | }; 12 | 13 | context('LoadTinyTest', () => { 14 | 15 | beforeEach(() => { 16 | remove(); 17 | cleanupGlobalTinymce(); 18 | }); 19 | 20 | it('Should be able to load local version of TinyMCE 7 using the tinymceScriptSrc prop', async () => { 21 | await pRender({}, ` 22 | 26 | `); 27 | 28 | AssertTinymceVersion('7'); 29 | }); 30 | 31 | it('Should be able to load local version of TinyMCE 6 using the tinymceScriptSrc prop', async () => { 32 | await pRender({}, ` 33 | 37 | `); 38 | 39 | AssertTinymceVersion('6'); 40 | }); 41 | 42 | it('Should be able to load local version of TinyMCE 5 using the tinymceScriptSrc prop', async () => { 43 | await pRender({}, ` 44 | 48 | `); 49 | 50 | AssertTinymceVersion('5'); 51 | }); 52 | 53 | it('Should be able to load local version of TinyMCE 4 using the tinymceScriptSrc prop', async () => { 54 | await pRender({}, ` 55 | 59 | `); 60 | 61 | AssertTinymceVersion('4'); 62 | }); 63 | 64 | it('Should be able to load TinyMCE 7 from Cloud', async () => { 65 | await pRender({}, ` 66 | 71 | `); 72 | 73 | AssertTinymceVersion('7'); 74 | Assertions.assertEq( 75 | 'TinyMCE 7 should have been loaded from Cloud', 76 | `https://cdn.tiny.cloud/1/${VALID_API_KEY}/tinymce/7-stable`, 77 | Global.tinymce.baseURI.source 78 | ); 79 | }); 80 | 81 | it('Should be able to load TinyMCE 6 from Cloud', async () => { 82 | await pRender({}, ` 83 | 88 | `); 89 | 90 | AssertTinymceVersion('6'); 91 | Assertions.assertEq( 92 | 'TinyMCE 6 should have been loaded from Cloud', 93 | `https://cdn.tiny.cloud/1/${VALID_API_KEY}/tinymce/6-stable`, 94 | Global.tinymce.baseURI.source 95 | ); 96 | }); 97 | 98 | it('Should be able to load TinyMCE 5 from Cloud', async () => { 99 | await pRender({}, ` 100 | 105 | `); 106 | 107 | AssertTinymceVersion('5'); 108 | Assertions.assertEq( 109 | 'TinyMCE 5 should have been loaded from Cloud', 110 | `https://cdn.tiny.cloud/1/${VALID_API_KEY}/tinymce/5-stable`, 111 | Global.tinymce.baseURI.source 112 | ); 113 | }); 114 | }); 115 | }); -------------------------------------------------------------------------------- /stories/index.js: -------------------------------------------------------------------------------- 1 | import { storiesOf } from '@storybook/vue'; 2 | 3 | import { Editor } from '../src/main/ts/components/Editor'; 4 | 5 | const apiKey = 'qagffr3pkuv17a8on1afax661irst1hbr4e6tbv888sz91jc'; 6 | const content = ` 7 |

8 | TinyMCE provides a full-featured rich text editing experience, and a featherweight download. 9 |

10 |

11 | No matter what you're building, TinyMCE has got you covered. 12 |

`; 13 | 14 | storiesOf('tinymce-vue', module) 15 | .add( 16 | 'Iframe editor', 17 | () => ({ 18 | components: { Editor }, 19 | data: () => ({ content }), 20 | template: ` 21 | ` 26 | }), 27 | { notes: 'Iframe editor.' } 28 | ) 29 | .add( 30 | 'Inline editor', 31 | () => ({ 32 | components: { Editor }, 33 | data: () => ({ content }), 34 | template: ` 35 |
36 | 41 |
42 | ` 43 | }), 44 | { notes: 'Inline editor.' } 45 | ) 46 | .add( 47 | 'Controlled input', 48 | () => ({ 49 | components: { Editor }, 50 | data: () => ({ content }), 51 | methods: { 52 | log: (e, _editor) => console.log(e) 53 | }, 54 | template: ` 55 |
56 | 62 | 67 |
68 |
69 | ` 70 | }), 71 | { notes: 'Example of usage as as a controlled component.' } 72 | ) 73 | .add( 74 | 'Disable button', () => ({ 75 | components: { Editor }, 76 | data: () => ({ content, disabled: true }), 77 | methods: { 78 | toggleDisabled(_e) { 79 | this.disabled = !this.disabled; 80 | } 81 | }, 82 | template: ` 83 |
84 | 85 | 91 |
92 | ` 93 | }), 94 | { notes: 'Example with setting the editor into readonly mode using the disabled prop.' } 95 | ) 96 | .add( 97 | 'cloudChannel set to 5-dev', 98 | () => ({ 99 | components: { Editor }, 100 | data: () => ({ content }), 101 | methods: { 102 | log: (e, _editor) => console.log(e) 103 | }, 104 | template: ` 105 | 111 | ` 112 | }), 113 | { notes: 'Editor with cloudChannel set to 5-dev, please make sure to reload page to load TinyMCE 5.' } 114 | ); 115 | -------------------------------------------------------------------------------- /tsconfig.browser.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "declaration": false, 5 | "module": "es2015", 6 | "target": "es5", 7 | "outDir": "./lib/browser", 8 | "rootDir": "src" 9 | }, 10 | "include": [ 11 | "src/main/**/*.ts" 12 | ], 13 | "exclude": [] 14 | } -------------------------------------------------------------------------------- /tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "commonjs", 5 | "outDir": "./lib/cjs", 6 | "rootDir": "src" 7 | }, 8 | "include": [ 9 | "src/main/**/*.ts" 10 | ], 11 | "exclude": [] 12 | } -------------------------------------------------------------------------------- /tsconfig.es2015.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "module": "es6", 5 | "outDir": "./lib/es2015", 6 | "rootDir": "src" 7 | }, 8 | "include": [ 9 | "src/main/**/*.ts" 10 | ], 11 | "exclude": [] 12 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "emitDecoratorMetadata": true, 4 | "experimentalDecorators": true, 5 | "forceConsistentCasingInFileNames": true, 6 | "moduleResolution": "node", 7 | "noImplicitAny": true, 8 | "noUnusedLocals": true, 9 | "declaration": true, 10 | "outDir": "./lib", 11 | "preserveConstEnums": true, 12 | "strictNullChecks": true, 13 | "target": "es5", 14 | // "lib": ["es2020.string"] // fix the mathAll issue 15 | "types": [ 16 | "node" 17 | ], 18 | "strict": true, 19 | "baseUrl": ".", 20 | "skipLibCheck": true // -> https://github.com/vuejs/vue-next/issues/1102 21 | }, 22 | "include": [ 23 | "src/**/*.ts" 24 | ], 25 | "exclude": [ 26 | "src/demo/*" 27 | ] 28 | } -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import vue from '@vitejs/plugin-vue'; 2 | import path from 'path'; 3 | 4 | export default { 5 | root: 'src/demo', 6 | plugins: [vue()], 7 | resolve: { 8 | alias: [ 9 | { 10 | find: '/@', 11 | replacement: path.resolve(__dirname, './src') 12 | } 13 | ] 14 | }, 15 | server: { 16 | port: 3001 17 | } 18 | }; --------------------------------------------------------------------------------