├── .gitattributes ├── .github └── ISSUE_TEMPLATE │ └── feature_request.md ├── .gitignore ├── LICENSE ├── LICENSE.md ├── README.md ├── gulpfile.js ├── package.json └── src ├── assets ├── hypertext-app-icon.svg ├── hypertext-app-icon144.png ├── hypertext-app-icon192.png ├── hypertext-app-icon32.png ├── hypertext-app-icon512.png ├── hypertext-app-icon72.png ├── hypertext-round-icon.png ├── hypertext-round-icon.svg └── hypertext-screenshot.png ├── css ├── app.css ├── doc-wrapper.css ├── html-document-v1.css └── hypertext-skin │ ├── content.min.css │ └── skin.min.css ├── editor.html ├── help ├── about-help.html ├── images-help.html └── shortcuts-help.html ├── index.html ├── libs ├── beautify-html.js ├── codemirror-bundle.css ├── codemirror-bundle.js ├── idb-keyval.js ├── tinymce │ ├── icons │ │ └── default │ │ │ └── icons.js │ ├── models │ │ └── dom │ │ │ └── model.js │ ├── plugins │ │ ├── anchor │ │ │ └── plugin.js │ │ ├── autolink │ │ │ └── plugin.js │ │ ├── autosave │ │ │ └── plugin.js │ │ ├── codesample │ │ │ └── plugin.js │ │ ├── help │ │ │ └── plugin.js │ │ ├── insertdatetime │ │ │ └── plugin.js │ │ ├── link │ │ │ └── plugin.js │ │ ├── lists │ │ │ └── plugin.js │ │ ├── searchreplace │ │ │ └── plugin.js │ │ ├── table │ │ │ └── plugin.js │ │ ├── visualblocks │ │ │ └── plugin.js │ │ ├── visualchars │ │ │ └── plugin.js │ │ └── wordcount │ │ │ └── plugin.js │ └── tinymce.js └── typo │ ├── dictionaries │ └── en_US │ │ ├── en_US.aff │ │ └── en_US.dic │ └── typo.js ├── load-sw.js ├── manifest.json ├── scripts ├── editor-document.js ├── editor-features.js ├── editor-file-handling.js ├── editor-init-settings.js ├── editor-setup.js ├── editor-window.js ├── hypertext-app.js ├── hypertext-model.js └── hypertext-theme.js └── service-worker.js /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea 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. Ex. I'm always frustrated when [...] 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. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | node_modules/ 3 | 4 | .DS_Store 5 | package-lock.json 6 | 7 | .log 8 | .tmp 9 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Russell Beattie 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 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2022 Russell Beattie (http://www.russellbeattie.com) 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 | # Hypertext HTML Document Editor 2 | 3 | ## About 4 | 5 | Version 1.0 - Beta. October 2022. 6 | 7 | Created by [Russell Beattie](https://www.russellbeattie.com). 8 | 9 | Find the project code here: 10 | . It\'s open source, 11 | pull up a chair and feel free to try it out and submit bug reports. Pull 12 | requests will be happily welcomed with open arms as long as you\'re not 13 | an idiot. 14 | 15 | Also, there\'s zero tracking in the app itself. I haven\'t even turned 16 | on logging on the server. When I update the app, it should update your 17 | local copy when you open it\... If the app seems to suddenly refresh on 18 | you, it\'s working as intended, though not ideally. 19 | 20 | **Disclaimer**: I wouldn\'t use this thing for anything important just 21 | yet, or you know, try to edit any HTML file you particularly care about. 22 | It tends to mangle markup that the app hasn\'t created itself. Don\'t 23 | say I didn\'t warn you. 24 | 25 | -Russ 26 | 27 | ## Open Source Projects 28 | 29 | Powered by the hard work of the folks at 30 | [TinyMCE](https://github.com/tinymce/){style="font-weight:bold"}, plus a 31 | bunch of hours of my own work trying to figure out why the hell they 32 | wrapped all the standard DOM APIs and what I needed to do to make it 33 | play nice. Thanks guys! 34 | 35 | Plus I slapped in [CodeMirror](https://github.com/codemirror) for the 36 | code bits. It\'s still not working correctly, but looks pretty. 37 | 38 | Thanks to whoever it is who made this: 39 | . It was super helpful 40 | in showing me how some of the wonky bits worked. 41 | 42 | Also thanks to the Google dev relations guys for the file stuff here: 43 | . 44 | 45 | -------------------------------------------------------------------------------- /gulpfile.js: -------------------------------------------------------------------------------- 1 | 2 | const fs = require('fs'); 3 | const del = require('del'); 4 | const glob = require('glob'); 5 | const gulp = require('gulp'); 6 | const copy = require('gulp-copy'); 7 | const connect = require('gulp-connect'); 8 | 9 | const SRC_DIR = 'src'; 10 | const DEST_DIR = 'build/editor'; 11 | 12 | function clean() { 13 | return del(['build/**', 'tmp/**']); 14 | } 15 | 16 | function copyStatic() { 17 | const filesToCopy = [ 18 | `${SRC_DIR}/*.html`, 19 | `${SRC_DIR}/load-sw.js`, 20 | `${SRC_DIR}/assets/**/*`, 21 | `${SRC_DIR}/help/**/*`, 22 | `${SRC_DIR}/css/**/*`, 23 | `${SRC_DIR}/scripts/**/*`, 24 | `${SRC_DIR}/libs/**/*`, 25 | `${SRC_DIR}/manifest.json` 26 | ]; 27 | return gulp.src(filesToCopy) 28 | .pipe(copy(DEST_DIR, {prefix: 1})); 29 | } 30 | 31 | // function copyLibs() { 32 | // let base = './node_modules'; 33 | // const filesToCopy = [ 34 | // `${base}/tinymce/plugins/anchor/plugin.js`, 35 | // `${base}/tinymce/plugins/autolink/plugin.js`, 36 | // `${base}/tinymce/plugins/autosave/plugin.js`, 37 | // `${base}/tinymce/plugins/codesample/plugin.js`, 38 | // `${base}/tinymce/plugins/help/plugin.js`, 39 | // `${base}/tinymce/plugins/insertdatetime/plugin.js`, 40 | // `${base}/tinymce/plugins/link/plugin.js`, 41 | // `${base}/tinymce/plugins/lists/plugin.js`, 42 | // `${base}/tinymce/plugins/searchreplace/plugin.js`, 43 | // `${base}/tinymce/plugins/table/plugin.js`, 44 | // `${base}/tinymce/plugins/visualblocks/plugin.js`, 45 | // `${base}/tinymce/plugins/visualchars/plugin.js`, 46 | // `${base}/tinymce/plugins/wordcount/plugin.js`, 47 | // `${base}/tinymce/models/dom/model.js`, 48 | // `${base}/tinymce/icons/default/icons.js`, 49 | // `${base}/tinymce/tinymce.js` 50 | // ]; 51 | // return gulp.src(filesToCopy) 52 | // .pipe(copy(`${DEST_DIR}/libs`, {prefix: 1})); 53 | // } 54 | 55 | 56 | function createManifest(cb) { 57 | let files = glob.sync(`./**`, {cwd: DEST_DIR, nodir: true, nomount: true}); 58 | files.unshift('./service-worker.js'); 59 | files.unshift('./'); 60 | files.sort(); 61 | 62 | let fileString = JSON.stringify(files, null, ' '); 63 | 64 | let buildDateTime = (new Date()).toISOString(); 65 | 66 | let sw = fs.readFileSync(`${SRC_DIR}/service-worker.js`).toString(); 67 | sw = sw.replace('${cacheName}', buildDateTime); 68 | sw = sw.replace('${files}', fileString); 69 | 70 | fs.writeFileSync(`${DEST_DIR}/service-worker.js`, sw); 71 | 72 | let loadSw = fs.readFileSync(`${SRC_DIR}/load-sw.js`).toString(); 73 | loadSw = loadSw.replace('${buildDateTime}', buildDateTime); 74 | 75 | fs.writeFileSync(`${DEST_DIR}/load-sw.js`, loadSw); 76 | 77 | 78 | cb(); 79 | } 80 | 81 | function serveDev() { 82 | gulp.watch(`${SRC_DIR}/**`, { events: 'all' }, function(cb) { 83 | exports.build(); 84 | cb(); 85 | }); 86 | exports.build(); 87 | return connect.server({root: 'build', host: '0.0.0.0'}); 88 | } 89 | 90 | 91 | exports.clean = clean; 92 | exports.serve = serveDev; 93 | 94 | exports.build = gulp.series( 95 | clean, 96 | // copyLibs, 97 | copyStatic, 98 | createManifest 99 | ); 100 | 101 | exports.buildProd = gulp.series( 102 | clean, 103 | // copyLibs, 104 | copyStatic, 105 | createManifest 106 | ); 107 | 108 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hypertext-editor", 3 | "version": "1.0.0", 4 | "description": "Hypertext HTML Document Editor", 5 | "author": "Russell Beattie (http://www.russellbeattie.com/)", 6 | "homepage": "https://www.hypertext.plus", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/russellbeattie/hypertext-editor.git" 11 | }, 12 | "browserslist": [ 13 | "last 2 versions" 14 | ], 15 | "dependencies": { 16 | "del": "^5.0.0", 17 | "glob": "8.0.3", 18 | "gulp": "^4.0.2", 19 | "gulp-connect": "^5.7.0", 20 | "gulp-copy": "^4.0.1", 21 | "tinymce": "6.2.0" 22 | }, 23 | "scripts": { 24 | "build": "gulp build", 25 | "build-prod": "gulp buildProd", 26 | "clean": "gulp clean", 27 | "serve": "gulp serve", 28 | "start": "gulp serve" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/assets/hypertext-app-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/hypertext-app-icon144.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/russellbeattie/hypertext-editor/9f11d5685811e70926e5847bcdf604d4fb21eecb/src/assets/hypertext-app-icon144.png -------------------------------------------------------------------------------- /src/assets/hypertext-app-icon192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/russellbeattie/hypertext-editor/9f11d5685811e70926e5847bcdf604d4fb21eecb/src/assets/hypertext-app-icon192.png -------------------------------------------------------------------------------- /src/assets/hypertext-app-icon32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/russellbeattie/hypertext-editor/9f11d5685811e70926e5847bcdf604d4fb21eecb/src/assets/hypertext-app-icon32.png -------------------------------------------------------------------------------- /src/assets/hypertext-app-icon512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/russellbeattie/hypertext-editor/9f11d5685811e70926e5847bcdf604d4fb21eecb/src/assets/hypertext-app-icon512.png -------------------------------------------------------------------------------- /src/assets/hypertext-app-icon72.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/russellbeattie/hypertext-editor/9f11d5685811e70926e5847bcdf604d4fb21eecb/src/assets/hypertext-app-icon72.png -------------------------------------------------------------------------------- /src/assets/hypertext-round-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/russellbeattie/hypertext-editor/9f11d5685811e70926e5847bcdf604d4fb21eecb/src/assets/hypertext-round-icon.png -------------------------------------------------------------------------------- /src/assets/hypertext-round-icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/hypertext-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/russellbeattie/hypertext-editor/9f11d5685811e70926e5847bcdf604d4fb21eecb/src/assets/hypertext-screenshot.png -------------------------------------------------------------------------------- /src/css/app.css: -------------------------------------------------------------------------------- 1 | 2 | :root { 3 | --ui-fg-color: #222f3e; 4 | --ui-bg-color: #FDFDFD; 5 | } 6 | 7 | html { 8 | -webkit-font-smoothing: antialiased; 9 | text-rendering: geometricPrecision; 10 | } 11 | 12 | body { 13 | margin: 0; 14 | padding: 0; 15 | background-color: var(--ui-bg-color); 16 | color: var(--ui-fg-color); 17 | display: flex; 18 | flex-direction: column; 19 | overflow: hidden; 20 | } 21 | 22 | .tox-tinymce { 23 | font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; 24 | font: menu; 25 | } 26 | 27 | .tox:not(.tox-tinymce-inline) .tox-editor-header { 28 | border-bottom: none; 29 | box-shadow: none; 30 | transition: initial; 31 | } 32 | 33 | 34 | 35 | a#logo { 36 | display: block; 37 | cursor: pointer; 38 | width: 28px; 39 | height: 28px; 40 | padding: 2px; 41 | background: url("../assets/hypertext-round-icon.svg") center center no-repeat; 42 | justify-self: flex-end; 43 | align-self: center; 44 | margin-left: auto; 45 | margin-right: 5px; 46 | 47 | } 48 | 49 | .not-supported { 50 | font-family: system-ui; 51 | font-size: 1.5rem; 52 | padding: 20px; 53 | margin: 200px auto; 54 | color: var(--ui-fg-color); 55 | max-width: 700px; 56 | border: 1px solid #020887; 57 | text-align: center; 58 | align-self: center; 59 | 60 | } 61 | 62 | .cke_1.cke_chrome, .cke_chrome { 63 | border: none; 64 | } 65 | 66 | textarea.cke_source { 67 | padding: 2rem !important; 68 | max-width: 95vw; 69 | font-size: 1rem !important; 70 | } 71 | 72 | #cke_tooltip { 73 | display: none !important; 74 | } 75 | 76 | .tox:not([dir=rtl]) .tox-toolbar__group:not(:last-of-type) { 77 | border-right: 1px solid #ddd !important; 78 | } 79 | 80 | .tox .tox-sidebar-wrap { 81 | /*flex-direction: row-reverse !important;*/ 82 | } 83 | 84 | #css-wrapper { 85 | width: 500px; 86 | display: flex; 87 | flex-direction: column; 88 | } 89 | 90 | #css-header { 91 | padding: 12px 8px 8px; 92 | border-bottom: 1px solid #ccc; 93 | height: 46px; 94 | display: flex; 95 | justify-content: space-between; 96 | } 97 | 98 | #css-header .tox-icon { 99 | padding-right: 5px; 100 | cursor: pointer; 101 | } 102 | 103 | #css-editor { 104 | width: 500px; 105 | height: calc(100% - 46px); 106 | } 107 | 108 | .toolbar-closed { 109 | transform: translateY(100%); 110 | transition: transform ease-out 0.3s; 111 | } 112 | 113 | .toolbar-open { 114 | transform: translateY(0); 115 | transition: transform ease-out 0.3s; 116 | } 117 | 118 | input::placeholder { 119 | color: #ccc; 120 | } 121 | 122 | 123 | .tox .tox-editor-header { 124 | background-color: var(--ui-bg-color); 125 | } 126 | 127 | 128 | .tox .tox-menubar { 129 | font: menu; 130 | background-color: var(--ui-bg-color); 131 | color: var(--ui-fg-color); 132 | } 133 | 134 | .tox .tox-menubar svg { 135 | transform: scale(0.75); 136 | } 137 | 138 | .tox .tox-menubar svg path { 139 | fill: var(--ui-fg-color); 140 | } 141 | 142 | .tox .tox-mbtn__select-label { 143 | color: var(--ui-fg-color); 144 | } 145 | 146 | .tox-icon .tox-tbtn__icon-wrap { 147 | color: var(--ui-fg-color); 148 | } 149 | 150 | .tox-platform-touch .tox-mbtn__select-label { 151 | display: none; 152 | } 153 | 154 | html.file-menu .tox-menubar .tox-mbtn__select-label { 155 | display: initial; 156 | } 157 | 158 | html.file-menu .tox-menubar .tox-tbtn__icon-wrap { 159 | display: none; 160 | } 161 | 162 | 163 | .tox .tox-mbtn:focus:not(:disabled) .tox-mbtn__select-label { 164 | color: var(--ui-fg-color); 165 | } 166 | 167 | .tox .tox-mbtn:focus:not(:disabled) svg path { 168 | color: var(--ui-fg-color); 169 | } 170 | 171 | .tox .tox-mbtn:hover:not(:disabled):not(.tox-mbtn--active) .tox-mbtn__select-label { 172 | color: var(--ui-fg-color); 173 | } 174 | 175 | .tox .tox-mbtn:hover:not(:disabled):not(.tox-mbtn--active) svg path { 176 | fill: var(--ui-fg-color); 177 | } 178 | 179 | .tox .tox-mbtn--active { 180 | fill: var(--ui-fg-color); 181 | } 182 | 183 | .tox .tox-mbtn--active svg path { 184 | fill: var(--ui-fg-color); 185 | } 186 | 187 | .tox .tox-mbtn--active .tox-mbtn__select-label { 188 | color: var(--ui-fg-color); 189 | fill: var(--ui-fg-color); 190 | } 191 | 192 | .tox .tox-mbtn:focus:not(:disabled) { 193 | color: var(--ui-fg-color); 194 | fill: var(--ui-fg-color); 195 | } 196 | 197 | 198 | html.dark-mode { 199 | filter: invert(100%) hue-rotate(180deg); 200 | background-color: var(--ui-fg-color); 201 | } 202 | 203 | html.dark-mode img, html.dark-mode a#logo { 204 | filter: none; 205 | 206 | } 207 | 208 | html.dark-mode .tox-menubar { 209 | /*filter: invert(100%) hue-rotate(180deg);*/ 210 | } 211 | 212 | .CodeMirror * { 213 | font-size: 0.9rem !important; 214 | } 215 | 216 | @media (prefers-color-scheme: dark) { 217 | 218 | html { 219 | filter: invert(100%) hue-rotate(180deg); 220 | /*background-color: var(--ui-fg-color);*/ 221 | } 222 | 223 | html img, html a#logo { 224 | filter: none; 225 | } 226 | 227 | html .tox-menubar { 228 | /*filter: invert(100%) hue-rotate(180deg);*/ 229 | } 230 | 231 | html.dark-mode { 232 | filter: none; 233 | } 234 | 235 | html.dark-mode img, html.dark-mode a#logo { 236 | filter: none; 237 | } 238 | 239 | html.dark-mode .tox-menubar { 240 | filter: none; 241 | } 242 | 243 | } 244 | 245 | @media (display-mode: window-controls-overlay) { 246 | 247 | :root { 248 | --ui-fg-color: #fff; 249 | --ui-bg-color: #020887; 250 | } 251 | 252 | body{ 253 | margin-top: env(titlebar-area-height, 33px); 254 | } 255 | 256 | .tox .tox-menubar { 257 | position: fixed; 258 | left: env(titlebar-area-x, 0); 259 | top: env(titlebar-area-y, 0); 260 | width: env(titlebar-area-width, 100%); 261 | height: env(titlebar-area-height, 33px); 262 | -webkit-app-region: drag; 263 | app-region: drag; 264 | overflow: hidden; 265 | } 266 | 267 | .tox-mbtn, #logo { 268 | -webkit-app-region: none; 269 | app-region: none; 270 | } 271 | 272 | } 273 | 274 | 275 | @media (display-mode: window-controls-overlay) or (display-mode: browser) { 276 | 277 | :root { 278 | --ui-fg-color: #fff; 279 | --ui-bg-color: #020887; 280 | } 281 | 282 | .tox .tox-mbtn--active .tox-mbtn__select-label { 283 | color: var(--ui-bg-color); 284 | fill: var(--ui-bg-color); 285 | } 286 | 287 | .tox .tox-mbtn--active svg path { 288 | fill: var(--ui-bg-color); 289 | } 290 | 291 | .tox .tox-mbtn:focus:not(:disabled) .tox-mbtn__select-label { 292 | color: var(--ui-bg-color); 293 | } 294 | 295 | 296 | .tox .tox-mbtn:hover:not(:disabled):not(.tox-mbtn--active) .tox-mbtn__select-label { 297 | color: var(--ui-bg-color); 298 | } 299 | 300 | .tox .tox-mbtn:hover:not(:disabled):not(.tox-mbtn--active) svg path { 301 | fill: var(--ui-bg-color); 302 | } 303 | 304 | } -------------------------------------------------------------------------------- /src/css/doc-wrapper.css: -------------------------------------------------------------------------------- 1 | html { 2 | background-color: #f8f9fa; 3 | } 4 | 5 | body { 6 | -webkit-font-smoothing: antialiased; 7 | text-rendering: geometricPrecision; 8 | overflow-wrap: break-word; 9 | outline: 1px solid var(--border-color); 10 | min-height: 1088px; 11 | max-width: 816px; 12 | margin: 1rem auto; 13 | padding: 1rem 2rem 2rem; 14 | background-color: #fff; 15 | box-shadow: 0 1px 3px 1px rgba(60,64,67,.15); 16 | } 17 | 18 | img, iframe, video { 19 | max-width: -webkit-fill-available; 20 | } 21 | 22 | .mce-object { 23 | margin: auto; 24 | display: block; 25 | } 26 | 27 | script, .mce-object-script { 28 | display: block; 29 | width: 100px; 30 | height: 1.5rem; 31 | background: transparent url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%2240%22%20width%3D%2240%22%3E%3Cpath%20d%3D%22M11.8%2025q-.8%200-1.3-.5t-.5-1.3v-2.4h2.2v2h3.1V15h2.2v8.2q0%20.8-.5%201.3t-1.3.5Zm9.7%200q-.7%200-1-.4-.5-.4-.5-1.1v-1.8h2.2v1.1h4v-2h-4.7q-.6%200-1-.4-.5-.5-.5-1.1v-2.8q0-.7.4-1%20.4-.5%201.1-.5h5.3q.7%200%201.1.4.4.4.4%201.1v1.8h-2.1v-1.1h-4v2h4.6q.7%200%201.1.4.4.5.4%201.1v2.8q0%20.7-.4%201-.4.5-1%20.5Z%22%2F%3E%3C%2Fsvg%3E%0A") no-repeat center; 32 | border: solid 1px #ccc; 33 | 34 | -webkit-user-modify: read-only; 35 | -moz-user-modify: read-only; 36 | -webkit-user-select: all; 37 | -moz-user-select: all; 38 | cursor: pointer; 39 | } 40 | 41 | .mce-object-iframe, .mce-object-video, .mce-object-audio, 42 | iframe, video, audio 43 | { 44 | aspect-ratio: 16/9; 45 | background: transparent url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2224%22%20height%3D%2224%22%3E%3Cpath%20d%3D%22M4%203h16a1%201%200%200%201%201%201v16a1%201%200%200%201-1%201H4a1%201%200%200%201-1-1V4a1%201%200%200%201%201-1zm1%202v14h14V5H5zm4.79%202.565l5.64%204.028a.5.5%200%200%201%200%20.814l-5.64%204.028a.5.5%200%200%201-.79-.407V7.972a.5.5%200%200%201%20.79-.407z%22%2F%3E%3C%2Fsvg%3E%0A") no-repeat center; 46 | } 47 | 48 | .mce-object-object, 49 | .mce-object-embed, 50 | object, 51 | embed { 52 | max-height: 48px; 53 | background: transparent url("data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20height%3D%2224%22%20width%3D%2224%22%3E%3Cpath%20d%3D%22M19%2017v-3h-3v-2h3V9h2v3h3v2h-3v3ZM3%2014V4h18v3h-2V6H5v6h9v2Zm2-4V6v6-2Z%22%2F%3E%3C%2Fsvg%3E%0A") no-repeat center; 54 | } 55 | 56 | 57 | a[id]:empty { 58 | cursor: default; 59 | display: inline-block; 60 | height: 12px !important; 61 | padding: 0 2px; 62 | -webkit-user-modify: read-only; 63 | -moz-user-modify: read-only; 64 | -webkit-user-select: all; 65 | -moz-user-select: all; 66 | user-select: all; 67 | width: 8px !important; 68 | background: transparent url("data:image/svg+xml;charset=UTF-8,%3Csvg%20width%3D'8'%20height%3D'12'%20xmlns%3D'http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg'%3E%3Cpath%20d%3D'M0%200L8%200%208%2012%204.09117821%209%200%2012z'%2F%3E%3C%2Fsvg%3E%0A") no-repeat center; 69 | } 70 | 71 | html.dark-mode img { 72 | filter: invert(100%) hue-rotate(180deg); 73 | } 74 | 75 | html.dark-mode body { 76 | outline: 1px solid #808080; 77 | } 78 | 79 | @media (prefers-color-scheme: dark) { 80 | 81 | html img { 82 | filter: invert(100%) hue-rotate(180deg); 83 | } 84 | 85 | html body { 86 | outline: 1px solid #808080; 87 | } 88 | 89 | html.dark-mode img { 90 | filter: invert(100%) hue-rotate(180deg); 91 | } 92 | 93 | html.dark-mode body { 94 | outline: 1px solid #808080; 95 | } 96 | 97 | } 98 | 99 | .hidden { 100 | display: none; 101 | } 102 | 103 | body[contenteditable] .hidden { 104 | display: initial; 105 | outline: 1px dashed #ccc; 106 | } 107 | 108 | pre.tag { 109 | display: inline-block !important; 110 | padding: 0.25rem 0.5rem; 111 | border: 1px solid var(--line-color); 112 | -webkit-user-modify: read-only; 113 | -moz-user-modify: read-only; 114 | -webkit-user-select: all; 115 | -moz-user-select: all; 116 | background: var(--block-color); 117 | font-size: 0pt; 118 | margin: 1rem 0; 119 | text-align: center; 120 | } 121 | 122 | body.mce-visualblocks pre.tag { 123 | display: block; 124 | border: 1px dashed var(--line-color); 125 | white-space: pre; 126 | overflow: auto; 127 | width: initial; 128 | font-size: 1rem; 129 | tab-size: 2; 130 | text-align: left; 131 | } 132 | 133 | body.mce-visualblocks pre.tag::before { 134 | content: attr(class) ': '; 135 | } 136 | 137 | pre.tag::before { 138 | content: '{' attr(class) '}'; 139 | font-size: 1rem; 140 | 141 | } -------------------------------------------------------------------------------- /src/css/html-document-v1.css: -------------------------------------------------------------------------------- 1 | /* 2 | Hypertext HTML Document CSS v1.0 3 | https://hypertext.plus/html-document-v1.css 4 | Copyright 2022 Russell Beattie 5 | MIT-License 6 | */ 7 | 8 | :root { 9 | 10 | --page-color: #fff; 11 | 12 | --page-font-color: #111; 13 | --page-font: system-ui; 14 | --page-font-size: 15px; 15 | 16 | --h1-color: #000; 17 | --h1-font: system-ui; 18 | --h1-font-size: 2rem; 19 | 20 | --heading-color: #333; 21 | --heading-font: system-ui; 22 | --h2-font-size: 1.5rem; 23 | --h3-font-size: 1.25rem; 24 | --h4-font-size: 1rem; 25 | --h5-font-size: 1rem; 26 | 27 | --link-color: #000888; 28 | --dark-text-color: #000; 29 | --line-color: #ccc; 30 | --block-color: #fafafa; 31 | 32 | } 33 | 34 | html { 35 | box-sizing: border-box; 36 | font-size: var(--page-font-size) !important; 37 | } 38 | 39 | body { 40 | -webkit-font-smoothing: antialiased; 41 | text-rendering: geometricPrecision; 42 | overflow-wrap: break-word; 43 | background-color: var(--page-color) !important; 44 | color: var(--page-font-color) !important; 45 | font-family: var(--page-font) !important; 46 | max-width: 816px !important; 47 | padding: 1rem 2rem 2rem !important; 48 | margin: 1rem auto !important; 49 | line-height: 1.5; 50 | } 51 | 52 | a, 53 | a:visited { 54 | color: var(--link-color); 55 | text-decoration: none; 56 | } 57 | 58 | h1 { 59 | color: var(--h1-color); 60 | font-family: var(--h1-font); 61 | font-size: var(--h1-font-size); 62 | font-weight: 300; 63 | margin: 1rem 0 2rem; 64 | line-height: 1.2; 65 | } 66 | 67 | h1 a { 68 | color: var(--h1-color); 69 | } 70 | 71 | h2 { 72 | color: var(--heading-color); 73 | font-family: var(--heading-font); 74 | font-size: var(--h2-font-size); 75 | margin: 2rem 0 1rem; 76 | line-height: 1.2; 77 | border-bottom: 1px solid var(--line-color); 78 | padding-bottom: 0.5rem; 79 | } 80 | 81 | h3 { 82 | color: var(--heading-color); 83 | font-family: var(--heading-font); 84 | font-size: var(--h3-font-size); 85 | margin: 2rem 0 1rem; 86 | line-height: 1.25; 87 | } 88 | 89 | h4 { 90 | color: var(--heading-color); 91 | font-family: var(--heading-font); 92 | font-size: var(--h4-font-size); 93 | margin: 2rem 0 1rem; 94 | line-height: 1.4; 95 | } 96 | 97 | h5 { 98 | color: var(--heading-color); 99 | font-family: var(--heading-font); 100 | font-size: var(--h5-font-size); 101 | margin: 2rem 0 1rem; 102 | line-height: 1.5; 103 | font-style: italic; 104 | } 105 | 106 | h1 + h2, 107 | h2 + h3, 108 | h3 + h4 { 109 | margin-top: 1rem; 110 | } 111 | 112 | h2 a, 113 | h3 a, 114 | h4 a, 115 | h5 a, 116 | h6 a { 117 | color: var(--heading-color); 118 | } 119 | 120 | p, 121 | dl { 122 | margin: 1rem 0; 123 | } 124 | 125 | ol, 126 | ul { 127 | margin: 1rem 2rem; 128 | padding: 0; 129 | } 130 | 131 | li, 132 | dd { 133 | margin: 0.5rem 1rem; 134 | } 135 | 136 | li > p, 137 | dl > p, 138 | dd > p { 139 | margin: 0; 140 | } 141 | 142 | li > ol { 143 | margin-bottom: 2rem; 144 | } 145 | 146 | dt { 147 | margin-top: 1rem; 148 | font-weight: bold; 149 | } 150 | 151 | mark { 152 | padding: 0.25rem; 153 | } 154 | 155 | time { 156 | font-weight: bold; 157 | background-color: #fff8dc; 158 | padding: 0.5rem; 159 | } 160 | 161 | code { 162 | padding: 0.25rem; 163 | color: var(--dark-text-color); 164 | background-color: var(--block-color); 165 | } 166 | 167 | pre > code { 168 | display: block; 169 | background-color: var(--block-color) !important; 170 | padding: 1rem; 171 | margin: 2rem 0 !important; 172 | white-space: pre-wrap; 173 | tab-size: 2; 174 | border: 1px solid var(--line-color); 175 | line-height: 1.25; 176 | } 177 | 178 | blockquote { 179 | margin: 2rem; 180 | border-left: 2px solid var(--line-color); 181 | background-color: var(--block-color); 182 | padding: 1pt 1rem; 183 | } 184 | 185 | blockquote h2, 186 | blockquote h3, 187 | blockquote h4 { 188 | margin-top: 1rem; 189 | } 190 | 191 | hr { 192 | border-style: none none solid none; 193 | border-color: var(--line-color); 194 | } 195 | 196 | video, 197 | iframe { 198 | aspect-ratio: 16/9; 199 | display: block; 200 | margin: auto; 201 | } 202 | 203 | img, iframe, video { 204 | max-width: 100%; 205 | max-width: -webkit-fill-available; 206 | } 207 | 208 | figure { 209 | text-align: center; 210 | margin: 2rem 1rem; 211 | padding: 0.5rem 0.5rem 1.5rem; 212 | border: 1px solid var(--line-color); 213 | } 214 | 215 | figcaption { 216 | font-style: italic; 217 | font-size: 0.9rem; 218 | } 219 | 220 | table { 221 | border-collapse: collapse; 222 | border: 1px solid var(--line-color); 223 | margin: 1.5rem 1rem; 224 | width: -moz-available; 225 | width: -webkit-fill-available; 226 | width: fill-available; 227 | } 228 | 229 | table thead { 230 | background-color: var(--block-color); 231 | } 232 | 233 | table thead td { 234 | color: var(--table-head); 235 | } 236 | 237 | table tr:nth-child(2n) { 238 | background-color: var(--block-color); 239 | } 240 | 241 | table td { 242 | padding: 0.5rem 1rem; 243 | border: 1px solid var(--line-color); 244 | } 245 | 246 | details { 247 | background-color: var(--block-color); 248 | padding: 1rem; 249 | } 250 | 251 | details a { 252 | color: initial; 253 | } 254 | 255 | summary { 256 | cursor: pointer; 257 | } 258 | 259 | -------------------------------------------------------------------------------- /src/editor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |


7 | 8 | -------------------------------------------------------------------------------- /src/help/about-help.html: -------------------------------------------------------------------------------- 1 |
2 |

Hypertext HTML Document Editor

3 |

About

4 |

5 | Version 1.0 - Beta. October 2022. 6 |

7 |

8 | Created by Russell Beattie. 9 |

10 |

11 | Find the project code here: https://github.com/russellbeattie/hypertext-editor. It's open source, pull up a chair and feel free to try it out and submit bug reports. Pull requests will be happily welcomed with open arms as long as you're not an idiot. 12 |

13 |

14 | Also, there's zero tracking in the app itself. I haven't even turned on logging on the server. When I update the app, it should update your local copy when you open it... If the app seems to suddenly refresh on you, it's working as intended, though not ideally. 15 |

16 | 17 |

18 | Disclaimer: I wouldn't use this thing for anything important just yet, or you know, try to edit any HTML file you particularly care about. It tends to mangle markup that the app hasn't created itself. Don't say I didn't warn you. 19 |

20 |

21 | -Russ 22 |

23 |

Open Source Projects

24 |

Powered by the hard work of the folks at TinyMCE, plus a bunch of hours of my own work trying to figure out why the hell they wrapped all the standard DOM APIs and what I needed to do to make it play nice. Thanks guys!

25 |

Plus I slapped in CodeMirror for the code bits. It's still not working correctly, but looks pretty.

26 |

Thanks to whoever it is who made this: https://github.com/Alyw234237/md-wysiwyg-editor. It was super helpful in showing me how some of the wonky bits worked.

27 |

Also thanks to the Google dev relations guys for the file stuff here: https://googlechromelabs.github.io/text-editor/. 28 |

29 |

License

30 |
31 | The MIT License (MIT)
32 | 
33 | Copyright (c) 2022 Russell Beattie (http://www.russellbeattie.com) 34 |
35 | Permission is hereby granted, free of charge, to any person obtaining a copy 36 | of this software and associated documentation files (the "Software"), to deal 37 | in the Software without restriction, including without limitation the rights 38 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 39 | copies of the Software, and to permit persons to whom the Software is 40 | furnished to do so, subject to the following conditions: 41 |
42 | The above copyright notice and this permission notice shall be included in all 43 | copies or substantial portions of the Software. 44 |
45 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 46 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 47 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 48 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 49 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 50 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 51 | SOFTWARE. 52 |
53 |

54 |

55 |

56 |
57 | -------------------------------------------------------------------------------- /src/help/images-help.html: -------------------------------------------------------------------------------- 1 |

Images, Media and Embedding

2 |

Why are my images broken?

3 |

In order to view image files (and any other media) stored on your desktop, you will need to set a "Working Folder" in order to give the editor access to those files. To do so, select File → Set Working Folder from the menu bar, and select a folder where you plan to save your document (or where it is already saved). Any files in that folder, or any subfolder of it, will be viewable in the editor.

4 |

5 | 6 |

7 |

8 | After you've set your Working Folder, you can use the Insert → Images menu to add an image by either typing a relative URL by hand, such as "images/test.png", or choosing the icon to select it using a file dialog. Again, the image and media files must be in either the Working Folder, or subfolder of it in order to appear in the editor. 9 |

10 |

11 | The Working Folder you've chosen will continue to be active until you close the editor. If you're starting a new session, images will be broken until you give access to the editor again. 12 |

13 |

14 | The best way to work on a document with images is to simply create an "htmldocs" folder where your .html files will be saved, and then add any media files to a subfolder of that one such as "images" or "assets". Then you can easily add them into your document by browsing for the image using the Insert Image dialog box. The correct relative URL (such as "assets/picture.png") will be added automatically. 15 |

16 |

If you don't see "Set Working Folder" menu item, you are probably using a non-Chromium browser which doesn't support the newest File System Access APIs yet, so you'll need to use full URLs to your media starting with "http" or "https".

17 |

Embeds and video

18 |

19 | To embed an online video such as from YouTube, or social media post from Twitter, etc., copy the "embed" HTML code provided on their site, select Insert → Embed and then paste it into the input box provided.

20 |

21 | If you want to put a locally stored media file into your document, use the same Embed dialog to add the appropriate HTML tag such as <video src="media/mymovie.mp4"> or <audio src="media/podcast.mp3">, where the file is located in your Working Folder or a subfolder of it. 22 |

23 |

Local web server

24 |

You can also start a local web server on your desktop and serve the directory where your HTML document will be saved so that the relative URLs can be found. To use this server, open the Tools → Preferences dialog box and enter the local web server address (see below) into the "Local Server" input field. The document will then use that server as the base address for any relative URLs such as images or media.

25 |

The simplest way to run a local web server is to use Python. If you are on Windows, you will need to install it from https://www.python.org/downloads/. Mac users will have it pre-installed, and Linux users will figure it out, I'm sure.

26 |

Open the Command Prompt / Terminal app, and change to your documents directory (e.g. cd Documents). Then run the following: 

27 |
python3 -m http.server
28 |

Your local server will start serving your documents at http://localhost:8000/ 

29 |

If you have trouble, feel free to ask a techie friend. I'm sure they'll be happy to help. 😀

30 | -------------------------------------------------------------------------------- /src/help/shortcuts-help.html: -------------------------------------------------------------------------------- 1 |
2 |

Hypertext Document Shortcuts

3 |

Below are the keyboard shortcuts, in addition to standard document editing combos.

4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 |
Formatting
Bold⌘ / Ctrl + B
Italics⌘ / Ctrl + I
Strikethrough⌘ / Ctrl + D
Underline⌘ / Ctrl + U
Code⌘ / Ctrl + Shift + K
Remove format⌘ / Ctrl + Shift + F
Alignment
Justify center⌘ / Ctrl + Shift + E
Justify left⌘ / Ctrl + Shift + L
Justify none⌘ / Ctrl + Shift + J
Justify right⌘ / Ctrl + Shift + R
Indent⌘ / Ctrl + Shift + ]
Outdent⌘ / Ctrl + Shift + [
Blocks
Paragraph⌘ / Ctrl + 0
Heading 1⌘ / Ctrl + 1
Heading 2⌘ / Ctrl + 2
Heading 3⌘ / Ctrl + 3
Heading 4⌘ / Ctrl + 4
Blockquote⌘ / Ctrl + 5
Insert
Bullet list⌘ / Ctrl + Shift + U
Numbered list⌘ / Ctrl + Shift + O
Image⌘ / Ctrl + Shift + I
Insert Link⌘ / Ctrl + Shift + K
Insert Table⌘ / Ctrl + Shift + T
Editor
Save⌘ / Ctrl + S
Find⌘ / Ctrl + F
Print⌘ / Ctrl + P
Undo⌘ / Ctrl + Z
Redo⌘ / Ctrl + Shift + Z
Select All⌘ / Ctrl + A
Focus menubarAlt + F9
Focus toolbarAlt + F10
Code
Edit Head⌘ / Ctrl + Shift + 8
Edit Body⌘ / Ctrl + Shift + 9
Edit CSS⌘ / Ctrl + Shift + 0
168 |
-------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Hypertext 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | -------------------------------------------------------------------------------- /src/libs/codemirror-bundle.css: -------------------------------------------------------------------------------- 1 | /* BASICS */ 2 | 3 | .CodeMirror { 4 | /* Set height, width, borders, and global font properties here */ 5 | font-family: monospace; 6 | height: 300px; 7 | color: black; 8 | direction: ltr; 9 | } 10 | 11 | /* PADDING */ 12 | 13 | .CodeMirror-lines { 14 | padding: 16px 0; /* Vertical padding around content */ 15 | } 16 | .CodeMirror pre.CodeMirror-line, 17 | .CodeMirror pre.CodeMirror-line-like { 18 | padding: 0 16px; /* Horizontal padding of content */ 19 | } 20 | 21 | .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { 22 | background-color: white; /* The little square between H and V scrollbars */ 23 | } 24 | 25 | /* GUTTER */ 26 | 27 | .CodeMirror-gutters { 28 | border-right: 1px solid #ddd; 29 | background-color: #f7f7f7; 30 | white-space: nowrap; 31 | } 32 | .CodeMirror-linenumbers {} 33 | .CodeMirror-linenumber { 34 | padding: 0 3px 0 5px; 35 | min-width: 20px; 36 | text-align: right; 37 | color: #999; 38 | white-space: nowrap; 39 | } 40 | 41 | .CodeMirror-guttermarker { color: black; } 42 | .CodeMirror-guttermarker-subtle { color: #999; } 43 | 44 | /* CURSOR */ 45 | 46 | .CodeMirror-cursor { 47 | border-left: 1px solid black; 48 | border-right: none; 49 | width: 0; 50 | } 51 | /* Shown when moving in bi-directional text */ 52 | .CodeMirror div.CodeMirror-secondarycursor { 53 | border-left: 1px solid silver; 54 | } 55 | .cm-fat-cursor .CodeMirror-cursor { 56 | width: auto; 57 | border: 0 !important; 58 | background: #7e7; 59 | } 60 | .cm-fat-cursor div.CodeMirror-cursors { 61 | z-index: 1; 62 | } 63 | .cm-fat-cursor .CodeMirror-line::selection, 64 | .cm-fat-cursor .CodeMirror-line > span::selection, 65 | .cm-fat-cursor .CodeMirror-line > span > span::selection { background: transparent; } 66 | .cm-fat-cursor .CodeMirror-line::-moz-selection, 67 | .cm-fat-cursor .CodeMirror-line > span::-moz-selection, 68 | .cm-fat-cursor .CodeMirror-line > span > span::-moz-selection { background: transparent; } 69 | .cm-fat-cursor { caret-color: transparent; } 70 | @-moz-keyframes blink { 71 | 0% {} 72 | 50% { background-color: transparent; } 73 | 100% {} 74 | } 75 | @-webkit-keyframes blink { 76 | 0% {} 77 | 50% { background-color: transparent; } 78 | 100% {} 79 | } 80 | @keyframes blink { 81 | 0% {} 82 | 50% { background-color: transparent; } 83 | 100% {} 84 | } 85 | 86 | /* Can style cursor different in overwrite (non-insert) mode */ 87 | .CodeMirror-overwrite .CodeMirror-cursor {} 88 | 89 | .cm-tab { display: inline-block; text-decoration: inherit; } 90 | 91 | .CodeMirror-rulers { 92 | position: absolute; 93 | left: 0; right: 0; top: -50px; bottom: 0; 94 | overflow: hidden; 95 | } 96 | .CodeMirror-ruler { 97 | border-left: 1px solid #ccc; 98 | top: 0; bottom: 0; 99 | position: absolute; 100 | } 101 | 102 | /* DEFAULT THEME */ 103 | 104 | .cm-s-default .cm-header {color: blue;} 105 | .cm-s-default .cm-quote {color: #090;} 106 | .cm-negative {color: #d44;} 107 | .cm-positive {color: #292;} 108 | .cm-header, .cm-strong {font-weight: bold;} 109 | .cm-em {font-style: italic;} 110 | .cm-link {text-decoration: underline;} 111 | .cm-strikethrough {text-decoration: line-through;} 112 | 113 | .cm-s-default .cm-keyword {color: #708;} 114 | .cm-s-default .cm-atom {color: #219;} 115 | .cm-s-default .cm-number {color: #164;} 116 | .cm-s-default .cm-def {color: #00f;} 117 | .cm-s-default .cm-variable, 118 | .cm-s-default .cm-punctuation, 119 | .cm-s-default .cm-property, 120 | .cm-s-default .cm-operator {} 121 | .cm-s-default .cm-variable-2 {color: #05a;} 122 | .cm-s-default .cm-variable-3, .cm-s-default .cm-type {color: #085;} 123 | .cm-s-default .cm-comment {color: #a50;} 124 | .cm-s-default .cm-string {color: #a11;} 125 | .cm-s-default .cm-string-2 {color: #f50;} 126 | .cm-s-default .cm-meta {color: #555;} 127 | .cm-s-default .cm-qualifier {color: #555;} 128 | .cm-s-default .cm-builtin {color: #30a;} 129 | .cm-s-default .cm-bracket {color: #997;} 130 | .cm-s-default .cm-tag {color: #170;} 131 | .cm-s-default .cm-attribute {color: #00c;} 132 | .cm-s-default .cm-hr {color: #999;} 133 | .cm-s-default .cm-link {color: #00c;} 134 | 135 | .cm-s-default .cm-error {color: #f00;} 136 | .cm-invalidchar {color: #f00;} 137 | 138 | .CodeMirror-composing { border-bottom: 2px solid; } 139 | 140 | /* Default styles for common addons */ 141 | 142 | div.CodeMirror span.CodeMirror-matchingbracket {color: #0b0;} 143 | div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #a22;} 144 | .CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); } 145 | .CodeMirror-activeline-background {background: #e8f2ff;} 146 | 147 | /* STOP */ 148 | 149 | /* The rest of this file contains styles related to the mechanics of 150 | the editor. You probably shouldn't touch them. */ 151 | 152 | .CodeMirror { 153 | position: relative; 154 | overflow: hidden; 155 | background: white; 156 | } 157 | 158 | .CodeMirror-scroll { 159 | overflow: scroll !important; /* Things will break if this is overridden */ 160 | /* 50px is the magic margin used to hide the element's real scrollbars */ 161 | /* See overflow: hidden in .CodeMirror */ 162 | margin-bottom: -50px; margin-right: -50px; 163 | padding-bottom: 50px; 164 | height: 100%; 165 | outline: none; /* Prevent dragging from highlighting the element */ 166 | position: relative; 167 | z-index: 0; 168 | } 169 | .CodeMirror-sizer { 170 | position: relative; 171 | border-right: 50px solid transparent; 172 | } 173 | 174 | /* The fake, visible scrollbars. Used to force redraw during scrolling 175 | before actual scrolling happens, thus preventing shaking and 176 | flickering artifacts. */ 177 | .CodeMirror-vscrollbar, .CodeMirror-hscrollbar, .CodeMirror-scrollbar-filler, .CodeMirror-gutter-filler { 178 | position: absolute; 179 | z-index: 6; 180 | display: none; 181 | outline: none; 182 | } 183 | .CodeMirror-vscrollbar { 184 | right: 0; top: 0; 185 | overflow-x: hidden; 186 | overflow-y: scroll; 187 | } 188 | .CodeMirror-hscrollbar { 189 | bottom: 0; left: 0; 190 | overflow-y: hidden; 191 | overflow-x: scroll; 192 | } 193 | .CodeMirror-scrollbar-filler { 194 | right: 0; bottom: 0; 195 | } 196 | .CodeMirror-gutter-filler { 197 | left: 0; bottom: 0; 198 | } 199 | 200 | .CodeMirror-gutters { 201 | position: absolute; left: 0; top: 0; 202 | min-height: 100%; 203 | z-index: 3; 204 | } 205 | .CodeMirror-gutter { 206 | white-space: normal; 207 | height: 100%; 208 | display: inline-block; 209 | vertical-align: top; 210 | margin-bottom: -50px; 211 | } 212 | .CodeMirror-gutter-wrapper { 213 | position: absolute; 214 | z-index: 4; 215 | background: none !important; 216 | border: none !important; 217 | } 218 | .CodeMirror-gutter-background { 219 | position: absolute; 220 | top: 0; bottom: 0; 221 | z-index: 4; 222 | } 223 | .CodeMirror-gutter-elt { 224 | position: absolute; 225 | cursor: default; 226 | z-index: 4; 227 | } 228 | .CodeMirror-gutter-wrapper ::selection { background-color: transparent } 229 | .CodeMirror-gutter-wrapper ::-moz-selection { background-color: transparent } 230 | 231 | .CodeMirror-lines { 232 | cursor: text; 233 | min-height: 1px; /* prevents collapsing before first draw */ 234 | } 235 | .CodeMirror pre.CodeMirror-line, 236 | .CodeMirror pre.CodeMirror-line-like { 237 | /* Reset some styles that the rest of the page might have set */ 238 | -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; 239 | border-width: 0; 240 | background: transparent; 241 | font-family: inherit; 242 | font-size: inherit; 243 | margin: 0; 244 | white-space: pre; 245 | word-wrap: normal; 246 | line-height: inherit; 247 | color: inherit; 248 | z-index: 2; 249 | position: relative; 250 | overflow: visible; 251 | -webkit-tap-highlight-color: transparent; 252 | -webkit-font-variant-ligatures: contextual; 253 | font-variant-ligatures: contextual; 254 | } 255 | .CodeMirror-wrap pre.CodeMirror-line, 256 | .CodeMirror-wrap pre.CodeMirror-line-like { 257 | word-wrap: break-word; 258 | white-space: pre-wrap; 259 | word-break: normal; 260 | } 261 | 262 | .CodeMirror-linebackground { 263 | position: absolute; 264 | left: 0; right: 0; top: 0; bottom: 0; 265 | z-index: 0; 266 | } 267 | 268 | .CodeMirror-linewidget { 269 | position: relative; 270 | z-index: 2; 271 | padding: 0.1px; /* Force widget margins to stay inside of the container */ 272 | } 273 | 274 | .CodeMirror-widget {} 275 | 276 | .CodeMirror-rtl pre { direction: rtl; } 277 | 278 | .CodeMirror-code { 279 | outline: none; 280 | } 281 | 282 | /* Force content-box sizing for the elements where we expect it */ 283 | .CodeMirror-scroll, 284 | .CodeMirror-sizer, 285 | .CodeMirror-gutter, 286 | .CodeMirror-gutters, 287 | .CodeMirror-linenumber { 288 | -moz-box-sizing: content-box; 289 | box-sizing: content-box; 290 | } 291 | 292 | .CodeMirror-measure { 293 | position: absolute; 294 | width: 100%; 295 | height: 0; 296 | overflow: hidden; 297 | visibility: hidden; 298 | } 299 | 300 | .CodeMirror-cursor { 301 | position: absolute; 302 | pointer-events: none; 303 | } 304 | .CodeMirror-measure pre { position: static; } 305 | 306 | div.CodeMirror-cursors { 307 | visibility: hidden; 308 | position: relative; 309 | z-index: 3; 310 | } 311 | div.CodeMirror-dragcursors { 312 | visibility: visible; 313 | } 314 | 315 | .CodeMirror-focused div.CodeMirror-cursors { 316 | visibility: visible; 317 | } 318 | 319 | .CodeMirror-selected { background: #d9d9d9; } 320 | .CodeMirror-focused .CodeMirror-selected { background: #d7d4f0; } 321 | .CodeMirror-crosshair { cursor: crosshair; } 322 | .CodeMirror-line::selection, .CodeMirror-line > span::selection, .CodeMirror-line > span > span::selection { background: #d7d4f0; } 323 | .CodeMirror-line::-moz-selection, .CodeMirror-line > span::-moz-selection, .CodeMirror-line > span > span::-moz-selection { background: #d7d4f0; } 324 | 325 | .cm-searching { 326 | background-color: #ffa; 327 | background-color: rgba(255, 255, 0, .4); 328 | } 329 | 330 | /* Used to force a border model for a node */ 331 | .cm-force-border { padding-right: .1px; } 332 | 333 | @media print { 334 | /* Hide the cursor when printing */ 335 | .CodeMirror div.CodeMirror-cursors { 336 | visibility: hidden; 337 | } 338 | } 339 | 340 | /* See issue #2901 */ 341 | .cm-tab-wrap-hack:after { content: ''; } 342 | 343 | /* Help users use markselection to safely style text background */ 344 | span.CodeMirror-selectedtext { background: none; } 345 | 346 | 347 | 348 | .CodeMirror-hints { 349 | position: absolute; 350 | z-index: 10; 351 | overflow: hidden; 352 | list-style: none; 353 | 354 | margin: 0; 355 | padding: 2px; 356 | 357 | -webkit-box-shadow: 2px 3px 5px rgba(0,0,0,.2); 358 | -moz-box-shadow: 2px 3px 5px rgba(0,0,0,.2); 359 | box-shadow: 2px 3px 5px rgba(0,0,0,.2); 360 | border-radius: 3px; 361 | border: 1px solid silver; 362 | 363 | background: white; 364 | font-size: 90%; 365 | font-family: monospace; 366 | 367 | max-height: 20em; 368 | overflow-y: auto; 369 | box-sizing: border-box; 370 | } 371 | 372 | .CodeMirror-hint { 373 | margin: 0; 374 | padding: 0 4px; 375 | border-radius: 2px; 376 | white-space: pre; 377 | color: black; 378 | cursor: pointer; 379 | } 380 | 381 | li.CodeMirror-hint-active { 382 | background: #08f; 383 | color: white; 384 | } 385 | -------------------------------------------------------------------------------- /src/libs/idb-keyval.js: -------------------------------------------------------------------------------- 1 | function promisifyRequest(request) { 2 | return new Promise((resolve, reject) => { 3 | // @ts-ignore - file size hacks 4 | request.oncomplete = request.onsuccess = () => resolve(request.result); 5 | // @ts-ignore - file size hacks 6 | request.onabort = request.onerror = () => reject(request.error); 7 | }); 8 | } 9 | function createStore(dbName, storeName) { 10 | const request = indexedDB.open(dbName); 11 | request.onupgradeneeded = () => request.result.createObjectStore(storeName); 12 | const dbp = promisifyRequest(request); 13 | return (txMode, callback) => dbp.then((db) => callback(db.transaction(storeName, txMode).objectStore(storeName))); 14 | } 15 | let defaultGetStoreFunc; 16 | function defaultGetStore() { 17 | if (!defaultGetStoreFunc) { 18 | defaultGetStoreFunc = createStore('keyval-store', 'keyval'); 19 | } 20 | return defaultGetStoreFunc; 21 | } 22 | /** 23 | * Get a value by its key. 24 | * 25 | * @param key 26 | * @param customStore Method to get a custom store. Use with caution (see the docs). 27 | */ 28 | function get(key, customStore = defaultGetStore()) { 29 | return customStore('readonly', (store) => promisifyRequest(store.get(key))); 30 | } 31 | /** 32 | * Set a value with a key. 33 | * 34 | * @param key 35 | * @param value 36 | * @param customStore Method to get a custom store. Use with caution (see the docs). 37 | */ 38 | function set(key, value, customStore = defaultGetStore()) { 39 | return customStore('readwrite', (store) => { 40 | store.put(value, key); 41 | return promisifyRequest(store.transaction); 42 | }); 43 | } 44 | /** 45 | * Set multiple values at once. This is faster than calling set() multiple times. 46 | * It's also atomic – if one of the pairs can't be added, none will be added. 47 | * 48 | * @param entries Array of entries, where each entry is an array of `[key, value]`. 49 | * @param customStore Method to get a custom store. Use with caution (see the docs). 50 | */ 51 | function setMany(entries, customStore = defaultGetStore()) { 52 | return customStore('readwrite', (store) => { 53 | entries.forEach((entry) => store.put(entry[1], entry[0])); 54 | return promisifyRequest(store.transaction); 55 | }); 56 | } 57 | /** 58 | * Get multiple values by their keys 59 | * 60 | * @param keys 61 | * @param customStore Method to get a custom store. Use with caution (see the docs). 62 | */ 63 | function getMany(keys, customStore = defaultGetStore()) { 64 | return customStore('readonly', (store) => Promise.all(keys.map((key) => promisifyRequest(store.get(key))))); 65 | } 66 | /** 67 | * Update a value. This lets you see the old value and update it as an atomic operation. 68 | * 69 | * @param key 70 | * @param updater A callback that takes the old value and returns a new value. 71 | * @param customStore Method to get a custom store. Use with caution (see the docs). 72 | */ 73 | function update(key, updater, customStore = defaultGetStore()) { 74 | return customStore('readwrite', (store) => 75 | // Need to create the promise manually. 76 | // If I try to chain promises, the transaction closes in browsers 77 | // that use a promise polyfill (IE10/11). 78 | new Promise((resolve, reject) => { 79 | store.get(key).onsuccess = function () { 80 | try { 81 | store.put(updater(this.result), key); 82 | resolve(promisifyRequest(store.transaction)); 83 | } 84 | catch (err) { 85 | reject(err); 86 | } 87 | }; 88 | })); 89 | } 90 | /** 91 | * Delete a particular key from the store. 92 | * 93 | * @param key 94 | * @param customStore Method to get a custom store. Use with caution (see the docs). 95 | */ 96 | function del(key, customStore = defaultGetStore()) { 97 | return customStore('readwrite', (store) => { 98 | store.delete(key); 99 | return promisifyRequest(store.transaction); 100 | }); 101 | } 102 | /** 103 | * Delete multiple keys at once. 104 | * 105 | * @param keys List of keys to delete. 106 | * @param customStore Method to get a custom store. Use with caution (see the docs). 107 | */ 108 | function delMany(keys, customStore = defaultGetStore()) { 109 | return customStore('readwrite', (store) => { 110 | keys.forEach((key) => store.delete(key)); 111 | return promisifyRequest(store.transaction); 112 | }); 113 | } 114 | /** 115 | * Clear all values in the store. 116 | * 117 | * @param customStore Method to get a custom store. Use with caution (see the docs). 118 | */ 119 | function clear(customStore = defaultGetStore()) { 120 | return customStore('readwrite', (store) => { 121 | store.clear(); 122 | return promisifyRequest(store.transaction); 123 | }); 124 | } 125 | function eachCursor(store, callback) { 126 | store.openCursor().onsuccess = function () { 127 | if (!this.result) 128 | return; 129 | callback(this.result); 130 | this.result.continue(); 131 | }; 132 | return promisifyRequest(store.transaction); 133 | } 134 | /** 135 | * Get all keys in the store. 136 | * 137 | * @param customStore Method to get a custom store. Use with caution (see the docs). 138 | */ 139 | function keys(customStore = defaultGetStore()) { 140 | return customStore('readonly', (store) => { 141 | // Fast path for modern browsers 142 | if (store.getAllKeys) { 143 | return promisifyRequest(store.getAllKeys()); 144 | } 145 | const items = []; 146 | return eachCursor(store, (cursor) => items.push(cursor.key)).then(() => items); 147 | }); 148 | } 149 | /** 150 | * Get all values in the store. 151 | * 152 | * @param customStore Method to get a custom store. Use with caution (see the docs). 153 | */ 154 | function values(customStore = defaultGetStore()) { 155 | return customStore('readonly', (store) => { 156 | // Fast path for modern browsers 157 | if (store.getAll) { 158 | return promisifyRequest(store.getAll()); 159 | } 160 | const items = []; 161 | return eachCursor(store, (cursor) => items.push(cursor.value)).then(() => items); 162 | }); 163 | } 164 | /** 165 | * Get all entries in the store. Each entry is an array of `[key, value]`. 166 | * 167 | * @param customStore Method to get a custom store. Use with caution (see the docs). 168 | */ 169 | function entries(customStore = defaultGetStore()) { 170 | return customStore('readonly', (store) => { 171 | // Fast path for modern browsers 172 | // (although, hopefully we'll get a simpler path some day) 173 | if (store.getAll && store.getAllKeys) { 174 | return Promise.all([ 175 | promisifyRequest(store.getAllKeys()), 176 | promisifyRequest(store.getAll()), 177 | ]).then(([keys, values]) => keys.map((key, i) => [key, values[i]])); 178 | } 179 | const items = []; 180 | return customStore('readonly', (store) => eachCursor(store, (cursor) => items.push([cursor.key, cursor.value])).then(() => items)); 181 | }); 182 | } 183 | 184 | -------------------------------------------------------------------------------- /src/libs/tinymce/plugins/anchor/plugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TinyMCE version 6.2.0 (2022-09-08) 3 | */ 4 | 5 | (function () { 6 | 'use strict'; 7 | 8 | var global$2 = tinymce.util.Tools.resolve('tinymce.PluginManager'); 9 | 10 | var global$1 = tinymce.util.Tools.resolve('tinymce.dom.RangeUtils'); 11 | 12 | var global = tinymce.util.Tools.resolve('tinymce.util.Tools'); 13 | 14 | const option = name => editor => editor.options.get(name); 15 | const register$2 = editor => { 16 | const registerOption = editor.options.register; 17 | registerOption('allow_html_in_named_anchor', { 18 | processor: 'boolean', 19 | default: false 20 | }); 21 | }; 22 | const allowHtmlInNamedAnchor = option('allow_html_in_named_anchor'); 23 | 24 | const namedAnchorSelector = 'a:not([href])'; 25 | const isEmptyString = str => !str; 26 | const getIdFromAnchor = elm => { 27 | const id = elm.getAttribute('id') || elm.getAttribute('name'); 28 | return id || ''; 29 | }; 30 | const isAnchor = elm => elm.nodeName.toLowerCase() === 'a'; 31 | const isNamedAnchor = elm => isAnchor(elm) && !elm.getAttribute('href') && getIdFromAnchor(elm) !== ''; 32 | const isEmptyNamedAnchor = elm => isNamedAnchor(elm) && !elm.firstChild; 33 | 34 | const removeEmptyNamedAnchorsInSelection = editor => { 35 | const dom = editor.dom; 36 | global$1(dom).walk(editor.selection.getRng(), nodes => { 37 | global.each(nodes, node => { 38 | if (isEmptyNamedAnchor(node)) { 39 | dom.remove(node, false); 40 | } 41 | }); 42 | }); 43 | }; 44 | const isValidId = id => /^[A-Za-z][A-Za-z0-9\-:._]*$/.test(id); 45 | const getNamedAnchor = editor => editor.dom.getParent(editor.selection.getStart(), namedAnchorSelector); 46 | const getId = editor => { 47 | const anchor = getNamedAnchor(editor); 48 | if (anchor) { 49 | return getIdFromAnchor(anchor); 50 | } else { 51 | return ''; 52 | } 53 | }; 54 | const createAnchor = (editor, id) => { 55 | editor.undoManager.transact(() => { 56 | if (!allowHtmlInNamedAnchor(editor)) { 57 | editor.selection.collapse(true); 58 | } 59 | if (editor.selection.isCollapsed()) { 60 | editor.insertContent(editor.dom.createHTML('a', { id })); 61 | } else { 62 | removeEmptyNamedAnchorsInSelection(editor); 63 | editor.formatter.remove('namedAnchor', undefined, undefined, true); 64 | editor.formatter.apply('namedAnchor', { value: id }); 65 | editor.addVisual(); 66 | } 67 | }); 68 | }; 69 | const updateAnchor = (editor, id, anchorElement) => { 70 | anchorElement.removeAttribute('name'); 71 | anchorElement.id = id; 72 | editor.addVisual(); 73 | editor.undoManager.add(); 74 | }; 75 | const insert = (editor, id) => { 76 | const anchor = getNamedAnchor(editor); 77 | if (anchor) { 78 | updateAnchor(editor, id, anchor); 79 | } else { 80 | createAnchor(editor, id); 81 | } 82 | editor.focus(); 83 | }; 84 | 85 | const insertAnchor = (editor, newId) => { 86 | if (!isValidId(newId)) { 87 | editor.windowManager.alert('ID should start with a letter, followed only by letters, numbers, dashes, dots, colons or underscores.'); 88 | return false; 89 | } else { 90 | insert(editor, newId); 91 | return true; 92 | } 93 | }; 94 | const open = editor => { 95 | const currentId = getId(editor); 96 | editor.windowManager.open({ 97 | title: 'Anchor', 98 | size: 'normal', 99 | body: { 100 | type: 'panel', 101 | items: [{ 102 | name: 'id', 103 | type: 'input', 104 | label: 'ID', 105 | placeholder: 'example' 106 | }] 107 | }, 108 | buttons: [ 109 | { 110 | type: 'cancel', 111 | name: 'cancel', 112 | text: 'Cancel' 113 | }, 114 | { 115 | type: 'submit', 116 | name: 'save', 117 | text: 'Save', 118 | primary: true 119 | } 120 | ], 121 | initialData: { id: currentId }, 122 | onSubmit: api => { 123 | if (insertAnchor(editor, api.getData().id)) { 124 | api.close(); 125 | } 126 | } 127 | }); 128 | }; 129 | 130 | const register$1 = editor => { 131 | editor.addCommand('mceAnchor', () => { 132 | open(editor); 133 | }); 134 | }; 135 | 136 | const isNamedAnchorNode = node => isEmptyString(node.attr('href')) && !isEmptyString(node.attr('id') || node.attr('name')); 137 | const isEmptyNamedAnchorNode = node => isNamedAnchorNode(node) && !node.firstChild; 138 | const setContentEditable = state => nodes => { 139 | for (let i = 0; i < nodes.length; i++) { 140 | const node = nodes[i]; 141 | if (isEmptyNamedAnchorNode(node)) { 142 | node.attr('contenteditable', state); 143 | } 144 | } 145 | }; 146 | const setup = editor => { 147 | editor.on('PreInit', () => { 148 | editor.parser.addNodeFilter('a', setContentEditable('false')); 149 | editor.serializer.addNodeFilter('a', setContentEditable(null)); 150 | }); 151 | }; 152 | 153 | const registerFormats = editor => { 154 | editor.formatter.register('namedAnchor', { 155 | inline: 'a', 156 | selector: namedAnchorSelector, 157 | remove: 'all', 158 | split: true, 159 | deep: true, 160 | attributes: { id: '%value' }, 161 | onmatch: (node, _fmt, _itemName) => { 162 | return isNamedAnchor(node); 163 | } 164 | }); 165 | }; 166 | 167 | const register = editor => { 168 | const onAction = () => editor.execCommand('mceAnchor'); 169 | editor.ui.registry.addToggleButton('anchor', { 170 | icon: 'bookmark', 171 | tooltip: 'Anchor', 172 | onAction, 173 | onSetup: buttonApi => editor.selection.selectorChangedWithUnbind('a:not([href])', buttonApi.setActive).unbind 174 | }); 175 | editor.ui.registry.addMenuItem('anchor', { 176 | icon: 'bookmark', 177 | text: 'Anchor...', 178 | onAction 179 | }); 180 | }; 181 | 182 | var Plugin = () => { 183 | global$2.add('anchor', editor => { 184 | register$2(editor); 185 | setup(editor); 186 | register$1(editor); 187 | register(editor); 188 | editor.on('PreInit', () => { 189 | registerFormats(editor); 190 | }); 191 | }); 192 | }; 193 | 194 | Plugin(); 195 | 196 | })(); 197 | -------------------------------------------------------------------------------- /src/libs/tinymce/plugins/autolink/plugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TinyMCE version 6.2.0 (2022-09-08) 3 | */ 4 | 5 | (function () { 6 | 'use strict'; 7 | 8 | var global$1 = tinymce.util.Tools.resolve('tinymce.PluginManager'); 9 | 10 | const link = () => /(?:[A-Za-z][A-Za-z\d.+-]{0,14}:\/\/(?:[-.~*+=!&;:'%@?^${}(),\w]+@)?|www\.|[-;:&=+$,.\w]+@)[A-Za-z\d-]+(?:\.[A-Za-z\d-]+)*(?::\d+)?(?:\/(?:[-.~*+=!;:'%@$(),\/\w]*[-~*+=%@$()\/\w])?)?(?:\?(?:[-.~*+=!&;:'%@?^${}(),\/\w]+))?(?:#(?:[-.~*+=!&;:'%@?^${}(),\/\w]+))?/g; 11 | 12 | const option = name => editor => editor.options.get(name); 13 | const register = editor => { 14 | const registerOption = editor.options.register; 15 | registerOption('autolink_pattern', { 16 | processor: 'regexp', 17 | default: new RegExp('^' + link().source + '$', 'i') 18 | }); 19 | registerOption('link_default_target', { processor: 'string' }); 20 | registerOption('link_default_protocol', { 21 | processor: 'string', 22 | default: 'https' 23 | }); 24 | }; 25 | const getAutoLinkPattern = option('autolink_pattern'); 26 | const getDefaultLinkTarget = option('link_default_target'); 27 | const getDefaultLinkProtocol = option('link_default_protocol'); 28 | const allowUnsafeLinkTarget = option('allow_unsafe_link_target'); 29 | 30 | const hasProto = (v, constructor, predicate) => { 31 | var _a; 32 | if (predicate(v, constructor.prototype)) { 33 | return true; 34 | } else { 35 | return ((_a = v.constructor) === null || _a === void 0 ? void 0 : _a.name) === constructor.name; 36 | } 37 | }; 38 | const typeOf = x => { 39 | const t = typeof x; 40 | if (x === null) { 41 | return 'null'; 42 | } else if (t === 'object' && Array.isArray(x)) { 43 | return 'array'; 44 | } else if (t === 'object' && hasProto(x, String, (o, proto) => proto.isPrototypeOf(o))) { 45 | return 'string'; 46 | } else { 47 | return t; 48 | } 49 | }; 50 | const isType = type => value => typeOf(value) === type; 51 | const eq = t => a => t === a; 52 | const isString = isType('string'); 53 | const isUndefined = eq(undefined); 54 | const isNullable = a => a === null || a === undefined; 55 | const isNonNullable = a => !isNullable(a); 56 | 57 | const not = f => t => !f(t); 58 | 59 | const hasOwnProperty = Object.hasOwnProperty; 60 | const has = (obj, key) => hasOwnProperty.call(obj, key); 61 | 62 | const checkRange = (str, substr, start) => substr === '' || str.length >= substr.length && str.substr(start, start + substr.length) === substr; 63 | const contains = (str, substr, start = 0, end) => { 64 | const idx = str.indexOf(substr, start); 65 | if (idx !== -1) { 66 | return isUndefined(end) ? true : idx + substr.length <= end; 67 | } else { 68 | return false; 69 | } 70 | }; 71 | const startsWith = (str, prefix) => { 72 | return checkRange(str, prefix, 0); 73 | }; 74 | 75 | const zeroWidth = '\uFEFF'; 76 | const isZwsp = char => char === zeroWidth; 77 | const removeZwsp = s => s.replace(/\uFEFF/g, ''); 78 | 79 | var global = tinymce.util.Tools.resolve('tinymce.dom.TextSeeker'); 80 | 81 | const isTextNode = node => node.nodeType === 3; 82 | const isElement = node => node.nodeType === 1; 83 | const isBracketOrSpace = char => /^[(\[{ \u00a0]$/.test(char); 84 | const hasProtocol = url => /^([A-Za-z][A-Za-z\d.+-]*:\/\/)|mailto:/.test(url); 85 | const isPunctuation = char => /[?!,.;:]/.test(char); 86 | const findChar = (text, index, predicate) => { 87 | for (let i = index - 1; i >= 0; i--) { 88 | const char = text.charAt(i); 89 | if (!isZwsp(char) && predicate(char)) { 90 | return i; 91 | } 92 | } 93 | return -1; 94 | }; 95 | const freefallRtl = (container, offset) => { 96 | let tempNode = container; 97 | let tempOffset = offset; 98 | while (isElement(tempNode) && tempNode.childNodes[tempOffset]) { 99 | tempNode = tempNode.childNodes[tempOffset]; 100 | tempOffset = isTextNode(tempNode) ? tempNode.data.length : tempNode.childNodes.length; 101 | } 102 | return { 103 | container: tempNode, 104 | offset: tempOffset 105 | }; 106 | }; 107 | 108 | const parseCurrentLine = (editor, offset) => { 109 | var _a; 110 | const voidElements = editor.schema.getVoidElements(); 111 | const autoLinkPattern = getAutoLinkPattern(editor); 112 | const {dom, selection} = editor; 113 | if (dom.getParent(selection.getNode(), 'a[href]') !== null) { 114 | return null; 115 | } 116 | const rng = selection.getRng(); 117 | const textSeeker = global(dom, node => { 118 | return dom.isBlock(node) || has(voidElements, node.nodeName.toLowerCase()) || dom.getContentEditable(node) === 'false'; 119 | }); 120 | const { 121 | container: endContainer, 122 | offset: endOffset 123 | } = freefallRtl(rng.endContainer, rng.endOffset); 124 | const root = (_a = dom.getParent(endContainer, dom.isBlock)) !== null && _a !== void 0 ? _a : dom.getRoot(); 125 | const endSpot = textSeeker.backwards(endContainer, endOffset + offset, (node, offset) => { 126 | const text = node.data; 127 | const idx = findChar(text, offset, not(isBracketOrSpace)); 128 | return idx === -1 || isPunctuation(text[idx]) ? idx : idx + 1; 129 | }, root); 130 | if (!endSpot) { 131 | return null; 132 | } 133 | let lastTextNode = endSpot.container; 134 | const startSpot = textSeeker.backwards(endSpot.container, endSpot.offset, (node, offset) => { 135 | lastTextNode = node; 136 | const idx = findChar(node.data, offset, isBracketOrSpace); 137 | return idx === -1 ? idx : idx + 1; 138 | }, root); 139 | const newRng = dom.createRng(); 140 | if (!startSpot) { 141 | newRng.setStart(lastTextNode, 0); 142 | } else { 143 | newRng.setStart(startSpot.container, startSpot.offset); 144 | } 145 | newRng.setEnd(endSpot.container, endSpot.offset); 146 | const rngText = removeZwsp(newRng.toString()); 147 | const matches = rngText.match(autoLinkPattern); 148 | if (matches) { 149 | let url = matches[0]; 150 | if (startsWith(url, 'www.')) { 151 | const protocol = getDefaultLinkProtocol(editor); 152 | url = protocol + '://' + url; 153 | } else if (contains(url, '@') && !hasProtocol(url)) { 154 | url = 'mailto:' + url; 155 | } 156 | return { 157 | rng: newRng, 158 | url 159 | }; 160 | } else { 161 | return null; 162 | } 163 | }; 164 | const convertToLink = (editor, result) => { 165 | const {dom, selection} = editor; 166 | const {rng, url} = result; 167 | const bookmark = selection.getBookmark(); 168 | selection.setRng(rng); 169 | const command = 'createlink'; 170 | const args = { 171 | command, 172 | ui: false, 173 | value: url 174 | }; 175 | const beforeExecEvent = editor.dispatch('BeforeExecCommand', args); 176 | if (!beforeExecEvent.isDefaultPrevented()) { 177 | editor.getDoc().execCommand(command, false, url); 178 | editor.dispatch('ExecCommand', args); 179 | const defaultLinkTarget = getDefaultLinkTarget(editor); 180 | if (isString(defaultLinkTarget)) { 181 | const anchor = selection.getNode(); 182 | dom.setAttrib(anchor, 'target', defaultLinkTarget); 183 | if (defaultLinkTarget === '_blank' && !allowUnsafeLinkTarget(editor)) { 184 | dom.setAttrib(anchor, 'rel', 'noopener'); 185 | } 186 | } 187 | } 188 | selection.moveToBookmark(bookmark); 189 | editor.nodeChanged(); 190 | }; 191 | const handleSpacebar = editor => { 192 | const result = parseCurrentLine(editor, -1); 193 | if (isNonNullable(result)) { 194 | convertToLink(editor, result); 195 | } 196 | }; 197 | const handleBracket = handleSpacebar; 198 | const handleEnter = editor => { 199 | const result = parseCurrentLine(editor, 0); 200 | if (isNonNullable(result)) { 201 | convertToLink(editor, result); 202 | } 203 | }; 204 | const setup = editor => { 205 | editor.on('keydown', e => { 206 | if (e.keyCode === 13 && !e.isDefaultPrevented()) { 207 | handleEnter(editor); 208 | } 209 | }); 210 | editor.on('keyup', e => { 211 | if (e.keyCode === 32) { 212 | handleSpacebar(editor); 213 | } else if (e.keyCode === 48 && e.shiftKey || e.keyCode === 221) { 214 | handleBracket(editor); 215 | } 216 | }); 217 | }; 218 | 219 | var Plugin = () => { 220 | global$1.add('autolink', editor => { 221 | register(editor); 222 | setup(editor); 223 | }); 224 | }; 225 | 226 | Plugin(); 227 | 228 | })(); 229 | -------------------------------------------------------------------------------- /src/libs/tinymce/plugins/autosave/plugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TinyMCE version 6.2.0 (2022-09-08) 3 | */ 4 | 5 | (function () { 6 | 'use strict'; 7 | 8 | var global$4 = tinymce.util.Tools.resolve('tinymce.PluginManager'); 9 | 10 | const hasProto = (v, constructor, predicate) => { 11 | var _a; 12 | if (predicate(v, constructor.prototype)) { 13 | return true; 14 | } else { 15 | return ((_a = v.constructor) === null || _a === void 0 ? void 0 : _a.name) === constructor.name; 16 | } 17 | }; 18 | const typeOf = x => { 19 | const t = typeof x; 20 | if (x === null) { 21 | return 'null'; 22 | } else if (t === 'object' && Array.isArray(x)) { 23 | return 'array'; 24 | } else if (t === 'object' && hasProto(x, String, (o, proto) => proto.isPrototypeOf(o))) { 25 | return 'string'; 26 | } else { 27 | return t; 28 | } 29 | }; 30 | const isType = type => value => typeOf(value) === type; 31 | const eq = t => a => t === a; 32 | const isString = isType('string'); 33 | const isUndefined = eq(undefined); 34 | 35 | var global$3 = tinymce.util.Tools.resolve('tinymce.util.Delay'); 36 | 37 | var global$2 = tinymce.util.Tools.resolve('tinymce.util.LocalStorage'); 38 | 39 | var global$1 = tinymce.util.Tools.resolve('tinymce.util.Tools'); 40 | 41 | const fireRestoreDraft = editor => editor.dispatch('RestoreDraft'); 42 | const fireStoreDraft = editor => editor.dispatch('StoreDraft'); 43 | const fireRemoveDraft = editor => editor.dispatch('RemoveDraft'); 44 | 45 | const parse = timeString => { 46 | const multiples = { 47 | s: 1000, 48 | m: 60000 49 | }; 50 | const parsedTime = /^(\d+)([ms]?)$/.exec(timeString); 51 | return (parsedTime && parsedTime[2] ? multiples[parsedTime[2]] : 1) * parseInt(timeString, 10); 52 | }; 53 | 54 | const option = name => editor => editor.options.get(name); 55 | const register$1 = editor => { 56 | const registerOption = editor.options.register; 57 | const timeProcessor = value => { 58 | const valid = isString(value); 59 | if (valid) { 60 | return { 61 | value: parse(value), 62 | valid 63 | }; 64 | } else { 65 | return { 66 | valid: false, 67 | message: 'Must be a string.' 68 | }; 69 | } 70 | }; 71 | registerOption('autosave_ask_before_unload', { 72 | processor: 'boolean', 73 | default: true 74 | }); 75 | registerOption('autosave_prefix', { 76 | processor: 'string', 77 | default: 'tinymce-autosave-{path}{query}{hash}-{id}-' 78 | }); 79 | registerOption('autosave_restore_when_empty', { 80 | processor: 'boolean', 81 | default: false 82 | }); 83 | registerOption('autosave_interval', { 84 | processor: timeProcessor, 85 | default: '30s' 86 | }); 87 | registerOption('autosave_retention', { 88 | processor: timeProcessor, 89 | default: '20m' 90 | }); 91 | }; 92 | const shouldAskBeforeUnload = option('autosave_ask_before_unload'); 93 | const shouldRestoreWhenEmpty = option('autosave_restore_when_empty'); 94 | const getAutoSaveInterval = option('autosave_interval'); 95 | const getAutoSaveRetention = option('autosave_retention'); 96 | const getAutoSavePrefix = editor => { 97 | const location = document.location; 98 | return editor.options.get('autosave_prefix').replace(/{path}/g, location.pathname).replace(/{query}/g, location.search).replace(/{hash}/g, location.hash).replace(/{id}/g, editor.id); 99 | }; 100 | 101 | const isEmpty = (editor, html) => { 102 | if (isUndefined(html)) { 103 | return editor.dom.isEmpty(editor.getBody()); 104 | } else { 105 | const trimmedHtml = global$1.trim(html); 106 | if (trimmedHtml === '') { 107 | return true; 108 | } else { 109 | const fragment = new DOMParser().parseFromString(trimmedHtml, 'text/html'); 110 | return editor.dom.isEmpty(fragment); 111 | } 112 | } 113 | }; 114 | const hasDraft = editor => { 115 | var _a; 116 | const time = parseInt((_a = global$2.getItem(getAutoSavePrefix(editor) + 'time')) !== null && _a !== void 0 ? _a : '0', 10) || 0; 117 | if (new Date().getTime() - time > getAutoSaveRetention(editor)) { 118 | removeDraft(editor, false); 119 | return false; 120 | } 121 | return true; 122 | }; 123 | const removeDraft = (editor, fire) => { 124 | const prefix = getAutoSavePrefix(editor); 125 | global$2.removeItem(prefix + 'draft'); 126 | global$2.removeItem(prefix + 'time'); 127 | if (fire !== false) { 128 | fireRemoveDraft(editor); 129 | } 130 | }; 131 | const storeDraft = editor => { 132 | const prefix = getAutoSavePrefix(editor); 133 | if (!isEmpty(editor) && editor.isDirty()) { 134 | global$2.setItem(prefix + 'draft', editor.getContent({ 135 | format: 'raw', 136 | no_events: true 137 | })); 138 | global$2.setItem(prefix + 'time', new Date().getTime().toString()); 139 | fireStoreDraft(editor); 140 | } 141 | }; 142 | const restoreDraft = editor => { 143 | var _a; 144 | const prefix = getAutoSavePrefix(editor); 145 | if (hasDraft(editor)) { 146 | editor.setContent((_a = global$2.getItem(prefix + 'draft')) !== null && _a !== void 0 ? _a : '', { format: 'raw' }); 147 | fireRestoreDraft(editor); 148 | } 149 | }; 150 | const startStoreDraft = editor => { 151 | const interval = getAutoSaveInterval(editor); 152 | global$3.setEditorInterval(editor, () => { 153 | storeDraft(editor); 154 | }, interval); 155 | }; 156 | const restoreLastDraft = editor => { 157 | editor.undoManager.transact(() => { 158 | restoreDraft(editor); 159 | removeDraft(editor); 160 | }); 161 | editor.focus(); 162 | }; 163 | 164 | const get = editor => ({ 165 | hasDraft: () => hasDraft(editor), 166 | storeDraft: () => storeDraft(editor), 167 | restoreDraft: () => restoreDraft(editor), 168 | removeDraft: fire => removeDraft(editor, fire), 169 | isEmpty: html => isEmpty(editor, html) 170 | }); 171 | 172 | var global = tinymce.util.Tools.resolve('tinymce.EditorManager'); 173 | 174 | const setup = editor => { 175 | editor.editorManager.on('BeforeUnload', e => { 176 | let msg; 177 | global$1.each(global.get(), editor => { 178 | if (editor.plugins.autosave) { 179 | editor.plugins.autosave.storeDraft(); 180 | } 181 | if (!msg && editor.isDirty() && shouldAskBeforeUnload(editor)) { 182 | msg = editor.translate('You have unsaved changes are you sure you want to navigate away?'); 183 | } 184 | }); 185 | if (msg) { 186 | e.preventDefault(); 187 | e.returnValue = msg; 188 | } 189 | }); 190 | }; 191 | 192 | const makeSetupHandler = editor => api => { 193 | api.setEnabled(hasDraft(editor)); 194 | const editorEventCallback = () => api.setEnabled(hasDraft(editor)); 195 | editor.on('StoreDraft RestoreDraft RemoveDraft', editorEventCallback); 196 | return () => editor.off('StoreDraft RestoreDraft RemoveDraft', editorEventCallback); 197 | }; 198 | const register = editor => { 199 | startStoreDraft(editor); 200 | const onAction = () => { 201 | restoreLastDraft(editor); 202 | }; 203 | editor.ui.registry.addButton('restoredraft', { 204 | tooltip: 'Restore last draft', 205 | icon: 'restore-draft', 206 | onAction, 207 | onSetup: makeSetupHandler(editor) 208 | }); 209 | editor.ui.registry.addMenuItem('restoredraft', { 210 | text: 'Restore last draft', 211 | icon: 'restore-draft', 212 | onAction, 213 | onSetup: makeSetupHandler(editor) 214 | }); 215 | }; 216 | 217 | var Plugin = () => { 218 | global$4.add('autosave', editor => { 219 | register$1(editor); 220 | setup(editor); 221 | register(editor); 222 | editor.on('init', () => { 223 | if (shouldRestoreWhenEmpty(editor) && editor.dom.isEmpty(editor.getBody())) { 224 | restoreDraft(editor); 225 | } 226 | }); 227 | return get(editor); 228 | }); 229 | }; 230 | 231 | Plugin(); 232 | 233 | })(); 234 | -------------------------------------------------------------------------------- /src/libs/tinymce/plugins/insertdatetime/plugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TinyMCE version 6.2.0 (2022-09-08) 3 | */ 4 | 5 | (function () { 6 | 'use strict'; 7 | 8 | var global$1 = tinymce.util.Tools.resolve('tinymce.PluginManager'); 9 | 10 | const option = name => editor => editor.options.get(name); 11 | const register$2 = editor => { 12 | const registerOption = editor.options.register; 13 | registerOption('insertdatetime_dateformat', { 14 | processor: 'string', 15 | default: editor.translate('%Y-%m-%d') 16 | }); 17 | registerOption('insertdatetime_timeformat', { 18 | processor: 'string', 19 | default: editor.translate('%H:%M:%S') 20 | }); 21 | registerOption('insertdatetime_formats', { 22 | processor: 'string[]', 23 | default: [ 24 | '%H:%M:%S', 25 | '%Y-%m-%d', 26 | '%I:%M:%S %p', 27 | '%D' 28 | ] 29 | }); 30 | registerOption('insertdatetime_element', { 31 | processor: 'boolean', 32 | default: false 33 | }); 34 | }; 35 | const getDateFormat = option('insertdatetime_dateformat'); 36 | const getTimeFormat = option('insertdatetime_timeformat'); 37 | const getFormats = option('insertdatetime_formats'); 38 | const shouldInsertTimeElement = option('insertdatetime_element'); 39 | const getDefaultDateTime = editor => { 40 | const formats = getFormats(editor); 41 | return formats.length > 0 ? formats[0] : getTimeFormat(editor); 42 | }; 43 | 44 | const daysShort = 'Sun Mon Tue Wed Thu Fri Sat Sun'.split(' '); 45 | const daysLong = 'Sunday Monday Tuesday Wednesday Thursday Friday Saturday Sunday'.split(' '); 46 | const monthsShort = 'Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec'.split(' '); 47 | const monthsLong = 'January February March April May June July August September October November December'.split(' '); 48 | const addZeros = (value, len) => { 49 | value = '' + value; 50 | if (value.length < len) { 51 | for (let i = 0; i < len - value.length; i++) { 52 | value = '0' + value; 53 | } 54 | } 55 | return value; 56 | }; 57 | const getDateTime = (editor, fmt, date = new Date()) => { 58 | fmt = fmt.replace('%D', '%m/%d/%Y'); 59 | fmt = fmt.replace('%r', '%I:%M:%S %p'); 60 | fmt = fmt.replace('%Y', '' + date.getFullYear()); 61 | fmt = fmt.replace('%y', '' + date.getYear()); 62 | fmt = fmt.replace('%m', addZeros(date.getMonth() + 1, 2)); 63 | fmt = fmt.replace('%d', addZeros(date.getDate(), 2)); 64 | fmt = fmt.replace('%H', '' + addZeros(date.getHours(), 2)); 65 | fmt = fmt.replace('%M', '' + addZeros(date.getMinutes(), 2)); 66 | fmt = fmt.replace('%S', '' + addZeros(date.getSeconds(), 2)); 67 | fmt = fmt.replace('%I', '' + ((date.getHours() + 11) % 12 + 1)); 68 | fmt = fmt.replace('%p', '' + (date.getHours() < 12 ? 'AM' : 'PM')); 69 | fmt = fmt.replace('%B', '' + editor.translate(monthsLong[date.getMonth()])); 70 | fmt = fmt.replace('%b', '' + editor.translate(monthsShort[date.getMonth()])); 71 | fmt = fmt.replace('%A', '' + editor.translate(daysLong[date.getDay()])); 72 | fmt = fmt.replace('%a', '' + editor.translate(daysShort[date.getDay()])); 73 | fmt = fmt.replace('%%', '%'); 74 | return fmt; 75 | }; 76 | const updateElement = (editor, timeElm, computerTime, userTime) => { 77 | const newTimeElm = editor.dom.create('time', { datetime: computerTime }, userTime); 78 | editor.dom.replace(newTimeElm, timeElm); 79 | editor.selection.select(newTimeElm, true); 80 | editor.selection.collapse(false); 81 | }; 82 | const insertDateTime = (editor, format) => { 83 | if (shouldInsertTimeElement(editor)) { 84 | const userTime = getDateTime(editor, format); 85 | let computerTime; 86 | if (/%[HMSIp]/.test(format)) { 87 | computerTime = getDateTime(editor, '%Y-%m-%dT%H:%M'); 88 | } else { 89 | computerTime = getDateTime(editor, '%Y-%m-%d'); 90 | } 91 | const timeElm = editor.dom.getParent(editor.selection.getStart(), 'time'); 92 | if (timeElm) { 93 | updateElement(editor, timeElm, computerTime, userTime); 94 | } else { 95 | editor.insertContent(''); 96 | } 97 | } else { 98 | editor.insertContent(getDateTime(editor, format)); 99 | } 100 | }; 101 | 102 | const register$1 = editor => { 103 | editor.addCommand('mceInsertDate', (_ui, value) => { 104 | insertDateTime(editor, value !== null && value !== void 0 ? value : getDateFormat(editor)); 105 | }); 106 | editor.addCommand('mceInsertTime', (_ui, value) => { 107 | insertDateTime(editor, value !== null && value !== void 0 ? value : getTimeFormat(editor)); 108 | }); 109 | }; 110 | 111 | const Cell = initial => { 112 | let value = initial; 113 | const get = () => { 114 | return value; 115 | }; 116 | const set = v => { 117 | value = v; 118 | }; 119 | return { 120 | get, 121 | set 122 | }; 123 | }; 124 | 125 | var global = tinymce.util.Tools.resolve('tinymce.util.Tools'); 126 | 127 | const register = editor => { 128 | const formats = getFormats(editor); 129 | const defaultFormat = Cell(getDefaultDateTime(editor)); 130 | const insertDateTime = format => editor.execCommand('mceInsertDate', false, format); 131 | editor.ui.registry.addSplitButton('insertdatetime', { 132 | icon: 'insert-time', 133 | tooltip: 'Insert date/time', 134 | select: value => value === defaultFormat.get(), 135 | fetch: done => { 136 | done(global.map(formats, format => ({ 137 | type: 'choiceitem', 138 | text: getDateTime(editor, format), 139 | value: format 140 | }))); 141 | }, 142 | onAction: _api => { 143 | insertDateTime(defaultFormat.get()); 144 | }, 145 | onItemAction: (_api, value) => { 146 | defaultFormat.set(value); 147 | insertDateTime(value); 148 | } 149 | }); 150 | const makeMenuItemHandler = format => () => { 151 | defaultFormat.set(format); 152 | insertDateTime(format); 153 | }; 154 | editor.ui.registry.addNestedMenuItem('insertdatetime', { 155 | icon: 'insert-time', 156 | text: 'Date/time', 157 | getSubmenuItems: () => global.map(formats, format => ({ 158 | type: 'menuitem', 159 | text: getDateTime(editor, format), 160 | onAction: makeMenuItemHandler(format) 161 | })) 162 | }); 163 | }; 164 | 165 | var Plugin = () => { 166 | global$1.add('insertdatetime', editor => { 167 | register$2(editor); 168 | register$1(editor); 169 | register(editor); 170 | }); 171 | }; 172 | 173 | Plugin(); 174 | 175 | })(); 176 | -------------------------------------------------------------------------------- /src/libs/tinymce/plugins/visualblocks/plugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TinyMCE version 6.2.0 (2022-09-08) 3 | */ 4 | 5 | (function () { 6 | 'use strict'; 7 | 8 | const Cell = initial => { 9 | let value = initial; 10 | const get = () => { 11 | return value; 12 | }; 13 | const set = v => { 14 | value = v; 15 | }; 16 | return { 17 | get, 18 | set 19 | }; 20 | }; 21 | 22 | var global = tinymce.util.Tools.resolve('tinymce.PluginManager'); 23 | 24 | const fireVisualBlocks = (editor, state) => { 25 | editor.dispatch('VisualBlocks', { state }); 26 | }; 27 | 28 | const toggleVisualBlocks = (editor, pluginUrl, enabledState) => { 29 | const dom = editor.dom; 30 | dom.toggleClass(editor.getBody(), 'mce-visualblocks'); 31 | enabledState.set(!enabledState.get()); 32 | fireVisualBlocks(editor, enabledState.get()); 33 | }; 34 | 35 | const register$2 = (editor, pluginUrl, enabledState) => { 36 | editor.addCommand('mceVisualBlocks', () => { 37 | toggleVisualBlocks(editor, pluginUrl, enabledState); 38 | }); 39 | }; 40 | 41 | const option = name => editor => editor.options.get(name); 42 | const register$1 = editor => { 43 | const registerOption = editor.options.register; 44 | registerOption('visualblocks_default_state', { 45 | processor: 'boolean', 46 | default: false 47 | }); 48 | }; 49 | const isEnabledByDefault = option('visualblocks_default_state'); 50 | 51 | const setup = (editor, pluginUrl, enabledState) => { 52 | editor.on('PreviewFormats AfterPreviewFormats', e => { 53 | if (enabledState.get()) { 54 | editor.dom.toggleClass(editor.getBody(), 'mce-visualblocks', e.type === 'afterpreviewformats'); 55 | } 56 | }); 57 | editor.on('init', () => { 58 | if (isEnabledByDefault(editor)) { 59 | toggleVisualBlocks(editor, pluginUrl, enabledState); 60 | } 61 | }); 62 | }; 63 | 64 | const toggleActiveState = (editor, enabledState) => api => { 65 | api.setActive(enabledState.get()); 66 | const editorEventCallback = e => api.setActive(e.state); 67 | editor.on('VisualBlocks', editorEventCallback); 68 | return () => editor.off('VisualBlocks', editorEventCallback); 69 | }; 70 | const register = (editor, enabledState) => { 71 | const onAction = () => editor.execCommand('mceVisualBlocks'); 72 | editor.ui.registry.addToggleButton('visualblocks', { 73 | icon: 'visualblocks', 74 | tooltip: 'Show blocks', 75 | onAction, 76 | onSetup: toggleActiveState(editor, enabledState) 77 | }); 78 | editor.ui.registry.addToggleMenuItem('visualblocks', { 79 | text: 'Show blocks', 80 | icon: 'visualblocks', 81 | onAction, 82 | onSetup: toggleActiveState(editor, enabledState) 83 | }); 84 | }; 85 | 86 | var Plugin = () => { 87 | global.add('visualblocks', (editor, pluginUrl) => { 88 | register$1(editor); 89 | const enabledState = Cell(false); 90 | register$2(editor, pluginUrl, enabledState); 91 | register(editor, enabledState); 92 | setup(editor, pluginUrl, enabledState); 93 | }); 94 | }; 95 | 96 | Plugin(); 97 | 98 | })(); 99 | -------------------------------------------------------------------------------- /src/libs/tinymce/plugins/visualchars/plugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TinyMCE version 6.2.0 (2022-09-08) 3 | */ 4 | 5 | (function () { 6 | 'use strict'; 7 | 8 | const Cell = initial => { 9 | let value = initial; 10 | const get = () => { 11 | return value; 12 | }; 13 | const set = v => { 14 | value = v; 15 | }; 16 | return { 17 | get, 18 | set 19 | }; 20 | }; 21 | 22 | var global = tinymce.util.Tools.resolve('tinymce.PluginManager'); 23 | 24 | const get$2 = toggleState => { 25 | const isEnabled = () => { 26 | return toggleState.get(); 27 | }; 28 | return { isEnabled }; 29 | }; 30 | 31 | const fireVisualChars = (editor, state) => { 32 | return editor.dispatch('VisualChars', { state }); 33 | }; 34 | 35 | const hasProto = (v, constructor, predicate) => { 36 | var _a; 37 | if (predicate(v, constructor.prototype)) { 38 | return true; 39 | } else { 40 | return ((_a = v.constructor) === null || _a === void 0 ? void 0 : _a.name) === constructor.name; 41 | } 42 | }; 43 | const typeOf = x => { 44 | const t = typeof x; 45 | if (x === null) { 46 | return 'null'; 47 | } else if (t === 'object' && Array.isArray(x)) { 48 | return 'array'; 49 | } else if (t === 'object' && hasProto(x, String, (o, proto) => proto.isPrototypeOf(o))) { 50 | return 'string'; 51 | } else { 52 | return t; 53 | } 54 | }; 55 | const isType$1 = type => value => typeOf(value) === type; 56 | const isSimpleType = type => value => typeof value === type; 57 | const eq = t => a => t === a; 58 | const isString = isType$1('string'); 59 | const isNull = eq(null); 60 | const isBoolean = isSimpleType('boolean'); 61 | const isNullable = a => a === null || a === undefined; 62 | const isNonNullable = a => !isNullable(a); 63 | const isNumber = isSimpleType('number'); 64 | 65 | class Optional { 66 | constructor(tag, value) { 67 | this.tag = tag; 68 | this.value = value; 69 | } 70 | static some(value) { 71 | return new Optional(true, value); 72 | } 73 | static none() { 74 | return Optional.singletonNone; 75 | } 76 | fold(onNone, onSome) { 77 | if (this.tag) { 78 | return onSome(this.value); 79 | } else { 80 | return onNone(); 81 | } 82 | } 83 | isSome() { 84 | return this.tag; 85 | } 86 | isNone() { 87 | return !this.tag; 88 | } 89 | map(mapper) { 90 | if (this.tag) { 91 | return Optional.some(mapper(this.value)); 92 | } else { 93 | return Optional.none(); 94 | } 95 | } 96 | bind(binder) { 97 | if (this.tag) { 98 | return binder(this.value); 99 | } else { 100 | return Optional.none(); 101 | } 102 | } 103 | exists(predicate) { 104 | return this.tag && predicate(this.value); 105 | } 106 | forall(predicate) { 107 | return !this.tag || predicate(this.value); 108 | } 109 | filter(predicate) { 110 | if (!this.tag || predicate(this.value)) { 111 | return this; 112 | } else { 113 | return Optional.none(); 114 | } 115 | } 116 | getOr(replacement) { 117 | return this.tag ? this.value : replacement; 118 | } 119 | or(replacement) { 120 | return this.tag ? this : replacement; 121 | } 122 | getOrThunk(thunk) { 123 | return this.tag ? this.value : thunk(); 124 | } 125 | orThunk(thunk) { 126 | return this.tag ? this : thunk(); 127 | } 128 | getOrDie(message) { 129 | if (!this.tag) { 130 | throw new Error(message !== null && message !== void 0 ? message : 'Called getOrDie on None'); 131 | } else { 132 | return this.value; 133 | } 134 | } 135 | static from(value) { 136 | return isNonNullable(value) ? Optional.some(value) : Optional.none(); 137 | } 138 | getOrNull() { 139 | return this.tag ? this.value : null; 140 | } 141 | getOrUndefined() { 142 | return this.value; 143 | } 144 | each(worker) { 145 | if (this.tag) { 146 | worker(this.value); 147 | } 148 | } 149 | toArray() { 150 | return this.tag ? [this.value] : []; 151 | } 152 | toString() { 153 | return this.tag ? `some(${ this.value })` : 'none()'; 154 | } 155 | } 156 | Optional.singletonNone = new Optional(false); 157 | 158 | const map = (xs, f) => { 159 | const len = xs.length; 160 | const r = new Array(len); 161 | for (let i = 0; i < len; i++) { 162 | const x = xs[i]; 163 | r[i] = f(x, i); 164 | } 165 | return r; 166 | }; 167 | const each$1 = (xs, f) => { 168 | for (let i = 0, len = xs.length; i < len; i++) { 169 | const x = xs[i]; 170 | f(x, i); 171 | } 172 | }; 173 | const filter = (xs, pred) => { 174 | const r = []; 175 | for (let i = 0, len = xs.length; i < len; i++) { 176 | const x = xs[i]; 177 | if (pred(x, i)) { 178 | r.push(x); 179 | } 180 | } 181 | return r; 182 | }; 183 | 184 | const keys = Object.keys; 185 | const each = (obj, f) => { 186 | const props = keys(obj); 187 | for (let k = 0, len = props.length; k < len; k++) { 188 | const i = props[k]; 189 | const x = obj[i]; 190 | f(x, i); 191 | } 192 | }; 193 | 194 | typeof window !== 'undefined' ? window : Function('return this;')(); 195 | 196 | const TEXT = 3; 197 | 198 | const type = element => element.dom.nodeType; 199 | const value = element => element.dom.nodeValue; 200 | const isType = t => element => type(element) === t; 201 | const isText = isType(TEXT); 202 | 203 | const rawSet = (dom, key, value) => { 204 | if (isString(value) || isBoolean(value) || isNumber(value)) { 205 | dom.setAttribute(key, value + ''); 206 | } else { 207 | console.error('Invalid call to Attribute.set. Key ', key, ':: Value ', value, ':: Element ', dom); 208 | throw new Error('Attribute value was not simple'); 209 | } 210 | }; 211 | const set = (element, key, value) => { 212 | rawSet(element.dom, key, value); 213 | }; 214 | const get$1 = (element, key) => { 215 | const v = element.dom.getAttribute(key); 216 | return v === null ? undefined : v; 217 | }; 218 | const remove$3 = (element, key) => { 219 | element.dom.removeAttribute(key); 220 | }; 221 | 222 | const read = (element, attr) => { 223 | const value = get$1(element, attr); 224 | return value === undefined || value === '' ? [] : value.split(' '); 225 | }; 226 | const add$2 = (element, attr, id) => { 227 | const old = read(element, attr); 228 | const nu = old.concat([id]); 229 | set(element, attr, nu.join(' ')); 230 | return true; 231 | }; 232 | const remove$2 = (element, attr, id) => { 233 | const nu = filter(read(element, attr), v => v !== id); 234 | if (nu.length > 0) { 235 | set(element, attr, nu.join(' ')); 236 | } else { 237 | remove$3(element, attr); 238 | } 239 | return false; 240 | }; 241 | 242 | const supports = element => element.dom.classList !== undefined; 243 | const get = element => read(element, 'class'); 244 | const add$1 = (element, clazz) => add$2(element, 'class', clazz); 245 | const remove$1 = (element, clazz) => remove$2(element, 'class', clazz); 246 | 247 | const add = (element, clazz) => { 248 | if (supports(element)) { 249 | element.dom.classList.add(clazz); 250 | } else { 251 | add$1(element, clazz); 252 | } 253 | }; 254 | const cleanClass = element => { 255 | const classList = supports(element) ? element.dom.classList : get(element); 256 | if (classList.length === 0) { 257 | remove$3(element, 'class'); 258 | } 259 | }; 260 | const remove = (element, clazz) => { 261 | if (supports(element)) { 262 | const classList = element.dom.classList; 263 | classList.remove(clazz); 264 | } else { 265 | remove$1(element, clazz); 266 | } 267 | cleanClass(element); 268 | }; 269 | 270 | const fromHtml = (html, scope) => { 271 | const doc = scope || document; 272 | const div = doc.createElement('div'); 273 | div.innerHTML = html; 274 | if (!div.hasChildNodes() || div.childNodes.length > 1) { 275 | const message = 'HTML does not have a single root node'; 276 | console.error(message, html); 277 | throw new Error(message); 278 | } 279 | return fromDom(div.childNodes[0]); 280 | }; 281 | const fromTag = (tag, scope) => { 282 | const doc = scope || document; 283 | const node = doc.createElement(tag); 284 | return fromDom(node); 285 | }; 286 | const fromText = (text, scope) => { 287 | const doc = scope || document; 288 | const node = doc.createTextNode(text); 289 | return fromDom(node); 290 | }; 291 | const fromDom = node => { 292 | if (node === null || node === undefined) { 293 | throw new Error('Node cannot be null or undefined'); 294 | } 295 | return { dom: node }; 296 | }; 297 | const fromPoint = (docElm, x, y) => Optional.from(docElm.dom.elementFromPoint(x, y)).map(fromDom); 298 | const SugarElement = { 299 | fromHtml, 300 | fromTag, 301 | fromText, 302 | fromDom, 303 | fromPoint 304 | }; 305 | 306 | const charMap = { 307 | '\xA0': 'nbsp', 308 | '\xAD': 'shy' 309 | }; 310 | const charMapToRegExp = (charMap, global) => { 311 | let regExp = ''; 312 | each(charMap, (_value, key) => { 313 | regExp += key; 314 | }); 315 | return new RegExp('[' + regExp + ']', global ? 'g' : ''); 316 | }; 317 | const charMapToSelector = charMap => { 318 | let selector = ''; 319 | each(charMap, value => { 320 | if (selector) { 321 | selector += ','; 322 | } 323 | selector += 'span.mce-' + value; 324 | }); 325 | return selector; 326 | }; 327 | const regExp = charMapToRegExp(charMap); 328 | const regExpGlobal = charMapToRegExp(charMap, true); 329 | const selector = charMapToSelector(charMap); 330 | const nbspClass = 'mce-nbsp'; 331 | 332 | const wrapCharWithSpan = value => '' + value + ''; 333 | 334 | const isMatch = n => { 335 | const value$1 = value(n); 336 | return isText(n) && isString(value$1) && regExp.test(value$1); 337 | }; 338 | const filterDescendants = (scope, predicate) => { 339 | let result = []; 340 | const dom = scope.dom; 341 | const children = map(dom.childNodes, SugarElement.fromDom); 342 | each$1(children, x => { 343 | if (predicate(x)) { 344 | result = result.concat([x]); 345 | } 346 | result = result.concat(filterDescendants(x, predicate)); 347 | }); 348 | return result; 349 | }; 350 | const findParentElm = (elm, rootElm) => { 351 | while (elm.parentNode) { 352 | if (elm.parentNode === rootElm) { 353 | return rootElm; 354 | } 355 | elm = elm.parentNode; 356 | } 357 | return undefined; 358 | }; 359 | const replaceWithSpans = text => text.replace(regExpGlobal, wrapCharWithSpan); 360 | 361 | const isWrappedNbsp = node => node.nodeName.toLowerCase() === 'span' && node.classList.contains('mce-nbsp-wrap'); 362 | const show = (editor, rootElm) => { 363 | const dom = editor.dom; 364 | const nodeList = filterDescendants(SugarElement.fromDom(rootElm), isMatch); 365 | each$1(nodeList, n => { 366 | var _a; 367 | const parent = n.dom.parentNode; 368 | if (isWrappedNbsp(parent)) { 369 | add(SugarElement.fromDom(parent), nbspClass); 370 | } else { 371 | const withSpans = replaceWithSpans(dom.encode((_a = value(n)) !== null && _a !== void 0 ? _a : '')); 372 | const div = dom.create('div', {}, withSpans); 373 | let node; 374 | while (node = div.lastChild) { 375 | dom.insertAfter(node, n.dom); 376 | } 377 | editor.dom.remove(n.dom); 378 | } 379 | }); 380 | }; 381 | const hide = (editor, rootElm) => { 382 | const nodeList = editor.dom.select(selector, rootElm); 383 | each$1(nodeList, node => { 384 | if (isWrappedNbsp(node)) { 385 | remove(SugarElement.fromDom(node), nbspClass); 386 | } else { 387 | editor.dom.remove(node, true); 388 | } 389 | }); 390 | }; 391 | const toggle = editor => { 392 | const body = editor.getBody(); 393 | const bookmark = editor.selection.getBookmark(); 394 | let parentNode = findParentElm(editor.selection.getNode(), body); 395 | parentNode = parentNode !== undefined ? parentNode : body; 396 | hide(editor, parentNode); 397 | show(editor, parentNode); 398 | editor.selection.moveToBookmark(bookmark); 399 | }; 400 | 401 | const applyVisualChars = (editor, toggleState) => { 402 | fireVisualChars(editor, toggleState.get()); 403 | const body = editor.getBody(); 404 | if (toggleState.get() === true) { 405 | show(editor, body); 406 | } else { 407 | hide(editor, body); 408 | } 409 | }; 410 | const toggleVisualChars = (editor, toggleState) => { 411 | toggleState.set(!toggleState.get()); 412 | const bookmark = editor.selection.getBookmark(); 413 | applyVisualChars(editor, toggleState); 414 | editor.selection.moveToBookmark(bookmark); 415 | }; 416 | 417 | const register$2 = (editor, toggleState) => { 418 | editor.addCommand('mceVisualChars', () => { 419 | toggleVisualChars(editor, toggleState); 420 | }); 421 | }; 422 | 423 | const option = name => editor => editor.options.get(name); 424 | const register$1 = editor => { 425 | const registerOption = editor.options.register; 426 | registerOption('visualchars_default_state', { 427 | processor: 'boolean', 428 | default: false 429 | }); 430 | }; 431 | const isEnabledByDefault = option('visualchars_default_state'); 432 | 433 | const setup$1 = (editor, toggleState) => { 434 | editor.on('init', () => { 435 | applyVisualChars(editor, toggleState); 436 | }); 437 | }; 438 | 439 | const first = (fn, rate) => { 440 | let timer = null; 441 | const cancel = () => { 442 | if (!isNull(timer)) { 443 | clearTimeout(timer); 444 | timer = null; 445 | } 446 | }; 447 | const throttle = (...args) => { 448 | if (isNull(timer)) { 449 | timer = setTimeout(() => { 450 | timer = null; 451 | fn.apply(null, args); 452 | }, rate); 453 | } 454 | }; 455 | return { 456 | cancel, 457 | throttle 458 | }; 459 | }; 460 | 461 | const setup = (editor, toggleState) => { 462 | const debouncedToggle = first(() => { 463 | toggle(editor); 464 | }, 300); 465 | editor.on('keydown', e => { 466 | if (toggleState.get() === true) { 467 | e.keyCode === 13 ? toggle(editor) : debouncedToggle.throttle(); 468 | } 469 | }); 470 | editor.on('remove', debouncedToggle.cancel); 471 | }; 472 | 473 | const toggleActiveState = (editor, enabledStated) => api => { 474 | api.setActive(enabledStated.get()); 475 | const editorEventCallback = e => api.setActive(e.state); 476 | editor.on('VisualChars', editorEventCallback); 477 | return () => editor.off('VisualChars', editorEventCallback); 478 | }; 479 | const register = (editor, toggleState) => { 480 | const onAction = () => editor.execCommand('mceVisualChars'); 481 | editor.ui.registry.addToggleButton('visualchars', { 482 | tooltip: 'Show invisible characters', 483 | icon: 'visualchars', 484 | onAction, 485 | onSetup: toggleActiveState(editor, toggleState) 486 | }); 487 | editor.ui.registry.addToggleMenuItem('visualchars', { 488 | text: 'Show invisible characters', 489 | icon: 'visualchars', 490 | onAction, 491 | onSetup: toggleActiveState(editor, toggleState) 492 | }); 493 | }; 494 | 495 | var Plugin = () => { 496 | global.add('visualchars', editor => { 497 | register$1(editor); 498 | const toggleState = Cell(isEnabledByDefault(editor)); 499 | register$2(editor, toggleState); 500 | register(editor, toggleState); 501 | setup(editor, toggleState); 502 | setup$1(editor, toggleState); 503 | return get$2(toggleState); 504 | }); 505 | }; 506 | 507 | Plugin(); 508 | 509 | })(); 510 | -------------------------------------------------------------------------------- /src/libs/tinymce/plugins/wordcount/plugin.js: -------------------------------------------------------------------------------- 1 | /** 2 | * TinyMCE version 6.2.0 (2022-09-08) 3 | */ 4 | 5 | (function () { 6 | 'use strict'; 7 | 8 | var global$2 = tinymce.util.Tools.resolve('tinymce.PluginManager'); 9 | 10 | const eq = t => a => t === a; 11 | const isNull = eq(null); 12 | 13 | const identity = x => { 14 | return x; 15 | }; 16 | 17 | const zeroWidth = '\uFEFF'; 18 | const removeZwsp$1 = s => s.replace(/\uFEFF/g, ''); 19 | 20 | const map = (xs, f) => { 21 | const len = xs.length; 22 | const r = new Array(len); 23 | for (let i = 0; i < len; i++) { 24 | const x = xs[i]; 25 | r[i] = f(x, i); 26 | } 27 | return r; 28 | }; 29 | 30 | const punctuationStr = '[!-#%-*,-\\/:;?@\\[-\\]_{}\xA1\xAB\xB7\xBB\xBF;\xB7\u055A-\u055F\u0589\u058A\u05BE\u05C0\u05C3\u05C6\u05F3\u05F4\u0609\u060A\u060C\u060D\u061B\u061E\u061F\u066A-\u066D\u06D4\u0700-\u070D\u07F7-\u07F9\u0830-\u083E\u085E\u0964\u0965\u0970\u0DF4\u0E4F\u0E5A\u0E5B\u0F04-\u0F12\u0F3A-\u0F3D\u0F85\u0FD0-\u0FD4\u0FD9\u0FDA\u104A-\u104F\u10FB\u1361-\u1368\u1400\u166D\u166E\u169B\u169C\u16EB-\u16ED\u1735\u1736\u17D4-\u17D6\u17D8-\u17DA\u1800-\u180A\u1944\u1945\u1A1E\u1A1F\u1AA0-\u1AA6\u1AA8-\u1AAD\u1B5A-\u1B60\u1BFC-\u1BFF\u1C3B-\u1C3F\u1C7E\u1C7F\u1CD3\u2010-\u2027\u2030-\u2043\u2045-\u2051\u2053-\u205E\u207D\u207E\u208D\u208E\u3008\u3009\u2768-\u2775\u27C5\u27C6\u27E6-\u27EF\u2983-\u2998\u29D8-\u29DB\u29FC\u29FD\u2CF9-\u2CFC\u2CFE\u2CFF\u2D70\u2E00-\u2E2E\u2E30\u2E31\u3001-\u3003\u3008-\u3011\u3014-\u301F\u3030\u303D\u30A0\u30FB\uA4FE\uA4FF\uA60D-\uA60F\uA673\uA67E\uA6F2-\uA6F7\uA874-\uA877\uA8CE\uA8CF\uA8F8-\uA8FA\uA92E\uA92F\uA95F\uA9C1-\uA9CD\uA9DE\uA9DF\uAA5C-\uAA5F\uAADE\uAADF\uABEB\uFD3E\uFD3F\uFE10-\uFE19\uFE30-\uFE52\uFE54-\uFE61\uFE63\uFE68\uFE6A\uFE6B\uFF01-\uFF03\uFF05-\uFF0A\uFF0C-\uFF0F\uFF1A\uFF1B\uFF1F\uFF20\uFF3B-\uFF3D\uff3f\uFF5B\uFF5D\uFF5F-\uFF65]'; 31 | const regExps = { 32 | aletter: '[A-Za-z\xaa\xb5\xba\xc0-\xd6\xd8-\xf6\xf8-\u02c1\u02c6-\u02d1\u02e0-\u02e4\u02ec\u02ee\u0370-\u0374\u0376\u0377\u037a-\u037d\u0386\u0388-\u038a\u038c\u038e-\u03a1\u03a3-\u03f5\u03f7-\u0481\u048a-\u0527\u0531-\u0556\u0559\u0561-\u0587\u05d0-\u05ea\u05f0-\u05F3\u0620-\u064a\u066e\u066f\u0671-\u06d3\u06d5\u06e5\u06e6\u06ee\u06ef\u06fa-\u06fc\u06ff\u0710\u0712-\u072f\u074d-\u07a5\u07b1\u07ca-\u07ea\u07f4\u07f5\u07fa\u0800-\u0815\u081a\u0824\u0828\u0840-\u0858\u0904-\u0939\u093d\u0950\u0958-\u0961\u0971-\u0977\u0979-\u097f\u0985-\u098c\u098f\u0990\u0993-\u09a8\u09aa-\u09b0\u09b2\u09b6-\u09b9\u09bd\u09ce\u09dc\u09dd\u09df-\u09e1\u09f0\u09f1\u0a05-\u0a0a\u0a0f\u0a10\u0a13-\u0a28\u0a2a-\u0a30\u0a32\u0a33\u0a35\u0a36\u0a38\u0a39\u0a59-\u0a5c\u0a5e\u0a72-\u0a74\u0a85-\u0a8d\u0a8f-\u0a91\u0a93-\u0aa8\u0aaa-\u0ab0\u0ab2\u0ab3\u0ab5-\u0ab9\u0abd\u0ad0\u0ae0\u0ae1\u0b05-\u0b0c\u0b0f\u0b10\u0b13-\u0b28\u0b2a-\u0b30\u0b32\u0b33\u0b35-\u0b39\u0b3d\u0b5c\u0b5d\u0b5f-\u0b61\u0b71\u0b83\u0b85-\u0b8a\u0b8e-\u0b90\u0b92-\u0b95\u0b99\u0b9a\u0b9c\u0b9e\u0b9f\u0ba3\u0ba4\u0ba8-\u0baa\u0bae-\u0bb9\u0bd0\u0c05-\u0c0c\u0c0e-\u0c10\u0c12-\u0c28\u0c2a-\u0c33\u0c35-\u0c39\u0c3d\u0c58\u0c59\u0c60\u0c61\u0c85-\u0c8c\u0c8e-\u0c90\u0c92-\u0ca8\u0caa-\u0cb3\u0cb5-\u0cb9\u0cbd\u0cde\u0ce0\u0ce1\u0cf1\u0cf2\u0d05-\u0d0c\u0d0e-\u0d10\u0d12-\u0d3a\u0d3d\u0d4e\u0d60\u0d61\u0d7a-\u0d7f\u0d85-\u0d96\u0d9a-\u0db1\u0db3-\u0dbb\u0dbd\u0dc0-\u0dc6\u0f00\u0f40-\u0f47\u0f49-\u0f6c\u0f88-\u0f8c\u10a0-\u10c5\u10d0-\u10fa\u10fc\u1100-\u1248\u124a-\u124d\u1250-\u1256\u1258\u125a-\u125d\u1260-\u1288\u128a-\u128d\u1290-\u12b0\u12b2-\u12b5\u12b8-\u12be\u12c0\u12c2-\u12c5\u12c8-\u12d6\u12d8-\u1310\u1312-\u1315\u1318-\u135a\u1380-\u138f\u13a0-\u13f4\u1401-\u166c\u166f-\u167f\u1681-\u169a\u16a0-\u16ea\u16ee-\u16f0\u1700-\u170c\u170e-\u1711\u1720-\u1731\u1740-\u1751\u1760-\u176c\u176e-\u1770\u1820-\u1877\u1880-\u18a8\u18aa\u18b0-\u18f5\u1900-\u191c\u1a00-\u1a16\u1b05-\u1b33\u1b45-\u1b4b\u1b83-\u1ba0\u1bae\u1baf\u1bc0-\u1be5\u1c00-\u1c23\u1c4d-\u1c4f\u1c5a-\u1c7d\u1ce9-\u1cec\u1cee-\u1cf1\u1d00-\u1dbf\u1e00-\u1f15\u1f18-\u1f1d\u1f20-\u1f45\u1f48-\u1f4d\u1f50-\u1f57\u1f59\u1f5b\u1f5d\u1f5f-\u1f7d\u1f80-\u1fb4\u1fb6-\u1fbc\u1fbe\u1fc2-\u1fc4\u1fc6-\u1fcc\u1fd0-\u1fd3\u1fd6-\u1fdb\u1fe0-\u1fec\u1ff2-\u1ff4\u1ff6-\u1ffc\u2071\u207f\u2090-\u209c\u2102\u2107\u210a-\u2113\u2115\u2119-\u211d\u2124\u2126\u2128\u212a-\u212d\u212f-\u2139\u213c-\u213f\u2145-\u2149\u214e\u2160-\u2188\u24B6-\u24E9\u2c00-\u2c2e\u2c30-\u2c5e\u2c60-\u2ce4\u2ceb-\u2cee\u2d00-\u2d25\u2d30-\u2d65\u2d6f\u2d80-\u2d96\u2da0-\u2da6\u2da8-\u2dae\u2db0-\u2db6\u2db8-\u2dbe\u2dc0-\u2dc6\u2dc8-\u2dce\u2dd0-\u2dd6\u2dd8-\u2dde\u2e2f\u3005\u303b\u303c\u3105-\u312d\u3131-\u318e\u31a0-\u31ba\ua000-\ua48c\ua4d0-\ua4fd\ua500-\ua60c\ua610-\ua61f\ua62a\ua62b\ua640-\ua66e\ua67f-\ua697\ua6a0-\ua6ef\ua717-\ua71f\ua722-\ua788\ua78b-\ua78e\ua790\ua791\ua7a0-\ua7a9\ua7fa-\ua801\ua803-\ua805\ua807-\ua80a\ua80c-\ua822\ua840-\ua873\ua882-\ua8b3\ua8f2-\ua8f7\ua8fb\ua90a-\ua925\ua930-\ua946\ua960-\ua97c\ua984-\ua9b2\ua9cf\uaa00-\uaa28\uaa40-\uaa42\uaa44-\uaa4b\uab01-\uab06\uab09-\uab0e\uab11-\uab16\uab20-\uab26\uab28-\uab2e\uabc0-\uabe2\uac00-\ud7a3\ud7b0-\ud7c6\ud7cb-\ud7fb\ufb00-\ufb06\ufb13-\ufb17\ufb1d\ufb1f-\ufb28\ufb2a-\ufb36\ufb38-\ufb3c\ufb3e\ufb40\ufb41\ufb43\ufb44\ufb46-\ufbb1\ufbd3-\ufd3d\ufd50-\ufd8f\ufd92-\ufdc7\ufdf0-\ufdfb\ufe70-\ufe74\ufe76-\ufefc\uff21-\uff3a\uff41-\uff5a\uffa0-\uffbe\uffc2-\uffc7\uffca-\uffcf\uffd2-\uffd7\uffda-\uffdc]', 33 | midnumlet: `[-'\\.\u2018\u2019\u2024\uFE52\uFF07\uFF0E]`, 34 | midletter: '[:\xB7\xB7\u05F4\u2027\uFE13\uFE55\uFF1A]', 35 | midnum: '[\xB1+*/,;;\u0589\u060C\u060D\u066C\u07F8\u2044\uFE10\uFE14\uFE50\uFE54\uFF0C\uFF1B]', 36 | numeric: '[0-9\u0660-\u0669\u066B\u06f0-\u06f9\u07c0-\u07c9\u0966-\u096f\u09e6-\u09ef\u0a66-\u0a6f\u0ae6-\u0aef\u0b66-\u0b6f\u0be6-\u0bef\u0c66-\u0c6f\u0ce6-\u0cef\u0d66-\u0d6f\u0e50-\u0e59\u0ed0-\u0ed9\u0f20-\u0f29\u1040-\u1049\u1090-\u1099\u17e0-\u17e9\u1810-\u1819\u1946-\u194f\u19d0-\u19d9\u1a80-\u1a89\u1a90-\u1a99\u1b50-\u1b59\u1bb0-\u1bb9\u1c40-\u1c49\u1c50-\u1c59\ua620-\ua629\ua8d0-\ua8d9\ua900-\ua909\ua9d0-\ua9d9\uaa50-\uaa59\uabf0-\uabf9]', 37 | cr: '\\r', 38 | lf: '\\n', 39 | newline: '[\x0B\f\x85\u2028\u2029]', 40 | extend: '[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065f\u0670\u06d6-\u06dc\u06df-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0859-\u085b\u0900-\u0903\u093a-\u093c\u093e-\u094f\u0951-\u0957\u0962\u0963\u0981-\u0983\u09bc\u09be-\u09c4\u09c7\u09c8\u09cb-\u09cd\u09d7\u09e2\u09e3\u0a01-\u0a03\u0a3c\u0a3e-\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81-\u0a83\u0abc\u0abe-\u0ac5\u0ac7-\u0ac9\u0acb-\u0acd\u0ae2\u0ae3\u0b01-\u0b03\u0b3c\u0b3e-\u0b44\u0b47\u0b48\u0b4b-\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe-\u0bc2\u0bc6-\u0bc8\u0bca-\u0bcd\u0bd7\u0c01-\u0c03\u0c3e-\u0c44\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0c82\u0c83\u0cbc\u0cbe-\u0cc4\u0cc6-\u0cc8\u0cca-\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d02\u0d03\u0d3e-\u0d44\u0d46-\u0d48\u0d4a-\u0d4d\u0d57\u0d62\u0d63\u0d82\u0d83\u0dca\u0dcf-\u0dd4\u0dd6\u0dd8-\u0ddf\u0df2\u0df3\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f3e\u0f3f\u0f71-\u0f84\u0f86\u0f87\u0f8d-\u0f97\u0f99-\u0fbc\u0fc6\u102b-\u103e\u1056-\u1059\u105e-\u1060\u1062-\u1064\u1067-\u106d\u1071-\u1074\u1082-\u108d\u108f\u109a-\u109d\u135d-\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b6-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u192b\u1930-\u193b\u19b0-\u19c0\u19c8\u19c9\u1a17-\u1a1b\u1a55-\u1a5e\u1a60-\u1a7c\u1a7f\u1b00-\u1b04\u1b34-\u1b44\u1b6b-\u1b73\u1b80-\u1b82\u1ba1-\u1baa\u1be6-\u1bf3\u1c24-\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce8\u1ced\u1cf2\u1dc0-\u1de6\u1dfc-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2d7f\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\uA672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua823-\ua827\ua880\ua881\ua8b4-\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua953\ua980-\ua983\ua9b3-\ua9c0\uaa29-\uaa36\uaa43\uaa4c\uaa4d\uaa7b\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe3-\uabea\uabec\uabed\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]', 41 | format: '[\xAD\u0600-\u0603\u06DD\u070F\u17b4\u17b5\u200E\u200F\u202A-\u202E\u2060-\u2064\u206A-\u206F\uFEFF\uFFF9-\uFFFB]', 42 | katakana: '[\u3031-\u3035\u309B\u309C\u30A0-\u30fa\u30fc-\u30ff\u31f0-\u31ff\u32D0-\u32FE\u3300-\u3357\uff66-\uff9d]', 43 | extendnumlet: '[=_\u203f\u2040\u2054\ufe33\ufe34\ufe4d-\ufe4f\uff3f\u2200-\u22FF<>]', 44 | punctuation: punctuationStr 45 | }; 46 | const characterIndices = { 47 | ALETTER: 0, 48 | MIDNUMLET: 1, 49 | MIDLETTER: 2, 50 | MIDNUM: 3, 51 | NUMERIC: 4, 52 | CR: 5, 53 | LF: 6, 54 | NEWLINE: 7, 55 | EXTEND: 8, 56 | FORMAT: 9, 57 | KATAKANA: 10, 58 | EXTENDNUMLET: 11, 59 | AT: 12, 60 | OTHER: 13 61 | }; 62 | const SETS$1 = [ 63 | new RegExp(regExps.aletter), 64 | new RegExp(regExps.midnumlet), 65 | new RegExp(regExps.midletter), 66 | new RegExp(regExps.midnum), 67 | new RegExp(regExps.numeric), 68 | new RegExp(regExps.cr), 69 | new RegExp(regExps.lf), 70 | new RegExp(regExps.newline), 71 | new RegExp(regExps.extend), 72 | new RegExp(regExps.format), 73 | new RegExp(regExps.katakana), 74 | new RegExp(regExps.extendnumlet), 75 | new RegExp('@') 76 | ]; 77 | const EMPTY_STRING$1 = ''; 78 | const PUNCTUATION$1 = new RegExp('^' + regExps.punctuation + '$'); 79 | const WHITESPACE$1 = /^\s+$/; 80 | 81 | const SETS = SETS$1; 82 | const OTHER = characterIndices.OTHER; 83 | const getType = char => { 84 | let type = OTHER; 85 | const setsLength = SETS.length; 86 | for (let j = 0; j < setsLength; ++j) { 87 | const set = SETS[j]; 88 | if (set && set.test(char)) { 89 | type = j; 90 | break; 91 | } 92 | } 93 | return type; 94 | }; 95 | const memoize = func => { 96 | const cache = {}; 97 | return char => { 98 | if (cache[char]) { 99 | return cache[char]; 100 | } else { 101 | const result = func(char); 102 | cache[char] = result; 103 | return result; 104 | } 105 | }; 106 | }; 107 | const classify = characters => { 108 | const memoized = memoize(getType); 109 | return map(characters, memoized); 110 | }; 111 | 112 | const isWordBoundary = (map, index) => { 113 | const type = map[index]; 114 | const nextType = map[index + 1]; 115 | if (index < 0 || index > map.length - 1 && index !== 0) { 116 | return false; 117 | } 118 | if (type === characterIndices.ALETTER && nextType === characterIndices.ALETTER) { 119 | return false; 120 | } 121 | const nextNextType = map[index + 2]; 122 | if (type === characterIndices.ALETTER && (nextType === characterIndices.MIDLETTER || nextType === characterIndices.MIDNUMLET || nextType === characterIndices.AT) && nextNextType === characterIndices.ALETTER) { 123 | return false; 124 | } 125 | const prevType = map[index - 1]; 126 | if ((type === characterIndices.MIDLETTER || type === characterIndices.MIDNUMLET || nextType === characterIndices.AT) && nextType === characterIndices.ALETTER && prevType === characterIndices.ALETTER) { 127 | return false; 128 | } 129 | if ((type === characterIndices.NUMERIC || type === characterIndices.ALETTER) && (nextType === characterIndices.NUMERIC || nextType === characterIndices.ALETTER)) { 130 | return false; 131 | } 132 | if ((type === characterIndices.MIDNUM || type === characterIndices.MIDNUMLET) && nextType === characterIndices.NUMERIC && prevType === characterIndices.NUMERIC) { 133 | return false; 134 | } 135 | if (type === characterIndices.NUMERIC && (nextType === characterIndices.MIDNUM || nextType === characterIndices.MIDNUMLET) && nextNextType === characterIndices.NUMERIC) { 136 | return false; 137 | } 138 | if (type === characterIndices.EXTEND || type === characterIndices.FORMAT || prevType === characterIndices.EXTEND || prevType === characterIndices.FORMAT || nextType === characterIndices.EXTEND || nextType === characterIndices.FORMAT) { 139 | return false; 140 | } 141 | if (type === characterIndices.CR && nextType === characterIndices.LF) { 142 | return false; 143 | } 144 | if (type === characterIndices.NEWLINE || type === characterIndices.CR || type === characterIndices.LF) { 145 | return true; 146 | } 147 | if (nextType === characterIndices.NEWLINE || nextType === characterIndices.CR || nextType === characterIndices.LF) { 148 | return true; 149 | } 150 | if (type === characterIndices.KATAKANA && nextType === characterIndices.KATAKANA) { 151 | return false; 152 | } 153 | if (nextType === characterIndices.EXTENDNUMLET && (type === characterIndices.ALETTER || type === characterIndices.NUMERIC || type === characterIndices.KATAKANA || type === characterIndices.EXTENDNUMLET)) { 154 | return false; 155 | } 156 | if (type === characterIndices.EXTENDNUMLET && (nextType === characterIndices.ALETTER || nextType === characterIndices.NUMERIC || nextType === characterIndices.KATAKANA)) { 157 | return false; 158 | } 159 | if (type === characterIndices.AT) { 160 | return false; 161 | } 162 | return true; 163 | }; 164 | 165 | const EMPTY_STRING = EMPTY_STRING$1; 166 | const WHITESPACE = WHITESPACE$1; 167 | const PUNCTUATION = PUNCTUATION$1; 168 | const isProtocol = str => str === 'http' || str === 'https'; 169 | const findWordEnd = (characters, startIndex) => { 170 | let i; 171 | for (i = startIndex; i < characters.length; i++) { 172 | if (WHITESPACE.test(characters[i])) { 173 | break; 174 | } 175 | } 176 | return i; 177 | }; 178 | const findUrlEnd = (characters, startIndex) => { 179 | const endIndex = findWordEnd(characters, startIndex + 1); 180 | const peakedWord = characters.slice(startIndex + 1, endIndex).join(EMPTY_STRING); 181 | return peakedWord.substr(0, 3) === '://' ? endIndex : startIndex; 182 | }; 183 | const findWords = (chars, sChars, characterMap, options) => { 184 | const words = []; 185 | let word = []; 186 | for (let i = 0; i < characterMap.length; ++i) { 187 | word.push(chars[i]); 188 | if (isWordBoundary(characterMap, i)) { 189 | const ch = sChars[i]; 190 | if ((options.includeWhitespace || !WHITESPACE.test(ch)) && (options.includePunctuation || !PUNCTUATION.test(ch))) { 191 | const startOfWord = i - word.length + 1; 192 | const endOfWord = i + 1; 193 | const str = sChars.slice(startOfWord, endOfWord).join(EMPTY_STRING); 194 | if (isProtocol(str)) { 195 | const endOfUrl = findUrlEnd(sChars, i); 196 | const url = chars.slice(endOfWord, endOfUrl); 197 | Array.prototype.push.apply(word, url); 198 | i = endOfUrl; 199 | } 200 | words.push(word); 201 | } 202 | word = []; 203 | } 204 | } 205 | return words; 206 | }; 207 | const getDefaultOptions = () => ({ 208 | includeWhitespace: false, 209 | includePunctuation: false 210 | }); 211 | const getWords$1 = (chars, extract, options) => { 212 | options = { 213 | ...getDefaultOptions(), 214 | ...options 215 | }; 216 | const filteredChars = []; 217 | const extractedChars = []; 218 | for (let i = 0; i < chars.length; i++) { 219 | const ch = extract(chars[i]); 220 | if (ch !== zeroWidth) { 221 | filteredChars.push(chars[i]); 222 | extractedChars.push(ch); 223 | } 224 | } 225 | const characterMap = classify(extractedChars); 226 | return findWords(filteredChars, extractedChars, characterMap, options); 227 | }; 228 | 229 | const getWords = getWords$1; 230 | 231 | var global$1 = tinymce.util.Tools.resolve('tinymce.dom.TreeWalker'); 232 | 233 | const getText = (node, schema) => { 234 | const blockElements = schema.getBlockElements(); 235 | const voidElements = schema.getVoidElements(); 236 | const isNewline = node => blockElements[node.nodeName] || voidElements[node.nodeName]; 237 | const textBlocks = []; 238 | let txt = ''; 239 | const treeWalker = new global$1(node, node); 240 | let tempNode; 241 | while (tempNode = treeWalker.next()) { 242 | if (tempNode.nodeType === 3) { 243 | txt += removeZwsp$1(tempNode.data); 244 | } else if (isNewline(tempNode) && txt.length) { 245 | textBlocks.push(txt); 246 | txt = ''; 247 | } 248 | } 249 | if (txt.length) { 250 | textBlocks.push(txt); 251 | } 252 | return textBlocks; 253 | }; 254 | 255 | const removeZwsp = text => text.replace(/\u200B/g, ''); 256 | const strLen = str => str.replace(/[\uD800-\uDBFF][\uDC00-\uDFFF]/g, '_').length; 257 | const countWords = (node, schema) => { 258 | const text = removeZwsp(getText(node, schema).join('\n')); 259 | return getWords(text.split(''), identity).length; 260 | }; 261 | const countCharacters = (node, schema) => { 262 | const text = getText(node, schema).join(''); 263 | return strLen(text); 264 | }; 265 | const countCharactersWithoutSpaces = (node, schema) => { 266 | const text = getText(node, schema).join('').replace(/\s/g, ''); 267 | return strLen(text); 268 | }; 269 | 270 | const createBodyCounter = (editor, count) => () => count(editor.getBody(), editor.schema); 271 | const createSelectionCounter = (editor, count) => () => count(editor.selection.getRng().cloneContents(), editor.schema); 272 | const createBodyWordCounter = editor => createBodyCounter(editor, countWords); 273 | const get = editor => ({ 274 | body: { 275 | getWordCount: createBodyWordCounter(editor), 276 | getCharacterCount: createBodyCounter(editor, countCharacters), 277 | getCharacterCountWithoutSpaces: createBodyCounter(editor, countCharactersWithoutSpaces) 278 | }, 279 | selection: { 280 | getWordCount: createSelectionCounter(editor, countWords), 281 | getCharacterCount: createSelectionCounter(editor, countCharacters), 282 | getCharacterCountWithoutSpaces: createSelectionCounter(editor, countCharactersWithoutSpaces) 283 | }, 284 | getCount: createBodyWordCounter(editor) 285 | }); 286 | 287 | const open = (editor, api) => { 288 | editor.windowManager.open({ 289 | title: 'Word Count', 290 | body: { 291 | type: 'panel', 292 | items: [{ 293 | type: 'table', 294 | header: [ 295 | 'Count', 296 | 'Document', 297 | 'Selection' 298 | ], 299 | cells: [ 300 | [ 301 | 'Words', 302 | String(api.body.getWordCount()), 303 | String(api.selection.getWordCount()) 304 | ], 305 | [ 306 | 'Characters (no spaces)', 307 | String(api.body.getCharacterCountWithoutSpaces()), 308 | String(api.selection.getCharacterCountWithoutSpaces()) 309 | ], 310 | [ 311 | 'Characters', 312 | String(api.body.getCharacterCount()), 313 | String(api.selection.getCharacterCount()) 314 | ] 315 | ] 316 | }] 317 | }, 318 | buttons: [{ 319 | type: 'cancel', 320 | name: 'close', 321 | text: 'Close', 322 | primary: true 323 | }] 324 | }); 325 | }; 326 | 327 | const register$1 = (editor, api) => { 328 | editor.addCommand('mceWordCount', () => open(editor, api)); 329 | }; 330 | 331 | const first = (fn, rate) => { 332 | let timer = null; 333 | const cancel = () => { 334 | if (!isNull(timer)) { 335 | clearTimeout(timer); 336 | timer = null; 337 | } 338 | }; 339 | const throttle = (...args) => { 340 | if (isNull(timer)) { 341 | timer = setTimeout(() => { 342 | timer = null; 343 | fn.apply(null, args); 344 | }, rate); 345 | } 346 | }; 347 | return { 348 | cancel, 349 | throttle 350 | }; 351 | }; 352 | 353 | var global = tinymce.util.Tools.resolve('tinymce.util.Delay'); 354 | 355 | const fireWordCountUpdate = (editor, api) => { 356 | editor.dispatch('wordCountUpdate', { 357 | wordCount: { 358 | words: api.body.getWordCount(), 359 | characters: api.body.getCharacterCount(), 360 | charactersWithoutSpaces: api.body.getCharacterCountWithoutSpaces() 361 | } 362 | }); 363 | }; 364 | 365 | const updateCount = (editor, api) => { 366 | fireWordCountUpdate(editor, api); 367 | }; 368 | const setup = (editor, api, delay) => { 369 | const debouncedUpdate = first(() => updateCount(editor, api), delay); 370 | editor.on('init', () => { 371 | updateCount(editor, api); 372 | global.setEditorTimeout(editor, () => { 373 | editor.on('SetContent BeforeAddUndo Undo Redo ViewUpdate keyup', debouncedUpdate.throttle); 374 | }, 0); 375 | editor.on('remove', debouncedUpdate.cancel); 376 | }); 377 | }; 378 | 379 | const register = editor => { 380 | const onAction = () => editor.execCommand('mceWordCount'); 381 | editor.ui.registry.addButton('wordcount', { 382 | tooltip: 'Word count', 383 | icon: 'character-count', 384 | onAction 385 | }); 386 | editor.ui.registry.addMenuItem('wordcount', { 387 | text: 'Word count', 388 | icon: 'character-count', 389 | onAction 390 | }); 391 | }; 392 | 393 | var Plugin = (delay = 300) => { 394 | global$2.add('wordcount', editor => { 395 | const api = get(editor); 396 | register$1(editor, api); 397 | register(editor); 398 | setup(editor, api, delay); 399 | return api; 400 | }); 401 | }; 402 | 403 | Plugin(); 404 | 405 | })(); 406 | -------------------------------------------------------------------------------- /src/libs/typo/dictionaries/en_US/en_US.aff: -------------------------------------------------------------------------------- 1 | SET UTF-8 2 | TRY esianrtolcdugmphbyfvkwzESIANRTOLCDUGMPHBYFVKWZ' 3 | ICONV 1 4 | ICONV ’ ' 5 | NOSUGGEST ! 6 | 7 | # ordinal numbers 8 | COMPOUNDMIN 1 9 | # only in compounds: 1th, 2th, 3th 10 | ONLYINCOMPOUND c 11 | # compound rules: 12 | # 1. [0-9]*1[0-9]th (10th, 11th, 12th, 56714th, etc.) 13 | # 2. [0-9]*[02-9](1st|2nd|3rd|[4-9]th) (21st, 22nd, 123rd, 1234th, etc.) 14 | COMPOUNDRULE 2 15 | COMPOUNDRULE n*1t 16 | COMPOUNDRULE n*mp 17 | WORDCHARS 0123456789 18 | 19 | PFX A Y 1 20 | PFX A 0 re . 21 | 22 | PFX I Y 1 23 | PFX I 0 in . 24 | 25 | PFX U Y 1 26 | PFX U 0 un . 27 | 28 | PFX C Y 1 29 | PFX C 0 de . 30 | 31 | PFX E Y 1 32 | PFX E 0 dis . 33 | 34 | PFX F Y 1 35 | PFX F 0 con . 36 | 37 | PFX K Y 1 38 | PFX K 0 pro . 39 | 40 | SFX V N 2 41 | SFX V e ive e 42 | SFX V 0 ive [^e] 43 | 44 | SFX N Y 3 45 | SFX N e ion e 46 | SFX N y ication y 47 | SFX N 0 en [^ey] 48 | 49 | SFX X Y 3 50 | SFX X e ions e 51 | SFX X y ications y 52 | SFX X 0 ens [^ey] 53 | 54 | SFX H N 2 55 | SFX H y ieth y 56 | SFX H 0 th [^y] 57 | 58 | SFX Y Y 1 59 | SFX Y 0 ly . 60 | 61 | SFX G Y 2 62 | SFX G e ing e 63 | SFX G 0 ing [^e] 64 | 65 | SFX J Y 2 66 | SFX J e ings e 67 | SFX J 0 ings [^e] 68 | 69 | SFX D Y 4 70 | SFX D 0 d e 71 | SFX D y ied [^aeiou]y 72 | SFX D 0 ed [^ey] 73 | SFX D 0 ed [aeiou]y 74 | 75 | SFX T N 4 76 | SFX T 0 st e 77 | SFX T y iest [^aeiou]y 78 | SFX T 0 est [aeiou]y 79 | SFX T 0 est [^ey] 80 | 81 | SFX R Y 4 82 | SFX R 0 r e 83 | SFX R y ier [^aeiou]y 84 | SFX R 0 er [aeiou]y 85 | SFX R 0 er [^ey] 86 | 87 | SFX Z Y 4 88 | SFX Z 0 rs e 89 | SFX Z y iers [^aeiou]y 90 | SFX Z 0 ers [aeiou]y 91 | SFX Z 0 ers [^ey] 92 | 93 | SFX S Y 4 94 | SFX S y ies [^aeiou]y 95 | SFX S 0 s [aeiou]y 96 | SFX S 0 es [sxzh] 97 | SFX S 0 s [^sxzhy] 98 | 99 | SFX P Y 3 100 | SFX P y iness [^aeiou]y 101 | SFX P 0 ness [aeiou]y 102 | SFX P 0 ness [^y] 103 | 104 | SFX M Y 1 105 | SFX M 0 's . 106 | 107 | SFX B Y 3 108 | SFX B 0 able [^aeiou] 109 | SFX B 0 able ee 110 | SFX B e able [^aeiou]e 111 | 112 | SFX L Y 1 113 | SFX L 0 ment . 114 | 115 | REP 90 116 | REP a ei 117 | REP ei a 118 | REP a ey 119 | REP ey a 120 | REP ai ie 121 | REP ie ai 122 | REP alot a_lot 123 | REP are air 124 | REP are ear 125 | REP are eir 126 | REP air are 127 | REP air ere 128 | REP ere air 129 | REP ere ear 130 | REP ere eir 131 | REP ear are 132 | REP ear air 133 | REP ear ere 134 | REP eir are 135 | REP eir ere 136 | REP ch te 137 | REP te ch 138 | REP ch ti 139 | REP ti ch 140 | REP ch tu 141 | REP tu ch 142 | REP ch s 143 | REP s ch 144 | REP ch k 145 | REP k ch 146 | REP f ph 147 | REP ph f 148 | REP gh f 149 | REP f gh 150 | REP i igh 151 | REP igh i 152 | REP i uy 153 | REP uy i 154 | REP i ee 155 | REP ee i 156 | REP j di 157 | REP di j 158 | REP j gg 159 | REP gg j 160 | REP j ge 161 | REP ge j 162 | REP s ti 163 | REP ti s 164 | REP s ci 165 | REP ci s 166 | REP k cc 167 | REP cc k 168 | REP k qu 169 | REP qu k 170 | REP kw qu 171 | REP o eau 172 | REP eau o 173 | REP o ew 174 | REP ew o 175 | REP oo ew 176 | REP ew oo 177 | REP ew ui 178 | REP ui ew 179 | REP oo ui 180 | REP ui oo 181 | REP ew u 182 | REP u ew 183 | REP oo u 184 | REP u oo 185 | REP u oe 186 | REP oe u 187 | REP u ieu 188 | REP ieu u 189 | REP ue ew 190 | REP ew ue 191 | REP uff ough 192 | REP oo ieu 193 | REP ieu oo 194 | REP ier ear 195 | REP ear ier 196 | REP ear air 197 | REP air ear 198 | REP w qu 199 | REP qu w 200 | REP z ss 201 | REP ss z 202 | REP shun tion 203 | REP shun sion 204 | REP shun cion 205 | REP size cise 206 | -------------------------------------------------------------------------------- /src/load-sw.js: -------------------------------------------------------------------------------- 1 | 2 | var buildDateTime = '${buildDateTime}'; 3 | 4 | if ('serviceWorker' in navigator) { 5 | 6 | navigator.serviceWorker.register('/editor/service-worker.js'); 7 | 8 | var refreshing = false; 9 | navigator.serviceWorker.addEventListener('controllerchange', function() { 10 | console.log('reloading'); 11 | if (refreshing) { 12 | return; 13 | } 14 | refreshing = true; 15 | window.location.reload(); 16 | }); 17 | 18 | navigator.serviceWorker.ready.then(function(){ 19 | console.log('ready'); 20 | }); 21 | 22 | async function detectSWUpdate() { 23 | 24 | const registration = await navigator.serviceWorker.ready; 25 | 26 | registration.addEventListener('updatefound', event => { 27 | tinymce.activeEditor.notificationManager.open({ 28 | text: 'New version found. Updating...', 29 | type: 'info' 30 | }); 31 | tinymce.activeEditor.getBody().hidden = true; 32 | }); 33 | } 34 | 35 | detectSWUpdate(); 36 | 37 | } 38 | 39 | function postMessage(message){ 40 | if(navigator.serviceWorker.controller){ 41 | navigator.serviceWorker.controller.postMessage(message); 42 | } else { 43 | console.log('no controller'); 44 | } 45 | } -------------------------------------------------------------------------------- /src/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Hypertext Editor", 3 | "short_name": "Hypertext", 4 | "description": "HTML Document Editor", 5 | "id": "/index.html", 6 | "scope": "/editor/", 7 | "start_url": "./", 8 | "display": "fullscreen", 9 | "theme_color": "#020887", 10 | "background_color": "#FDFDFD", 11 | "icons": [ 12 | { 13 | "src": "./assets/hypertext-app-icon192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "./assets/hypertext-app-icon512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "./assets/hypertext-app-icon.svg", 24 | "type": "image/svg", 25 | "purpose": "any" 26 | } 27 | ], 28 | "screenshots" : [ 29 | { 30 | "src": "./assets/hypertext-screenshot.png", 31 | "sizes": "800x629", 32 | "type": "image/png", 33 | "platform": "wide", 34 | "label": "Hypertext HTML Document Editor" 35 | }], 36 | "shortcuts" : [ 37 | { 38 | "name": "Edit", 39 | "url": ".", 40 | "description": "Edit HTML Document" 41 | }] 42 | , 43 | "file_handlers": [ 44 | { 45 | "action": "./index.html", 46 | "accept": { 47 | "text/html": [".html",".htmd"] 48 | }, 49 | "launch_type": "single-client" 50 | } 51 | ], 52 | "protocol_handlers": [ 53 | { 54 | "protocol": "web+htmld", 55 | "url": "./index.html?file=%s" 56 | } 57 | ], 58 | "url_handlers": [ 59 | {"origin": "https://www.hypertext.plus"} 60 | ], 61 | "display_override": ["window-controls-overlay", "fullscreen", "standalone", "minimal-ui"], 62 | "dir": "auto", 63 | "lang": "en", 64 | "orientation": "any", 65 | "categories": ["productivity"] 66 | } 67 | -------------------------------------------------------------------------------- /src/scripts/editor-document.js: -------------------------------------------------------------------------------- 1 | /* *********************** */ 2 | 3 | var currentDocument; 4 | 5 | /* *********************** */ 6 | 7 | async function getHTMLTemplate() { 8 | let htmlText = ` 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | `; 19 | 20 | return htmlText; 21 | } 22 | 23 | /* *********************** */ 24 | 25 | function getJsonld(headEl) { 26 | let jsonld = { 27 | '@context': 'https://schema.org', 28 | '@type': 'DigitalDocument', 29 | name: '', 30 | description: '', 31 | author: '', 32 | dateCreated: '', 33 | dateModified: '', 34 | }; 35 | 36 | if (!headEl) { 37 | return jsonld; 38 | } 39 | 40 | let jsonEl = headEl.querySelector('script[type="application/ld+json"]'); 41 | 42 | if (jsonEl) { 43 | try { 44 | let docJsonld = JSON.parse(jsonEl.textContent); 45 | Object.assign(jsonld, docJsonld); 46 | } catch (e) {} 47 | } 48 | 49 | let titleEl = headEl.querySelector('title'); 50 | let title = ''; 51 | if (titleEl) { 52 | title = titleEl.textContent; 53 | } 54 | 55 | if (jsonld.title == '' && title !== '') { 56 | jsonld.title = title; 57 | } 58 | 59 | return jsonld; 60 | } 61 | 62 | /* *********************** */ 63 | 64 | function setJsonld(headEl, jsonld) { 65 | let textContent = ''; 66 | 67 | try { 68 | textContent = JSON.stringify(jsonld, null, ' '); 69 | } catch (e) { 70 | textContent = JSON.stringify(getJsonld(), null, ' '); 71 | } 72 | 73 | let jsonEl = headEl.querySelector('script[type="application/ld+json"]'); 74 | 75 | if (!jsonEl) { 76 | jsonEl = headEl.ownerDocument.createElement('script'); 77 | jsonEl.setAttribute('type', 'application/ld+json'); 78 | headEl.append(jsonEl); 79 | } 80 | 81 | jsonEl.textContent = '\n' + textContent + '\n'; 82 | } 83 | 84 | /* *********************** */ 85 | 86 | function confirmDocumentChange() { 87 | tinyMCE.get('textEditor').getBody().focus(); 88 | 89 | if (tinymce.activeEditor.isDirty()) { 90 | return confirm('Unsaved changes. Continue without saving?'); 91 | } else { 92 | return true; 93 | } 94 | } 95 | 96 | /* *********************** */ 97 | 98 | async function newDocument() { 99 | htmlText = await getHTMLTemplate(); 100 | 101 | return loadDocument(htmlText); 102 | } 103 | 104 | /* *********************** */ 105 | 106 | async function loadDocument(htmlText) { 107 | const parser = new DOMParser(); 108 | 109 | currentDocument = parser.parseFromString(htmlText, 'text/html'); 110 | 111 | let headEl = currentDocument.querySelector('head'); 112 | 113 | let cssText = ''; 114 | 115 | let stylesEl = headEl.querySelectorAll('style'); 116 | 117 | if (stylesEl.length > 0) { 118 | stylesEl.forEach(function(styleEl){ 119 | cssText = cssText + styleEl.textContent + '\n'; 120 | stylesEl.forEach(function(el){ 121 | el.remove(); 122 | }); 123 | }); 124 | } else { 125 | cssText = await getDefaultCSSText(); 126 | } 127 | 128 | let styleEl = currentDocument.createElement('style'); 129 | styleEl.textContent = cssText; 130 | headEl.append(styleEl); 131 | 132 | let currentStyleEl = tinymce.activeEditor.getDoc().querySelector('#mceDefaultStyles'); 133 | 134 | if(currentStyleEl){ 135 | currentStyleEl.textContent = ''; 136 | } 137 | 138 | tinymce.activeEditor.dom.addStyle(cssText); 139 | 140 | let linksEl = headEl.querySelectorAll('link[rel="stylesheet"]'); 141 | 142 | linksEl.forEach(function (linkEl) { 143 | let href = linkEl.getAttribute('href'); 144 | if(href.startsWith('http:')){ 145 | tinymce.activeEditor.dom.loadCSS(href); 146 | } 147 | }); 148 | 149 | let jsonld = getJsonld(headEl); 150 | 151 | if(jsonld.dateCreated == ''){ 152 | jsonld.dateCreated = new Date().toISOString(); 153 | jsonld.dateModified = new Date().toISOString(); 154 | } 155 | 156 | let author = localStorage.getItem('author'); 157 | if (author !== null) { 158 | jsonld.author = author; 159 | } 160 | 161 | setJsonld(headEl, jsonld); 162 | 163 | let bodyText = currentDocument.body.innerHTML; 164 | 165 | tinymce.activeEditor.resetContent(bodyText); 166 | 167 | tinymce.activeEditor.undoManager.clear(); 168 | tinymce.activeEditor.setDirty(false); 169 | 170 | tinymce.activeEditor.getWin().scroll(0,0); 171 | } 172 | 173 | /* *********************** */ 174 | 175 | function getDocumentHTMLToSave() { 176 | 177 | let headEl = currentDocument.querySelector('head'); 178 | 179 | let jsonld = getJsonld(headEl); 180 | 181 | jsonld.dateModified = new Date().toISOString(); 182 | 183 | let title = getTitle(); 184 | 185 | jsonld.name = title; 186 | 187 | setJsonld(headEl, jsonld); 188 | 189 | let content = tinymce.activeEditor.getContent({ format: 'html' }); 190 | 191 | currentDocument.body.innerHTML = content; 192 | 193 | let htmlText = '\n' + currentDocument.documentElement.outerHTML; 194 | 195 | htmlText = beautify(htmlText); 196 | 197 | return htmlText; 198 | } 199 | 200 | /* *********************** */ 201 | 202 | function getUnformattedHTML(){ 203 | 204 | let newDoc = document.implementation.createHTMLDocument(); 205 | newDoc.body.innerHTML = tinymce.activeEditor.getContent({ format: 'html' }); 206 | 207 | let htmlText = newDoc.documentElement.outerHTML; 208 | 209 | return htmlText; 210 | 211 | } 212 | 213 | 214 | /* *********************** */ 215 | 216 | async function getDefaultCSSText(){ 217 | 218 | let defaultCSS = localStorage.getItem('defaultCSS'); 219 | 220 | if(defaultCSS){ 221 | cssText = defaultCSS; 222 | } else { 223 | let response = await fetch('./css/html-document-v1.css'); 224 | if (response.ok) { 225 | cssText = await response.text(); 226 | } 227 | } 228 | 229 | return cssText; 230 | 231 | } 232 | 233 | /* *********************** */ 234 | 235 | function getCSSText(){ 236 | let styleEl = currentDocument.querySelector('head style'); 237 | if(styleEl){ 238 | return styleEl.textContent; 239 | } else { 240 | return ''; 241 | } 242 | } 243 | 244 | /* *********************** */ 245 | 246 | function setCSSText(cssText){ 247 | let styleEl = currentDocument.querySelector('head style'); 248 | if(styleEl && cssText && cssText !== ''){ 249 | 250 | styleEl.textContent = cssText.trim(); 251 | 252 | tinymce.activeEditor.getDoc().querySelector('#mceDefaultStyles').textContent = cssText.trim(); 253 | 254 | } 255 | } 256 | 257 | /* *********************** */ 258 | 259 | function setCSSDefault(){ 260 | 261 | tinymce.activeEditor.windowManager.confirm('Use this document\'s CSS for new documents?', (state) => { 262 | if(state){ 263 | let cssText = getCSSText(); 264 | localStorage.setItem('defaultCSS', cssText); 265 | } 266 | }); 267 | 268 | } 269 | 270 | /* *********************** */ 271 | 272 | async function resetCSSDefault(){ 273 | tinymce.activeEditor.windowManager.confirm('Reset default CSS used for new documents?', (state) => { 274 | if(state){ 275 | localStorage.removeItem('defaultCSS'); 276 | } 277 | }); 278 | } 279 | 280 | /* *********************** */ 281 | 282 | function getTitle(){ 283 | 284 | let title = ''; 285 | 286 | let titleEl = currentDocument.querySelector('title'); 287 | 288 | if (titleEl) { 289 | if (titleEl.textContent == '') { 290 | let h1El = tinymce.activeEditor.getDoc().querySelector('h1'); 291 | if (h1El) { 292 | title = h1El.textContent.trim(); 293 | titleEl.textContent = title; 294 | } 295 | } else { 296 | title = titleEl.textContent; 297 | } 298 | } 299 | 300 | return title; 301 | 302 | } 303 | 304 | 305 | /* *********************** */ 306 | 307 | function getSuggestedFileName(){ 308 | 309 | let filename = ''; 310 | 311 | let title = getTitle(); 312 | if(title !== ''){ 313 | filename = title.trim().replaceAll(' ','-').replace(/[^a-z0-9-]/gi, '').replace(/-+/gi,'-').toLowerCase(); 314 | } else { 315 | filename = getDateTimeText(); 316 | } 317 | 318 | return filename + '.html'; 319 | 320 | 321 | } 322 | 323 | 324 | /* *********************** */ 325 | 326 | function getDateTimeText(){ 327 | 328 | let dt = new Date(); 329 | 330 | let y = dt.getFullYear(); 331 | let m = ((dt.getMonth() + 1) + '').padStart(2, '0'); 332 | let d = (dt.getDate() + '').padStart(2, '0'); 333 | let h = (dt.getHours() + '').padStart(2, '0'); 334 | let t = (dt.getMinutes() + '').padStart(2, '0'); 335 | 336 | let dateTime = y + '-' + m + '-' + d+ '-' + h + '-' + t; 337 | 338 | return dateTime; 339 | 340 | } 341 | 342 | -------------------------------------------------------------------------------- /src/scripts/editor-file-handling.js: -------------------------------------------------------------------------------- 1 | /* *********************** */ 2 | 3 | var currentHTMLFileHandle = null; 4 | var toOpenHTMLFileHandle = null; 5 | var dirHandle = null; 6 | 7 | var fileSystemSupport = 'showOpenFilePicker' in window; 8 | 9 | /* *********************** */ 10 | 11 | function getFileSystemSupport(){ 12 | return fileSystemSupport; 13 | } 14 | 15 | /* *********************** */ 16 | 17 | async function deleteWorkingFolder(){ 18 | dirHandle = null; 19 | await del('dirHandle'); 20 | } 21 | 22 | /* *********************** */ 23 | 24 | async function getWorkingFolder(){ 25 | 26 | dirHandle = await get('dirHandle'); 27 | 28 | if(dirHandle){ 29 | return dirHandle.name; 30 | } else { 31 | return ''; 32 | } 33 | 34 | } 35 | 36 | /* *********************** */ 37 | 38 | async function verifyWorkingFolder(){ 39 | 40 | dirHandle = await get('dirHandle'); 41 | 42 | if(dirHandle){ 43 | 44 | if ((await verifyPermission(dirHandle, true)) === false) { 45 | console.error(`User did not grant permission to '${dirHandle.name}'`); 46 | await del('dirHandle'); 47 | } 48 | 49 | } 50 | 51 | } 52 | 53 | 54 | 55 | /* *********************** */ 56 | 57 | async function setWorkingFolder(){ 58 | 59 | try { 60 | dirHandle = await window.showDirectoryPicker({mode: 'readwrite'}); 61 | 62 | await set('dirHandle', dirHandle); 63 | 64 | } catch(err){ 65 | console.log(err); 66 | } 67 | } 68 | 69 | /* *********************** */ 70 | 71 | async function getFileList(dir, handles){ 72 | 73 | if(!handles){ 74 | handles = {}; 75 | } 76 | 77 | for await (const entry of dir.values()) { 78 | 79 | if(entry.name.startsWith('.') == true){ 80 | continue; 81 | } 82 | 83 | if (entry.kind !== 'file') { 84 | handles = await getFileList(entry, handles); 85 | continue; 86 | } 87 | 88 | let filepath = entry.name; 89 | let relativePaths = await dirHandle.resolve(entry); 90 | if (relativePaths !== null) { 91 | filepath = relativePaths.join('/'); 92 | } 93 | 94 | handles[filepath] = entry; 95 | 96 | } 97 | 98 | return handles; 99 | 100 | } 101 | 102 | /* *********************** */ 103 | 104 | async function getLocalFile(filename){ 105 | 106 | dirHandle = await get('dirHandle'); 107 | 108 | if(dirHandle){ 109 | let fileHandles = await getFileList(dirHandle); 110 | 111 | let filepath = null; 112 | 113 | let paths = Object.keys(fileHandles); 114 | 115 | for(let i = 0; i < paths.length; i++){ 116 | let path = paths[i]; 117 | if(path.includes(filename)){ 118 | filepath = path; 119 | break; 120 | } 121 | } 122 | 123 | if(filepath){ 124 | 125 | let fileHandle = fileHandles[filepath]; 126 | let file = await fileHandle.getFile(); 127 | 128 | return { 129 | filepath: filepath, 130 | file: file 131 | }; 132 | } 133 | } 134 | 135 | return null; 136 | 137 | } 138 | 139 | /* *********************** */ 140 | 141 | async function getImage(){ 142 | 143 | dirHandle = await get('dirHandle'); 144 | 145 | try { 146 | const opts = { 147 | startIn: dirHandle, 148 | types: [ 149 | { 150 | description: 'Images', 151 | accept: { 152 | 'image/*': ['.png', '.gif', '.jpeg', '.jpg'] 153 | } 154 | }, 155 | ], 156 | excludeAcceptAllOption: true, 157 | }; 158 | 159 | let fileHandle; 160 | 161 | [fileHandle] = await window.showOpenFilePicker(opts); 162 | 163 | 164 | let filename = fileHandle.name; 165 | let relativePaths = await dirHandle.resolve(fileHandle); 166 | if (relativePaths !== null) { 167 | filename = relativePaths.join('/'); 168 | } 169 | 170 | return filename; 171 | 172 | } catch(err){ 173 | console.err(err); 174 | } 175 | } 176 | 177 | /* *********************** */ 178 | 179 | function newHTMLFile() { 180 | currentHTMLFileHandle = null; 181 | newDocument(); 182 | updateWindowTitle(''); 183 | } 184 | 185 | /* *********************** */ 186 | 187 | async function openHTMLFile(fileHandle) { 188 | 189 | if(fileSystemSupport == false){ 190 | openHTMLFileFromInput(); 191 | return; 192 | } 193 | 194 | if (fileHandle) { 195 | if ((await verifyPermission(fileHandle, true)) === false) { 196 | console.error(`User did not grant permission to '${fileHandle.name}'`); 197 | return; 198 | } 199 | } else { 200 | try { 201 | 202 | const opts = { 203 | types: [ 204 | { 205 | description: 'HTML file', 206 | accept: { 'text/html': ['.html', '.htm', '.htmd'] }, 207 | }, 208 | ], 209 | excludeAcceptAllOption: true, 210 | }; 211 | 212 | 213 | [fileHandle] = await window.showOpenFilePicker(opts); 214 | } catch (ex) { 215 | if (ex.name === 'AbortError') { 216 | return; 217 | } 218 | const msg = 'An error occured trying to open the file.'; 219 | console.error(msg, ex); 220 | alert(msg); 221 | } 222 | } 223 | 224 | if (!fileHandle) { 225 | return; 226 | } 227 | 228 | const file = await fileHandle.getFile(); 229 | 230 | try { 231 | let htmlText = await file.text(); 232 | 233 | loadDocument(htmlText); 234 | 235 | currentHTMLFileHandle = fileHandle; 236 | 237 | updateWindowTitle(currentHTMLFileHandle.name); 238 | } catch (ex) { 239 | const msg = `An error occured reading ${currentHTMLFileHandle.name}`; 240 | console.error(msg, ex); 241 | alert(msg); 242 | } 243 | } 244 | 245 | /* *********************** */ 246 | 247 | async function verifyPermission(fileHandle, withWrite) { 248 | const opts = {}; 249 | if (withWrite) { 250 | opts.writable = true; 251 | opts.mode = 'readwrite'; 252 | } 253 | if ((await fileHandle.queryPermission(opts)) === 'granted') { 254 | return true; 255 | } 256 | if ((await fileHandle.requestPermission(opts)) === 'granted') { 257 | return true; 258 | } 259 | return false; 260 | } 261 | 262 | /* *********************** */ 263 | 264 | async function saveHTMLFile() { 265 | 266 | if(fileSystemSupport == false){ 267 | saveHTMLFileAsDownload(currentHTMLFileHandle); 268 | return; 269 | } 270 | 271 | try { 272 | if (!currentHTMLFileHandle) { 273 | return await saveHTMLFileAs(); 274 | } 275 | 276 | if ((await verifyPermission(currentHTMLFileHandle, true)) === false) { 277 | console.error(`User did not grant permission to '${currentHTMLFileHandle.name}'`); 278 | return; 279 | } 280 | 281 | let htmlText = getDocumentHTMLToSave(); 282 | 283 | await writeHTMLFile(currentHTMLFileHandle, htmlText); 284 | 285 | documentSaved(currentHTMLFileHandle.name); 286 | } catch (ex) { 287 | const msg = 'Unable to save file'; 288 | console.error(msg, ex); 289 | alert(msg); 290 | } 291 | } 292 | 293 | /* *********************** */ 294 | 295 | async function saveHTMLFileAs() { 296 | 297 | if(fileSystemSupport == false){ 298 | saveHTMLFileAsDownload(); 299 | return; 300 | } 301 | 302 | let fileHandle; 303 | try { 304 | fileHandle = await getNewHTMLFileHandle(); 305 | } catch (ex) { 306 | if (ex.name === 'AbortError') { 307 | return; 308 | } 309 | const msg = 'An error occured trying to open the file.'; 310 | console.error(msg, ex); 311 | alert(msg); 312 | return; 313 | } 314 | 315 | try { 316 | let htmlText = getDocumentHTMLToSave(); 317 | 318 | await writeHTMLFile(fileHandle, htmlText); 319 | 320 | currentHTMLFileHandle = fileHandle; 321 | 322 | documentSaved(currentHTMLFileHandle.name); 323 | } catch (ex) { 324 | const msg = 'Unable to save file.'; 325 | console.error(msg, ex); 326 | alert(msg); 327 | return; 328 | } 329 | } 330 | 331 | /* *********************** */ 332 | 333 | async function writeHTMLFile(fileHandle, contents) { 334 | const writable = await fileHandle.createWritable(); 335 | await writable.write(contents); 336 | await writable.close(); 337 | } 338 | 339 | /* *********************** */ 340 | 341 | function getNewHTMLFileHandle() { 342 | 343 | if (currentHTMLFileHandle) { 344 | var saveAsHTMLFileHandle = currentHTMLFileHandle; 345 | } else { 346 | var saveAsHTMLFileHandle = 'desktop'; 347 | } 348 | const opts = { 349 | suggestedName: getSuggestedFileName(), 350 | startIn: saveAsHTMLFileHandle, 351 | types: [ 352 | { 353 | description: 'HTML file', 354 | accept: { 'text/html': ['.html', '.htm', '.htmd'] }, 355 | }, 356 | ], 357 | excludeAcceptAllOption: true, 358 | }; 359 | return window.showSaveFilePicker(opts); 360 | } 361 | 362 | /* *********************** */ 363 | 364 | if ('launchQueue' in window && 'files' in LaunchParams.prototype) { 365 | launchQueue.setConsumer((launchParams) => { 366 | if (!launchParams.files.length) { 367 | return; 368 | } 369 | for (const fileHandle of launchParams.files) { 370 | toOpenHTMLFileHandle = fileHandle; 371 | } 372 | }); 373 | } 374 | 375 | /* *********************** */ 376 | 377 | function documentSaved(filename) { 378 | tinymce.activeEditor.setDirty(false); 379 | 380 | updateWindowTitle(filename); 381 | 382 | tinymce.activeEditor.undoManager.clear(); 383 | tinymce.activeEditor.setDirty(false); 384 | } 385 | 386 | /* *********************** */ 387 | 388 | function updateWindowTitle(filename) { 389 | 390 | if(!filename){ 391 | filename = ''; 392 | } 393 | 394 | let windowTitleEl = document.querySelector('.window-title'); 395 | 396 | if (!windowTitleEl) { 397 | let bottomBar = document.querySelector('.tox-statusbar__text-container'); 398 | windowTitleEl = document.createElement('span'); 399 | windowTitleEl.setAttribute('class', 'window-title'); 400 | 401 | let wordcountEl = document.querySelector('.tox-statusbar__wordcount'); 402 | if(wordcountEl){ 403 | wordcountEl.before(windowTitleEl); 404 | } else { 405 | bottomBar.append(windowTitleEl); 406 | } 407 | 408 | 409 | } 410 | 411 | if (tinymce.activeEditor.isDirty()) { 412 | windowTitleEl.setAttribute('style', 'font-style: italic; margin: auto;'); 413 | } else { 414 | windowTitleEl.setAttribute('style', 'margin: auto;'); 415 | } 416 | 417 | document.title = filename; 418 | windowTitleEl.textContent = filename; 419 | } 420 | 421 | 422 | /* *********************** */ 423 | 424 | function openHTMLFileFromInput(){ 425 | let input = document.createElement('input'); 426 | input.setAttribute('type', 'file'); 427 | input.setAttribute('accept', 'text/html'); 428 | 429 | input.addEventListener('change', function(e){ 430 | let file = e.target.files[0]; 431 | 432 | let reader = new FileReader(); 433 | 434 | reader.addEventListener('loadend', function(e){ 435 | 436 | let htmlText = e.srcElement.result; 437 | 438 | loadDocument(htmlText); 439 | 440 | currentHTMLFileHandle = file; 441 | 442 | updateWindowTitle(currentHTMLFileHandle.name); 443 | 444 | }); 445 | 446 | reader.readAsText(file); 447 | }); 448 | 449 | input.click(); 450 | 451 | } 452 | 453 | /* *********************** */ 454 | 455 | function saveHTMLFileAsDownload(currentHTMLFileHandle){ 456 | 457 | let filename = getSuggestedFileName(); 458 | 459 | if(currentHTMLFileHandle){ 460 | filename = currentHTMLFileHandle.name; 461 | } else { 462 | filename = window.prompt('Save As...', filename); 463 | } 464 | 465 | if(filename == null){ 466 | return; 467 | } 468 | 469 | let htmlText = getDocumentHTMLToSave(); 470 | 471 | let file = new File([htmlText], '', {type: 'text/html'}); 472 | 473 | let anchor = document.createElement('a'); 474 | anchor.href = window.URL.createObjectURL(file); 475 | anchor.setAttribute('download', filename); 476 | 477 | anchor.click(); 478 | 479 | } 480 | 481 | /* *********************** */ 482 | /* *********************** */ 483 | 484 | async function importCSSFile(){ 485 | 486 | if(fileSystemSupport == false){ 487 | importCSSFileFromInput(); 488 | return; 489 | } 490 | 491 | let fileHandle; 492 | 493 | try { 494 | 495 | const opts = { 496 | types: [ 497 | { 498 | description: 'CSS file', 499 | accept: { 'text/css': ['.css'] }, 500 | }, 501 | ], 502 | excludeAcceptAllOption: true, 503 | }; 504 | 505 | 506 | [fileHandle] = await window.showOpenFilePicker(opts); 507 | } catch (ex) { 508 | if (ex.name === 'AbortError') { 509 | return; 510 | } 511 | const msg = 'An error occured trying to open the file.'; 512 | console.error(msg, ex); 513 | alert(msg); 514 | } 515 | 516 | if (!fileHandle) { 517 | return; 518 | } 519 | 520 | const file = await fileHandle.getFile(); 521 | 522 | try { 523 | let cssText = await file.text(); 524 | 525 | setCSSText(cssText); 526 | 527 | } catch (ex) { 528 | const msg = `An error occured reading ${currentHTMLFileHandle.name}`; 529 | console.error(msg, ex); 530 | alert(msg); 531 | } 532 | 533 | 534 | } 535 | 536 | /* *********************** */ 537 | 538 | async function exportCSSFile(){ 539 | 540 | let suggestedFileName = getSuggestedFileName().replace('.html') + '.css'; 541 | 542 | let saveAsHTMLFileHandle = 'desktop'; 543 | 544 | if (currentHTMLFileHandle) { 545 | 546 | saveAsHTMLFileHandle = currentHTMLFileHandle; 547 | 548 | let filename = currentHTMLFileHandle.name; 549 | if(filename){ 550 | filename = filename.substring(0, filename.lastIndexOf('.')) + '.css'; 551 | suggestedFileName = filename; 552 | } 553 | } 554 | 555 | if(fileSystemSupport == false){ 556 | exportCSSFileAsDownload(suggestedFileName); 557 | return; 558 | } 559 | 560 | const opts = { 561 | suggestedName: suggestedFileName, 562 | startIn: saveAsHTMLFileHandle, 563 | types: [ 564 | { 565 | description: 'CSS file', 566 | accept: { 'text/css': ['.css'] }, 567 | }, 568 | ], 569 | excludeAcceptAllOption: true, 570 | }; 571 | 572 | try { 573 | fileHandle = await window.showSaveFilePicker(opts); 574 | } catch (ex) { 575 | if (ex.name === 'AbortError') { 576 | return; 577 | } 578 | const msg = 'An error occured trying to open the file.'; 579 | console.error(msg, ex); 580 | alert(msg); 581 | return; 582 | } 583 | 584 | if ((await verifyPermission(fileHandle, true)) === false) { 585 | console.error(`User did not grant permission to '${fileHandle.name}'`); 586 | return; 587 | } 588 | 589 | let cssText = getCSSText(); 590 | 591 | const writable = await fileHandle.createWritable(); 592 | await writable.write(cssText); 593 | await writable.close(); 594 | 595 | } 596 | 597 | /* *********************** */ 598 | 599 | function importCSSFileFromInput(){ 600 | 601 | let input = document.createElement('input'); 602 | input.setAttribute('type', 'file'); 603 | input.setAttribute('accept', 'text/css'); 604 | 605 | input.addEventListener('change', function(e){ 606 | 607 | let file = e.target.files[0]; 608 | 609 | let reader = new FileReader(); 610 | 611 | reader.addEventListener('loadend', function(e){ 612 | 613 | let cssText = e.srcElement.result; 614 | 615 | setCSSText(cssText); 616 | 617 | }); 618 | 619 | reader.readAsText(file); 620 | }); 621 | 622 | input.click(); 623 | 624 | 625 | } 626 | 627 | /* *********************** */ 628 | 629 | function exportCSSFileAsDownload(suggestedFileName){ 630 | 631 | let filename = window.prompt('Save As...', suggestedFileName); 632 | 633 | if(filename == null){ 634 | return; 635 | } 636 | 637 | let cssText = getCSSText(); 638 | 639 | let file = new File([cssText], '', {type: 'text/css'}); 640 | 641 | let anchor = document.createElement('a'); 642 | anchor.href = window.URL.createObjectURL(file); 643 | anchor.setAttribute('download', filename); 644 | 645 | anchor.click(); 646 | 647 | } 648 | 649 | /* *********************** */ 650 | 651 | async function exportUnformattedHTML(){ 652 | 653 | let suggestedFileName = 'unformatted' + getSuggestedFileName(); 654 | 655 | let saveAsHTMLFileHandle = 'desktop'; 656 | 657 | if (currentHTMLFileHandle) { 658 | 659 | saveAsHTMLFileHandle = currentHTMLFileHandle; 660 | 661 | let filename = currentHTMLFileHandle.name; 662 | if(filename){ 663 | filename = 'unformatted' + filename; 664 | suggestedFileName = filename; 665 | } 666 | } 667 | 668 | if(fileSystemSupport == false){ 669 | exportUnformattedHTMLAsDownload(suggestedFileName); 670 | return; 671 | } 672 | 673 | const opts = { 674 | suggestedName: suggestedFileName, 675 | startIn: saveAsHTMLFileHandle, 676 | types: [ 677 | { 678 | description: 'HTML file', 679 | accept: { 'text/html': ['.html', '.htm', '.htmd'] }, 680 | }, 681 | ], 682 | excludeAcceptAllOption: true, 683 | }; 684 | 685 | try { 686 | fileHandle = await window.showSaveFilePicker(opts); 687 | } catch (ex) { 688 | if (ex.name === 'AbortError') { 689 | return; 690 | } 691 | const msg = 'An error occured trying to open the file.'; 692 | console.error(msg, ex); 693 | alert(msg); 694 | return; 695 | } 696 | 697 | if ((await verifyPermission(fileHandle, true)) === false) { 698 | console.error(`User did not grant permission to '${fileHandle.name}'`); 699 | return; 700 | } 701 | 702 | let htmlText = getUnformattedHTML(); 703 | 704 | const writable = await fileHandle.createWritable(); 705 | await writable.write(htmlText); 706 | await writable.close(); 707 | 708 | } 709 | 710 | /* *********************** */ 711 | 712 | function exportUnformattedHTMLAsDownload(suggestedFileName){ 713 | 714 | let filename = window.prompt('Save As...', suggestedFileName); 715 | 716 | if(filename == null){ 717 | return; 718 | } 719 | 720 | let htmlText = getUnformattedHTML(); 721 | 722 | let file = new File([htmlText], '', {type: 'text/html'}); 723 | 724 | let anchor = document.createElement('a'); 725 | anchor.href = window.URL.createObjectURL(file); 726 | anchor.setAttribute('download', filename); 727 | 728 | anchor.click(); 729 | 730 | } 731 | 732 | 733 | /* *********************** */ 734 | 735 | async function getScript(){ 736 | 737 | if(fileSystemSupport == false){ 738 | let scriptText = await getScriptFromInput(); 739 | return scriptText; 740 | } 741 | 742 | const opts = { 743 | types: [ 744 | { 745 | description: 'JavaScript file', 746 | accept: { 'text/javascript': ['.js'] }, 747 | }, 748 | ], 749 | excludeAcceptAllOption: true, 750 | }; 751 | 752 | let fileHandle; 753 | 754 | try { 755 | [fileHandle] = await window.showOpenFilePicker(opts); 756 | } catch (ex) { 757 | if (ex.name === 'AbortError') { 758 | return; 759 | } 760 | const msg = 'An error occured trying to open the file.'; 761 | console.error(msg, ex); 762 | alert(msg); 763 | } 764 | 765 | if (!fileHandle) { 766 | return; 767 | } 768 | 769 | const file = await fileHandle.getFile(); 770 | 771 | try { 772 | let scriptText = await file.text(); 773 | 774 | return scriptText; 775 | 776 | } catch (ex) { 777 | const msg = `An error occured reading ${currentHTMLFileHandle.name}`; 778 | console.error(msg, ex); 779 | alert(msg); 780 | } 781 | 782 | } 783 | 784 | /* *********************** */ 785 | 786 | function getScriptFromImport(){ 787 | 788 | return new Promise ((resolve, reject) => { 789 | 790 | let input = document.createElement('input'); 791 | input.setAttribute('type', 'file'); 792 | input.setAttribute('accept', 'text/css'); 793 | 794 | input.addEventListener('change', function(e){ 795 | 796 | let file = e.target.files[0]; 797 | 798 | let reader = new FileReader(); 799 | 800 | reader.addEventListener('loadend', function(e){ 801 | 802 | let scriptText = e.srcElement.result; 803 | 804 | resolve(scriptText); 805 | 806 | }); 807 | 808 | reader.addEventListener('error', function(e){ 809 | reject(); 810 | }); 811 | 812 | reader.addEventListener('abort', function(e){ 813 | reject(); 814 | }); 815 | 816 | reader.readAsText(file); 817 | }); 818 | 819 | input.click(); 820 | 821 | }); 822 | 823 | } 824 | 825 | /* *********************** */ 826 | 827 | 828 | -------------------------------------------------------------------------------- /src/scripts/editor-init-settings.js: -------------------------------------------------------------------------------- 1 | /* *********************** */ 2 | 3 | let contentCSSUrl = './css/doc-wrapper.css'; 4 | let skinName = './css/hypertext-skin'; 5 | 6 | let content_css = new URL(contentCSSUrl, window.location.href).toString(); 7 | let skin_url = new URL(skinName, window.location.href).toString(); 8 | let theme_url = new URL('./scripts/hypertext-theme.js', window.location.href).toString(); 9 | let model_url = new URL('./scripts/hypertext-model.js', window.location.href).toString(); 10 | 11 | 12 | let editorInitSettings = { 13 | selector: '#textEditor', 14 | // document_base_url: localServer, 15 | content_css: content_css, 16 | skin_url: skin_url, 17 | theme_url: theme_url, 18 | min_height: 1088, 19 | max_width: 816, 20 | 21 | model_url: model_url, 22 | 23 | setup: editorSetup, 24 | 25 | plugins: ['anchor', 'autolink', 'autosave', 'codesample', 'help', 'insertdatetime', 'link', 'lists', 'searchreplace', 'table', 'visualblocks', 'visualchars', 'wordcount'], 26 | 27 | contextmenu: 'spelling | link | forecolor backcolor | image | table | attributes edit-block | edit-tag', 28 | 29 | toolbar: false, 30 | 31 | menubar: 'file edit view insert format blocks tools help', 32 | 33 | menu: { 34 | file: { title: 'File', items: 'menunew menuopen | newwindow setfolder | menusave menusaveas | cssfile-menu | edit-doc-props | restoredraft | preview print ' }, 35 | edit: { title: 'Edit', items: 'undo redo | cut copy paste selectall | pastetext | searchreplace' }, 36 | view: { title: 'View', items: 'visualchars visualblocks | toggle-textmenu toggle-darkmode | wordcount' }, 37 | insert: { title: 'Insert', items: 'link image embed codesample add-toc hr anchor | inserttable | insertdatetime' }, 38 | format: { title: 'Format', items: 'bold italic underline strikethrough superscript subscript codeformat forecolor backcolor | removeformat ' }, 39 | blocks: { title: 'Paragraphs', items: 'styles | indentation align | unordered ordered' }, 40 | tools: { title: 'Tools', items: 'edit-prefs | edit-head edit-body edit-css | edit-tag | run-macro ' }, 41 | help: { title: 'Help', items: 'help' }, 42 | }, 43 | 44 | style_formats: [ 45 | { title: 'Headings', items: [ 46 | { title: 'Heading 1', format: 'h1' }, 47 | { title: 'Heading 2', format: 'h2' }, 48 | { title: 'Heading 3', format: 'h3' }, 49 | { title: 'Heading 4', format: 'h4' }, 50 | { title: 'Heading 5', format: 'h5' }, 51 | ]}, 52 | { title: 'Blocks', items: [ 53 | { title: 'Paragraph', format: 'p' }, 54 | { title: 'Blockquote', format: 'blockquote' }, 55 | { title: 'Pre', format: 'pre' } 56 | ]} 57 | ], 58 | 59 | block_formats: 'Paragraph=p; Heading 1=h1; Heading 2=h2; Heading 3=h3; Heading 4=h4; Blockquote=blockquote; Pre=pre', 60 | 61 | valid_elements: '*[*]', 62 | extended_valid_elements : 'a[*],script[*],iframe[*],img[*],object[*],embed[*]', 63 | 64 | formats: { 65 | bold: { inline: 'strong' }, 66 | italic: { inline: 'em' }, 67 | underline: { inline: 'u' }, 68 | strikethrough: { inline: 's' } 69 | }, 70 | 71 | mobile: { 72 | menubar: 'file edit view insert format blocks tools help', 73 | }, 74 | 75 | help_tabs: ['shortcuts-help', 'images-help', 'about-help', 'build-version'], 76 | 77 | add_unload_trigger: true, 78 | allow_html_in_named_anchor: true, 79 | automatic_uploads: false, 80 | autosave_ask_before_unload: true, 81 | autosave_restore_when_empty: false, 82 | autosave_retention: '3600m', 83 | body_id: 'doc', 84 | branding: false, 85 | browser_spellcheck: true, 86 | contextmenu_never_use_native: false, 87 | convert_urls: false, 88 | elementpath: true, 89 | end_container_on_empty_block: true, 90 | entity_encoding: 'named', 91 | file_picker_types: 'image media', 92 | font_family_formats: '', 93 | font_size_formats: '', 94 | format_empty_lines: true, 95 | height: '100vh', 96 | image_advtab: false, 97 | image_caption: true, 98 | image_title: true, 99 | image_description: false, 100 | image_dimensions: false, 101 | image_uploadtab: false, 102 | images_reuse_filename: true, 103 | images_upload_url: '', 104 | importcss_append: true, 105 | indent_use_margin: false, 106 | indentation: '2rem', 107 | insertdatetime_element: false, 108 | insertdatetime_formats: ['%A %B %d, %Y - %I:%M %p', '%A %B %d, %Y', '%B %d, %Y', '%D', '%Y-%m-%d', '%Y-%m-%d %I:%M %p'], 109 | keep_styles: false, 110 | line_height_formats: '', 111 | link_context_toolbar: true, 112 | link_default_target: '_blank', 113 | link_quicklink: true, 114 | link_target_list: false, 115 | link_title: false, 116 | media_alt_source: false, 117 | media_dimensions: false, 118 | media_live_embeds: true, 119 | noneditable_class: 'tag', 120 | object_resizing: 'table', 121 | paste_block_drop: false, 122 | paste_data_images: false, 123 | paste_preprocess: pastePreprocess, 124 | paste_remove_styles_if_webkit: true, 125 | paste_merge_formats: false, 126 | promotion: false, 127 | quickbars_insert_toolbar: false, 128 | relative_urls: true, 129 | remove_trailing_brs: false, 130 | resize: false, 131 | resize_img_proportional: true, 132 | schema: 'html5-strict', 133 | smart_paste: true, 134 | style_formats_autohide: false, 135 | style_formats_merge: false, 136 | table_advtab: true, 137 | table_appearance_options: false, 138 | table_cell_advtab: false, 139 | table_clone_elements: 'strong em a p', 140 | table_header_type: 'section', 141 | table_resize_bars: true, 142 | table_row_advtab: true, 143 | table_sizing_mode: 'relative', 144 | table_style_by_css: true, 145 | table_tab_navigation: true, 146 | table_use_colgroups: false, 147 | target_list: false, 148 | theme: 'silver', 149 | toolbar_mode: 'floating', 150 | typeahead_urls: false, 151 | verify_html: false, 152 | visual: false, 153 | 154 | allow_html_data_urls: true, 155 | allow_svg_data_urls: true, 156 | 157 | file_picker_callback: filePicker, 158 | 159 | // init_instance_callback: (editor) => { 160 | // editor.on('ExecCommand', (e) => { 161 | // console.log(`The ${e.command} command was fired.`); 162 | // }); 163 | // } 164 | 165 | text_patterns: [ 166 | {start: '#', format: 'h1'}, 167 | {start: '##', format: 'h2'}, 168 | {start: '###', format: 'h3'}, 169 | {start: '####', format: 'h4'}, 170 | {start: '#####', format: 'h5'}, 171 | {start: '*', end: '*', format: 'italic'}, 172 | {start: '_', end: '_', format: 'italic'}, 173 | {start: '**', end: '**', format: 'bold'}, 174 | {start: '***', end: '***', format: 'bold+italic'}, 175 | {start: '~~', end: '~~', format: 'strikethrough'}, 176 | {start: '1. ', cmd: 'InsertOrderedList'}, 177 | {start: '* ', cmd: 'InsertUnorderedList'}, 178 | {start: '- ', cmd: 'InsertUnorderedList'}, 179 | {start: '> ', cmd: 'mceBlockQuote'}, 180 | {start: '`', end: '`', format: 'code'}, 181 | {start: '---', replacement: '
'}, 182 | ] 183 | 184 | 185 | }; 186 | -------------------------------------------------------------------------------- /src/scripts/editor-setup.js: -------------------------------------------------------------------------------- 1 | /* *********************** */ 2 | 3 | var cssToggleState = false; 4 | var toolbarToggleState = (localStorage.getItem('toolbar') === 'true'); 5 | var darkModeToggle = (localStorage.getItem('darkMode') === 'true'); 6 | 7 | let textMenuToggle = (localStorage.getItem('textMenu') === 'true'); 8 | 9 | toggleTextMenu(textMenuToggle); 10 | 11 | let localServer = localStorage.getItem('localServer') || window.location.pathname; 12 | 13 | function editorSetup(editor) { 14 | 15 | /* *********************** */ 16 | 17 | editor.ui.registry.addIcon('body-icon', ''); 18 | editor.ui.registry.addIcon('head-icon', ''); 19 | editor.ui.registry.addIcon('css-icon', ''); 20 | editor.ui.registry.addIcon('docprops-icon', ''); 21 | editor.ui.registry.addIcon('toolbar-icon', ''); 22 | editor.ui.registry.addIcon('cssfile-icon', ''); 23 | editor.ui.registry.addIcon('textmenu-icon', ''); 24 | editor.ui.registry.addIcon('export-icon', ''); 25 | editor.ui.registry.addIcon('folder-icon', ''); 26 | editor.ui.registry.addIcon('coffee-icon', ''); 27 | editor.ui.registry.addIcon('attributes-icon', ''); 28 | editor.ui.registry.addIcon('window-icon', ''); 29 | editor.ui.registry.addIcon('File', ''); 30 | editor.ui.registry.addIcon('Edit', ''); 31 | editor.ui.registry.addIcon('View', ''); 32 | editor.ui.registry.addIcon('Insert', ''); 33 | editor.ui.registry.addIcon('Format', ''); 34 | editor.ui.registry.addIcon('Tools', ''); 35 | editor.ui.registry.addIcon('Table', ''); 36 | editor.ui.registry.addIcon('Help', ''); 37 | editor.ui.registry.addIcon('Paragraphs', ''); 38 | 39 | 40 | 41 | /* *********************** */ 42 | 43 | if('showOpenFilePicker' in window){ 44 | 45 | editor.ui.registry.addButton('setfolder', { 46 | icon: 'folder-icon', 47 | onAction: async function () { 48 | await setWorkingFolder(); 49 | editor.execCommand('mceCleanup'); 50 | }, 51 | }); 52 | 53 | editor.ui.registry.addMenuItem('setfolder', { 54 | icon: 'folder-icon', 55 | text: 'Set Working Folder', 56 | onAction: async function () { 57 | await setWorkingFolder(); 58 | editor.execCommand('mceCleanup'); 59 | }, 60 | }); 61 | } 62 | 63 | /* *********************** */ 64 | 65 | if (window.matchMedia('(display-mode: browser)').matches == false) { 66 | 67 | editor.ui.registry.addButton('newwindow', { 68 | icon: 'window-icon', 69 | onAction: async function () { 70 | window.open('/editor', 'newwindow' + Math.random(), 'height=' + window.innerHeight + ',width=' + window.outerWidth); 71 | }, 72 | }); 73 | 74 | 75 | editor.ui.registry.addMenuItem('newwindow', { 76 | icon: 'window-icon', 77 | text: 'New window', 78 | onAction: async function () { 79 | window.open('/editor', 'newwindow' + Math.random(), 'height=' + window.innerHeight + ',width=' + window.outerWidth); 80 | }, 81 | }); 82 | 83 | } 84 | 85 | /* *********************** */ 86 | 87 | editor.ui.registry.addMenuItem('embed', { 88 | icon: 'embed', 89 | text: 'Embed...', 90 | onAction: function () { 91 | editEmbed(); 92 | }, 93 | }); 94 | 95 | editor.ui.registry.addButton('embed', { 96 | icon: 'embed', 97 | onAction: function () { 98 | editEmbed(); 99 | }, 100 | }); 101 | 102 | editor.ui.registry.addContextMenu('embed', { 103 | update: (element) => element.classList.contains('embed') ? 'embed' : '' 104 | }); 105 | 106 | 107 | /* *********************** */ 108 | 109 | editor.ui.registry.addMenuItem('image', { 110 | icon: 'image', 111 | text: 'Image...', 112 | onAction: function () { 113 | editImage(); 114 | }, 115 | }); 116 | 117 | editor.ui.registry.addButton('image', { 118 | icon: 'image', 119 | onAction: function () { 120 | editImage(); 121 | }, 122 | }); 123 | 124 | editor.ui.registry.addContextMenu('image', { 125 | update: (element) => (element.nodeName.toLowerCase() == 'img' || element.nodeName.toLowerCase() == 'figure') ? 'image' : '' 126 | }); 127 | 128 | /* *********************** */ 129 | 130 | editor.ui.registry.addMenuItem('spelling', { 131 | icon: 'spell-check', 132 | text: 'Spelling', 133 | onAction: function () { 134 | showSpelling(); 135 | }, 136 | }); 137 | 138 | editor.ui.registry.addButton('spelling', { 139 | icon: 'spell-check', 140 | onAction: function () { 141 | showSpelling(); 142 | }, 143 | }); 144 | 145 | editor.ui.registry.addContextMenu('spelling', { 146 | update: function(element) { 147 | let range = editor.selection.getRng(); 148 | if (range.startContainer.nodeType === Node.TEXT_NODE) { 149 | return 'spelling'; 150 | } 151 | return ''; 152 | } 153 | }); 154 | 155 | /* *********************** */ 156 | 157 | editor.ui.registry.addMenuItem('attributes', { 158 | icon: 'attributes-icon', 159 | text: 'Attributes', 160 | onAction: function () { 161 | showAttributesMenu(); 162 | }, 163 | }); 164 | 165 | editor.ui.registry.addButton('attributes', { 166 | icon: 'attributes-icon', 167 | onAction: function () { 168 | showAttributesMenu(); 169 | }, 170 | }); 171 | 172 | editor.ui.registry.addContextMenu('attributes', { 173 | update: (element) => (element.nodeName.toLowerCase() == 'body') ? '' : 'attributes' 174 | }); 175 | 176 | /* *********************** */ 177 | 178 | editor.ui.registry.addMenuItem('edit-block', { 179 | icon: 'sourcecode', 180 | text: 'Edit Block', 181 | onAction: function () { 182 | editBlock(); 183 | }, 184 | }); 185 | 186 | editor.ui.registry.addButton('edit-block', { 187 | icon: 'sourcecode', 188 | onAction: function () { 189 | editBlock(); 190 | }, 191 | }); 192 | 193 | editor.ui.registry.addContextMenu('edit-block', { 194 | update: (element) => { 195 | if( element.nodeName.toLowerCase() !== 'body'){ 196 | return 'edit-block' 197 | } else { 198 | return ''; 199 | } 200 | } 201 | }); 202 | 203 | /* *********************** */ 204 | 205 | editor.ui.registry.addMenuItem('context-edit-tag', { 206 | icon: 'addtag', 207 | text: 'Edit Tag', 208 | onAction: function () { 209 | editTag(); 210 | }, 211 | }); 212 | 213 | 214 | editor.ui.registry.addMenuItem('edit-tag', { 215 | icon: 'addtag', 216 | text: 'Add Tag', 217 | onAction: function () { 218 | editTag(); 219 | }, 220 | }); 221 | 222 | editor.ui.registry.addButton('edit-tag', { 223 | icon: 'addtag', 224 | onAction: function () { 225 | editTag(); 226 | }, 227 | }); 228 | 229 | editor.ui.registry.addContextMenu('edit-tag', { 230 | update: (element) => { 231 | if(element.nodeName.toLowerCase() == 'pre' && element.classList.contains('tag')){ 232 | return 'context-edit-tag'; 233 | } else { 234 | return ''; 235 | } 236 | } 237 | }); 238 | 239 | /* *********************** */ 240 | 241 | editor.ui.registry.addSidebar('edit-css', { 242 | tooltip: 'Edit CSS', 243 | icon: 'css-icon', 244 | onShow: function (api) { 245 | openCSSEditor(editor, api.element()); 246 | } 247 | }); 248 | 249 | editor.ui.registry.addToggleMenuItem('edit-css', { 250 | icon: 'css-icon', 251 | text: 'Edit CSS', 252 | shortcut: 'Meta+Shift+0', 253 | onAction: function () { 254 | toggleSidebarCSS(); 255 | }, 256 | onSetup: function (api) { 257 | api.setActive(cssToggleState); 258 | return () => {}; 259 | }, 260 | }); 261 | 262 | /* *********************** */ 263 | 264 | editor.ui.registry.addButton('run-macro', { 265 | icon: 'arrow-right', 266 | onAction: function () { 267 | runMacro(); 268 | }, 269 | }); 270 | 271 | editor.ui.registry.addMenuItem('run-macro', { 272 | icon: 'arrow-right', 273 | text: 'Run Macro...', 274 | onAction: function () { 275 | runMacro(); 276 | }, 277 | }); 278 | 279 | 280 | /* *********************** */ 281 | 282 | editor.ui.registry.addButton('edit-head', { 283 | icon: 'head-icon', 284 | onAction: function () { 285 | editHead(); 286 | }, 287 | }); 288 | 289 | editor.ui.registry.addMenuItem('edit-head', { 290 | icon: 'head-icon', 291 | text: 'Edit Head...', 292 | shortcut: 'Meta+Shift+8', 293 | onAction: function () { 294 | editHead(); 295 | }, 296 | }); 297 | 298 | /* *********************** */ 299 | 300 | editor.ui.registry.addButton('edit-body', { 301 | icon: 'body-icon', 302 | onAction: function () { 303 | editBody(); 304 | }, 305 | }); 306 | 307 | editor.ui.registry.addMenuItem('edit-body', { 308 | icon: 'body-icon', 309 | text: 'Edit Body...', 310 | shortcut: 'Meta+Shift+9', 311 | onAction: function () { 312 | editBody(); 313 | }, 314 | }); 315 | 316 | /* *********************** */ 317 | 318 | editor.ui.registry.addButton('edit-prefs', { 319 | icon: 'preferences', 320 | onAction: function () { 321 | editPrefs(); 322 | }, 323 | }); 324 | 325 | editor.ui.registry.addMenuItem('edit-prefs', { 326 | icon: 'preferences', 327 | text: 'Preferences', 328 | onAction: function () { 329 | editPrefs(); 330 | }, 331 | }); 332 | 333 | /* *********************** */ 334 | 335 | editor.ui.registry.addButton('edit-doc-props', { 336 | icon: 'docprops-icon', 337 | onAction: function () { 338 | editDocProps(); 339 | }, 340 | }); 341 | 342 | editor.ui.registry.addMenuItem('edit-doc-props', { 343 | icon: 'docprops-icon', 344 | text: 'Document Properties', 345 | onAction: function () { 346 | editDocProps(); 347 | }, 348 | }); 349 | 350 | /* *********************** */ 351 | 352 | editor.ui.registry.addButton('add-toc', { 353 | icon: 'toc', 354 | onAction: function () { 355 | createTableOfContents(); 356 | }, 357 | }); 358 | 359 | editor.ui.registry.addMenuItem('add-toc', { 360 | icon: 'toc', 361 | text: 'Table of Contents', 362 | onAction: function () { 363 | createTableOfContents(); 364 | }, 365 | }); 366 | 367 | /* *********************** */ 368 | 369 | editor.ui.registry.addToggleMenuItem('toggle-textmenu', { 370 | icon: 'textmenu-icon', 371 | text: 'Show Text Menu', 372 | onAction: function () { 373 | toggleTextMenu(); 374 | }, 375 | onSetup: function (api) { 376 | api.setActive(textMenuToggle); 377 | return () => {}; 378 | }, 379 | }); 380 | 381 | /* *********************** */ 382 | 383 | editor.ui.registry.addToggleMenuItem('toggle-darkmode', { 384 | icon: 'contrast', 385 | text: 'Toggle dark mode', 386 | onAction: function () { 387 | toggleDarkMode(); 388 | }, 389 | onSetup: function (api) { 390 | api.setActive(darkModeToggle); 391 | return () => {}; 392 | }, 393 | }); 394 | 395 | /* *********************** */ 396 | 397 | editor.ui.registry.addNestedMenuItem('cssfile-menu', { 398 | icon: 'cssfile-icon', 399 | text: 'CSS', 400 | getSubmenuItems: () => [ 401 | { 402 | type: 'menuitem', 403 | text: 'Import CSS', 404 | onAction: function () { 405 | importCSSFile(); 406 | }, 407 | }, 408 | { 409 | type: 'menuitem', 410 | text: 'Export CSS', 411 | onAction: function () { 412 | exportCSSFile(); 413 | }, 414 | }, 415 | { 416 | type: 'menuitem', 417 | text: 'Save as my default...', 418 | onAction: function () { 419 | setCSSDefault(); 420 | }, 421 | }, 422 | { 423 | type: 'menuitem', 424 | text: 'Reset default...', 425 | onAction: function () { 426 | resetCSSDefault(); 427 | }, 428 | }, 429 | ], 430 | }); 431 | 432 | 433 | /* *********************** */ 434 | 435 | editor.ui.registry.addToggleMenuItem('visualaid-option', { 436 | text: 'Show invisible items', 437 | icon: 'drag', 438 | onSetup: function(api){ 439 | api.setActive(editor.hasVisual); 440 | }, 441 | onAction: function(){ 442 | tinyMCE.execCommand('mceToggleVisualAid'); 443 | } 444 | }); 445 | 446 | editor.ui.registry.addButton('visualaid-option', { 447 | tooltip: 'Visual aids', 448 | text: 'Visual aids', 449 | icon: 'drag', 450 | onAction: function(){ 451 | tinyMCE.execCommand('mceToggleVisualAid'); 452 | } 453 | }); 454 | 455 | 456 | /* *********************** */ 457 | 458 | editor.ui.registry.addMenuItem('menunew', { 459 | icon: 'new-document', 460 | text: 'New', 461 | onAction: function () { 462 | if (confirmDocumentChange()) { 463 | toggleSidebarCSS(false); 464 | newHTMLFile(); 465 | } 466 | }, 467 | }); 468 | 469 | /* *********************** */ 470 | 471 | editor.ui.registry.addMenuItem('menuopen', { 472 | icon: 'browse', 473 | text: 'Open', 474 | shortcut: 'Meta+O', 475 | onAction: function () { 476 | if (confirmDocumentChange()) { 477 | toggleSidebarCSS(false); 478 | openHTMLFile(); 479 | } 480 | }, 481 | }); 482 | 483 | /* *********************** */ 484 | 485 | editor.ui.registry.addMenuItem('menusave', { 486 | icon: 'save', 487 | text: 'Save', 488 | shortcut: 'Meta+S', 489 | onAction: function () { 490 | saveHTMLFile(); 491 | }, 492 | }); 493 | 494 | /* *********************** */ 495 | 496 | editor.ui.registry.addMenuItem('menusaveas', { 497 | icon: 'save', 498 | text: 'Save As...', 499 | shortcut: 'Meta+Shift+S', 500 | onAction: function () { 501 | saveHTMLFileAs(); 502 | }, 503 | }); 504 | 505 | /* *********************** */ 506 | 507 | editor.ui.registry.addMenuItem('blockquotemenu', { 508 | icon: 'quote', 509 | text: 'Blockquote', 510 | onAction: function () { 511 | tinyMCE.execCommand('mceBlockQuote'); 512 | }, 513 | }); 514 | 515 | /* *********************** */ 516 | 517 | editor.ui.registry.addMenuItem('unordered', { 518 | icon: 'unordered-list', 519 | text: 'Bullet List', 520 | onAction: function () { 521 | tinyMCE.execCommand('InsertUnorderedList'); 522 | }, 523 | }); 524 | 525 | /* *********************** */ 526 | 527 | editor.ui.registry.addMenuItem('ordered', { 528 | icon: 'ordered-list', 529 | text: 'Numbered List', 530 | onAction: function () { 531 | tinyMCE.execCommand('InsertOrderedList'); 532 | }, 533 | }); 534 | 535 | /* *********************** */ 536 | 537 | editor.ui.registry.addNestedMenuItem('export', { 538 | text: 'Export', 539 | icon: 'export-icon', 540 | getSubmenuItems: () => [ 541 | { 542 | type: 'menuitem', 543 | text: 'Unformatted HTML', 544 | onAction: function () { 545 | exportUnformattedHTML(); 546 | }, 547 | }, 548 | ], 549 | }); 550 | 551 | /* *********************** */ 552 | 553 | 554 | editor.ui.registry.addNestedMenuItem('indentation', { 555 | text: 'Indentation', 556 | getSubmenuItems: () => [ 557 | { 558 | type: 'menuitem', 559 | icon: 'indent', 560 | text: 'Indent', 561 | onAction: function () { 562 | tinyMCE.execCommand('indent'); 563 | }, 564 | }, 565 | { 566 | type: 'menuitem', 567 | icon: 'outdent', 568 | text: 'Outdent', 569 | onAction: function () { 570 | tinyMCE.execCommand('outdent'); 571 | }, 572 | }, 573 | ], 574 | }); 575 | 576 | /* *********************** */ 577 | 578 | editor.addShortcut('Meta+Shift+Z', 'Redo', function () { 579 | tinyMCE.execCommand('Redo'); 580 | }); 581 | 582 | editor.addShortcut('Meta+1', 'Heading 1', function () { 583 | tinyMCE.execCommand('mceToggleFormat', false, 'h1'); 584 | }); 585 | 586 | editor.addShortcut('Meta+2', 'Heading 2', function () { 587 | tinyMCE.execCommand('mceToggleFormat', false, 'h2'); 588 | }); 589 | 590 | editor.addShortcut('Meta+3', 'Heading 3', function () { 591 | tinyMCE.execCommand('mceToggleFormat', false, 'h3'); 592 | }); 593 | 594 | editor.addShortcut('Meta+4', 'Heading 4', function () { 595 | tinyMCE.execCommand('mceToggleFormat', false, 'h4'); 596 | }); 597 | 598 | editor.addShortcut('Meta+5', 'Blockquote', function () { 599 | tinyMCE.execCommand('mceBlockQuote'); 600 | }); 601 | 602 | editor.addShortcut('Meta+0', 'Paragraph', function () { 603 | tinyMCE.execCommand('mceToggleFormat', false, 'p'); 604 | }); 605 | 606 | editor.addShortcut('Meta+Shift+K', 'Code', function () { 607 | tinyMCE.execCommand('mceToggleFormat', false, 'code'); 608 | }); 609 | 610 | editor.addShortcut('Meta+D', 'Strikethrough', function () { 611 | tinyMCE.execCommand('Strikethrough'); 612 | }); 613 | 614 | editor.addShortcut('Meta+Shift+O', 'Numbered list', function () { 615 | tinyMCE.execCommand('InsertOrderedList'); 616 | }); 617 | 618 | editor.addShortcut('Meta+Shift+U', 'Bullet list', function () { 619 | tinyMCE.execCommand('InsertUnorderedList'); 620 | }); 621 | 622 | editor.addShortcut('Meta+Shift+T', 'Table', function () { 623 | tinyMCE.execCommand('mceInsertTable'); 624 | }); 625 | 626 | editor.addShortcut('Meta+Shift+I', 'Image', function () { 627 | editImage(); 628 | }); 629 | 630 | editor.addShortcut('Meta+Shift+L', 'Justify left', function () { 631 | tinyMCE.execCommand('JustifyLeft'); 632 | }); 633 | 634 | editor.addShortcut('Meta+Shift+E', 'Justify center', function () { 635 | tinyMCE.execCommand('JustifyCenter'); 636 | }); 637 | 638 | editor.addShortcut('Meta+Shift+R', 'Justify right', function () { 639 | tinyMCE.execCommand('JustifyRight'); 640 | }); 641 | 642 | editor.addShortcut('Meta+Shift+J', 'Justify none', function () { 643 | tinyMCE.execCommand('JustifyNone'); 644 | }); 645 | 646 | editor.addShortcut('Meta+Shift+F', 'Remove format', function () { 647 | tinyMCE.execCommand('RemoveFormat'); 648 | }); 649 | 650 | editor.addShortcut('Meta+Shift+8', 'Edit Head', function () { 651 | editHead(); 652 | }); 653 | 654 | editor.addShortcut('Meta+Shift+9', 'Edit Body', function () { 655 | editBody(); 656 | }); 657 | 658 | editor.addShortcut('Meta+Shift+0', 'Edit CSS', function () { 659 | toggleSidebarCSS(); 660 | }); 661 | 662 | 663 | /* *********************** */ 664 | 665 | editor.on('keyup', function (e) { 666 | overrideKeyboardEvent(e); 667 | }); 668 | 669 | editor.on('keydown', function (e) { 670 | overrideKeyboardEvent(e); 671 | }); 672 | 673 | /* *********************** */ 674 | 675 | editor.on('Dirty', function (event) { 676 | updateWindowTitle(); 677 | }); 678 | 679 | /* *********************** */ 680 | 681 | editor.on('init', function () { 682 | /* *********************** */ 683 | 684 | (async function(editor){ 685 | 686 | let helpFiles = await getHelpFiles(); 687 | 688 | editor.plugins.help.addTab({ 689 | name: 'shortcuts-help', 690 | title: 'Keyboard Shortcuts', 691 | items: [{ 692 | type: 'htmlpanel', 693 | html: helpFiles['shortcuts-help'] 694 | }] 695 | }); 696 | editor.plugins.help.addTab({ 697 | name: 'images-help', 698 | title: 'Images Help', 699 | items: [{ 700 | type: 'htmlpanel', 701 | html: helpFiles['images-help'] 702 | }] 703 | }); 704 | editor.plugins.help.addTab({ 705 | name: 'about-help', 706 | title: 'About Hypertext', 707 | items: [{ 708 | type: 'htmlpanel', 709 | html: helpFiles['about-help'] 710 | }] 711 | }); 712 | editor.plugins.help.addTab({ 713 | name: 'build-version', 714 | title: 'Build', 715 | items: [{ 716 | type: 'htmlpanel', 717 | html: helpFiles['build-version'] 718 | }] 719 | }); 720 | 721 | })(editor); 722 | 723 | /* *********************** */ 724 | 725 | editor.on('BeforeGetContent', beforeGetContent); 726 | 727 | /* *********************** */ 728 | 729 | editor.on('dragover', dragOverHandler); 730 | editor.on('drop', dragDropHandler); 731 | 732 | 733 | /* *********************** */ 734 | 735 | toggleDarkMode(darkModeToggle); 736 | 737 | /* *********************** */ 738 | 739 | if (toOpenHTMLFileHandle !== null) { 740 | openHTMLFile(toOpenHTMLFileHandle); 741 | } else { 742 | newDocument(); 743 | } 744 | 745 | /* *********************** */ 746 | 747 | if(tinymce.Env.deviceType.isDesktop()){ 748 | 749 | let logoEl = document.querySelector('#logo'); 750 | if(!logoEl){ 751 | logoEl = document.createElement('a'); 752 | logoEl.href = 'https://www.hypertext.plus'; 753 | logoEl.target = '_blank'; 754 | logoEl.id = 'logo'; 755 | let menubar = document.querySelector('.tox-menubar'); 756 | menubar.append(logoEl); 757 | } 758 | 759 | } 760 | 761 | /* *********************** */ 762 | 763 | window.addEventListener('beforeunload', function (event) { 764 | if (tinymce.activeEditor.isDirty()) { 765 | event.preventDefault(); 766 | return (event.returnValue = 'Are you sure you want to exit?'); 767 | } 768 | }); 769 | 770 | /* *********************** */ 771 | 772 | }); 773 | } 774 | -------------------------------------------------------------------------------- /src/scripts/editor-window.js: -------------------------------------------------------------------------------- 1 | /* *********************** */ 2 | 3 | // window.addEventListener('contextmenu', function(event) { 4 | // event.preventDefault(); 5 | // }, false); 6 | 7 | /* *********************** */ 8 | 9 | 10 | window.addEventListener('keydown', overrideKeyboardEvent, {capture: true}); 11 | window.addEventListener('keyup', overrideKeyboardEvent, {capture: true}); 12 | 13 | function overrideKeyboardEvent(event) { 14 | let returnVal = true; 15 | 16 | if (event.type == 'keydown') { 17 | 18 | if (event.ctrlKey || event.metaKey) { 19 | 20 | if (!event.shiftKey && event.code === 'KeyO') { 21 | event.stopPropagation(); 22 | event.preventDefault(); 23 | returnVal = false; 24 | 25 | openHTMLFile(); 26 | } 27 | 28 | if (!event.shiftKey && event.code === 'KeyS') { 29 | event.stopPropagation(); 30 | event.preventDefault(); 31 | returnVal = false; 32 | 33 | saveHTMLFile(); 34 | } 35 | 36 | if (event.shiftKey && event.code === 'KeyS') { 37 | event.stopPropagation(); 38 | event.preventDefault(); 39 | returnVal = false; 40 | 41 | saveHTMLFileAs(); 42 | } 43 | 44 | if (event.shiftKey && event.code === 'BracketRight') { 45 | event.stopPropagation(); 46 | event.preventDefault(); 47 | returnVal = false; 48 | 49 | tinyMCE.execCommand('Indent'); 50 | } 51 | 52 | if (event.shiftKey && event.code === 'BracketLeft') { 53 | event.stopPropagation(); 54 | event.preventDefault(); 55 | returnVal = false; 56 | 57 | tinyMCE.execCommand('Outdent'); 58 | } 59 | 60 | } 61 | } 62 | 63 | return returnVal; 64 | } 65 | 66 | 67 | window.deferredPrompt = {}; 68 | 69 | 70 | window.addEventListener('beforeinstallprompt', e => { 71 | 72 | e.preventDefault(); 73 | 74 | let installButtonEl = document.querySelector('#installButton'); 75 | 76 | if(installButtonEl){ 77 | return; 78 | } 79 | 80 | installButtonEl = document.createElement('button'); 81 | installButtonEl.id = 'installButton'; 82 | installButtonEl.setAttribute('unselectable', 'on'); 83 | installButtonEl.setAttribute('class', 'tox-mbtn tox-mbtn--select'); 84 | installButtonEl.setAttribute('style', 'user-select: none;'); 85 | 86 | let iconSpanEl = document.createElement('span'); 87 | iconSpanEl.setAttribute('class', 'tox-icon tox-tbtn__icon-wrap'); 88 | iconSpanEl.innerHTML = ''; 89 | installButtonEl.append(iconSpanEl); 90 | 91 | let labelSpanEl = document.createElement('span'); 92 | labelSpanEl.setAttribute('class', 'tox-mbtn__select-label'); 93 | labelSpanEl.textContent = 'Install'; 94 | installButtonEl.append(labelSpanEl); 95 | 96 | let logoEl = document.querySelector('#logo'); 97 | 98 | if(logoEl){ 99 | logoEl.before(installButtonEl); 100 | } else { 101 | document.querySelector('.tox-menubar').append(installButtonEl); 102 | } 103 | 104 | window.deferredPrompt = e; 105 | 106 | installButtonEl.addEventListener('click', e => { 107 | installButtonEl.style.display = 'none'; 108 | window.deferredPrompt.prompt(); 109 | window.deferredPrompt.userChoice.then(choiceResult => { 110 | if (choiceResult.outcome === 'accepted') { 111 | installButtonEl.style.display = 'none'; 112 | } else { 113 | console.log('User dismissed the prompt'); 114 | } 115 | window.deferredPrompt = null; 116 | }); 117 | }); 118 | 119 | }); 120 | 121 | if (window.matchMedia('(display-mode: standalone)').matches || window.navigator.standalone === true) { 122 | let installButtonEl = document.querySelector('#installButton'); 123 | if(installButtonEl){ 124 | installButtonEl.style.display = 'none'; 125 | } 126 | } 127 | 128 | window.addEventListener('appinstalled', e => { 129 | console.log("success app install!"); 130 | }); 131 | 132 | 133 | // window.onpageshow = window.onpagehide = window.onfocus = window.onblur = function(event){ 134 | 135 | // if(document.hasFocus()){ 136 | // document.querySelector('meta[name="theme-color"]').setAttribute('content', '#020887'); 137 | // } else { 138 | // document.querySelector('meta[name="theme-color"]').setAttribute('content', '#FDFDFD'); 139 | // } 140 | 141 | // } -------------------------------------------------------------------------------- /src/scripts/hypertext-app.js: -------------------------------------------------------------------------------- 1 | /* *********************** */ 2 | 3 | var appName = 'Hypertext'; 4 | 5 | tinymce.init(editorInitSettings); 6 | -------------------------------------------------------------------------------- /src/service-worker.js: -------------------------------------------------------------------------------- 1 | 2 | importScripts('./libs/idb-keyval.js'); 3 | 4 | var cacheName = '${cacheName}'; 5 | 6 | var contentToCache = ${files}; 7 | 8 | self.addEventListener('install', (e) => { 9 | console.log('Service Worker Install', cacheName); 10 | self.skipWaiting(); 11 | e.waitUntil((async () => { 12 | const cache = await caches.open(cacheName); 13 | await cache.addAll(contentToCache); 14 | })()); 15 | }); 16 | 17 | self.addEventListener('fetch', async (e) => { 18 | 19 | if (e.request.method !== 'GET') { 20 | return; 21 | } 22 | if (!e.request.url.startsWith(self.location.origin)) { 23 | return; 24 | } 25 | 26 | let url = e.request.url; 27 | 28 | let path = url.replace(self.location.origin + '/editor/', ''); 29 | 30 | e.respondWith((async () => { 31 | 32 | if(e.request.referrer.endsWith('editor.html')){ 33 | 34 | let dirHandle = await get('dirHandle'); 35 | 36 | if(dirHandle){ 37 | 38 | let path = url.replace(self.location.origin + '/editor/', ''); 39 | 40 | if(path.startsWith('./')){ 41 | path = path.substring(2, path.length); 42 | } 43 | 44 | let paths = path.split('/'); 45 | let len = paths.length; 46 | 47 | let curDir = dirHandle; 48 | 49 | try { 50 | 51 | for(let i = 0; i < len - 1; i++){ 52 | let dirName = decodeURI(paths[i]); 53 | curDir = await curDir.getDirectoryHandle(dirName); 54 | } 55 | 56 | let fileHandle = await curDir.getFileHandle(paths[len -1]); 57 | let file = await fileHandle.getFile(); 58 | return new Response(file.stream(), { 59 | headers: { 60 | 'content-type': file.type, 61 | } 62 | }); 63 | return; 64 | 65 | } catch(err){ 66 | 67 | } 68 | 69 | } 70 | 71 | } 72 | 73 | const r = await caches.match(e.request); 74 | if (r) { 75 | return r; 76 | } 77 | // console.log('fetching', url); 78 | const response = await fetch(e.request); 79 | const cache = await caches.open(cacheName); 80 | cache.put(e.request, response.clone()); 81 | return response; 82 | 83 | })()); 84 | 85 | }); 86 | 87 | self.addEventListener('activate', (e) => { 88 | e.waitUntil(caches.keys().then((keyList) => { 89 | return Promise.all(keyList.map((key) => { 90 | if (key === cacheName) { 91 | return; 92 | } 93 | return caches.delete(key); 94 | })); 95 | })); 96 | }); 97 | 98 | self.addEventListener('activate', event => { 99 | console.log('activated'); 100 | clients.claim(); 101 | 102 | caches.keys().then(function(cacheNames){ 103 | for (const key of cacheNames) { 104 | if (cacheName === key ) { 105 | continue; 106 | } 107 | console.log('deleting cache: ', key); 108 | caches.delete(key); 109 | } 110 | 111 | }); 112 | 113 | 114 | }); 115 | 116 | self.addEventListener('message', async (e) => { 117 | 118 | }); --------------------------------------------------------------------------------