├── .gitignore ├── History.md ├── LICENSE ├── Makefile ├── README.md ├── block.d.ts ├── block.js ├── build.sh ├── example ├── app.js ├── example.css ├── fonts │ ├── wpeditor.eot │ ├── wpeditor.svg │ ├── wpeditor.ttf │ └── wpeditor.woff ├── index.html └── wpeditor.css ├── index.d.ts ├── index.js ├── lib ├── auto-scroll │ └── index.ts ├── block-code │ ├── code-block.jade │ ├── code-block.styl │ ├── codemirror.css │ ├── command.es6 │ └── index.es6 ├── block-controls │ ├── block-controls.styl │ └── index.js ├── block-html │ ├── html-block.jade │ ├── html-block.styl │ └── index.js ├── block │ ├── block.jade │ ├── block.styl │ ├── common.jade │ ├── index.d.ts │ └── index.ts ├── editor-keyboard-shortcuts │ └── index.ts ├── editor-link-tooltip │ ├── index.es6 │ ├── link-tooltip.jade │ └── link-tooltip.styl ├── editor-overlay │ ├── editor-overlay.styl │ └── index.ts ├── editor-selection │ └── index.ts ├── editor-serializer │ └── index.ts ├── editor-tip │ ├── editor-tip.styl │ └── index.js ├── editor-toolbar-tooltips │ └── index.js ├── editor-toolbar │ └── index.js ├── editor │ ├── align-center-icon.svg │ ├── align-full-icon.svg │ ├── align-left-icon.svg │ ├── align-right-icon.svg │ ├── bold-icon.svg │ ├── code-icon.svg │ ├── del-icon.svg │ ├── editor.styl │ ├── formatbar-header.jade │ ├── formatbar-justify.jade │ ├── formatbar.jade │ ├── formatbar.styl │ ├── h1-icon.svg │ ├── h2-icon.svg │ ├── h3-icon.svg │ ├── header-icon.svg │ ├── index.d.ts │ ├── index.js │ ├── italic-icon.svg │ ├── link-icon.svg │ ├── ol-icon.svg │ ├── quote-icon.svg │ ├── test │ │ └── test.js │ └── ul-icon.svg ├── font-loader │ └── index.ts ├── gallery-entry │ ├── gallery-entry-out.styl │ ├── gallery-entry.styl │ └── index.js ├── gallery │ ├── constants.js │ ├── gallery.styl │ └── index.js ├── hacks │ └── index.ts ├── html-debugger │ ├── html-debugger.styl │ └── index.js ├── input-normalizer │ ├── index.ts │ ├── leaf-range.ts │ └── range-position.ts ├── is │ ├── index.ts │ └── test │ │ └── test.js ├── progress-spinner │ ├── index.js │ └── progress-spinner.styl ├── sandbox │ └── index.ts ├── tokenizer-links │ └── index.ts ├── tokenizer │ ├── index.ts │ ├── renderer.ts │ ├── token.styl │ ├── token.ts │ └── util.ts └── transaction-manager │ ├── command.ts │ ├── composite-operation.ts │ ├── frozen-range.ts │ ├── index.ts │ ├── operation-stack.ts │ ├── operation.ts │ └── unknown-operation.ts ├── node.d.ts ├── package.json ├── plugins ├── zeditor-normalizer │ └── index.es6 ├── zeditor-paste │ ├── Readme.md │ ├── index.js │ └── insert.ts └── zeditor-plugin │ └── index.es6 ├── reset.css └── types.d.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # compiled files 2 | *.tsjs 3 | *.styl.css 4 | *.jadejs 5 | *.es6js 6 | /editor.css 7 | /example/bundle.js 8 | 9 | # root node_modules dir 10 | /node_modules 11 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2015 Automattic 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | 'Software'), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 21 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 22 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | # Get Makefile directory name: http://stackoverflow.com/a/5982798/376773. 3 | # This is a defensive programming approach to ensure that this Makefile 4 | # works even when invoked with the `-C`/`--directory` option. 5 | THIS_MAKEFILE_PATH:=$(word $(words $(MAKEFILE_LIST)),$(MAKEFILE_LIST)) 6 | THIS_DIR:=$(shell cd $(dir $(THIS_MAKEFILE_PATH));pwd) 7 | 8 | # BIN directory 9 | BIN := $(THIS_DIR)/node_modules/.bin 10 | 11 | # applications 12 | NODE ?= node 13 | NPM ?= $(NODE) $(shell which npm) 14 | STYL ?= $(NODE) $(BIN)/styl 15 | BROWSERIFY ?= $(NODE) $(BIN)/browserify 16 | BROWSERIFY_FLAGS ?= --debug 17 | BABEL ?= $(NODE) $(BIN)/babel 18 | ZUUL ?= $(NODE) $(BIN)/zuul 19 | WR ?= $(NODE) $(BIN)/wr 20 | SERVE ?= $(NODE) $(BIN)/serve 21 | PARALLELSHELL ?= $(NODE) $(BIN)/parallelshell 22 | BROWSERIFY_SINGLE_FILE ?= $(NODE) $(BIN)/browserify-single-file 23 | 24 | # source files 25 | JADE_FILES := $(wildcard lib/*/*.jade plugins/*/*.jade) 26 | STYL_FILES := $(wildcard lib/*/*.styl plugins/*/*.styl) 27 | JS_FILES := $(wildcard lib/*/*.js plugins/*/*.js) 28 | ES6_FILES := $(wildcard lib/*/*.es6 plugins/*/*.es6) 29 | TS_FILES := $(filter-out $(wildcard lib/*/*.d.ts plugins/*/*.d.ts), $(wildcard lib/*/*.ts plugins/*/*.ts)) 30 | 31 | # compiled files 32 | COMPILED_FILES := $(JADE_FILES:.jade=.jadejs) $(TS_FILES:.ts=.tsjs) $(ES6_FILES:.es6=.es6js) $(STYL_FILES:.styl=.styl.css) 33 | CSS_FILES := $(STYL_FILES:.styl=.styl.css) $(filter-out $(wildcard lib/*/*.styl.css), $(wildcard lib/*/*.css)) 34 | 35 | # for gh-pages 36 | LOCAL_CLIENT_ID=36059 37 | GH_PAGES_CLIENT_ID=34793 38 | GH_PAGES_TMP=/tmp/editor_gh_pages 39 | 40 | # prevents `npm install` "infinite recursion" 41 | export INSIDE_MAKE=1 42 | 43 | # default rule 44 | build: node_modules $(COMPILED_FILES) editor.css 45 | 46 | # alias to the `node_modules` rule 47 | install: node_modules 48 | 49 | # helper rules to ensure that the `styl` / `tsify` / 50 | # `browserify-single-file`, etc. commands are installed 51 | node_modules/browserify-single-file: node_modules 52 | 53 | node_modules/styl: node_modules 54 | 55 | node_modules/tsify: node_modules 56 | 57 | node_modules/babel: node_modules 58 | 59 | node_modules/browserify-jade: node_modules 60 | 61 | node_modules/parallelshell: node_modules 62 | 63 | node_modules/serve: node_modules 64 | 65 | node_modules/wr: node_modules 66 | 67 | # ensures that the `node_modules` directory is installed and up-to-date with 68 | # the dependencies listed in the "package.json" file. 69 | node_modules: package.json 70 | @$(NPM) install 71 | @touch node_modules 72 | 73 | # compile all *.jade template files into *.jadejs files usable from the 74 | # client-side through browserify. Note that we could also simply use the 75 | # `jadeify` transform, however then we lose the `mtime` benefits of make 76 | %.jadejs: %.jade node_modules/browserify-single-file node_modules/browserify-jade 77 | @printf '\e[1;36m %-10s\e[m %s > %s\n' "jade" "$<" "$@" 78 | @$(BROWSERIFY_SINGLE_FILE) --transform browserify-jade $< > $@ 79 | 80 | # compile all *.styl CSS preprocessor files into *.styl.css files, which is 81 | # what the `editor.css` rule relies on to concat the final CSS bundle 82 | %.styl.css: %.styl node_modules/styl 83 | @printf '\e[1;35m %-10s\e[m %s > %s\n' "styl" "$<" "$@" 84 | @DEBUG= $(STYL) --whitespace < $< > $@ # note: have to reset DEBUG otherwise styl outputs some junk 85 | @echo >> $@ # ensure trailing \n 86 | 87 | # compile all *.ts Typescript files into *.js files 88 | %.tsjs: %.ts node_modules/tsify types.d.ts 89 | @printf '\e[1;32m %-10s\e[m %s > %s\n' "typescript" "$<" "$@" 90 | @$(NODE) -pe "(new (require('tsify/lib/Tsifier'))({ target: 'ES5' })).getCompiledFile('$<')" > "$@" 91 | 92 | # compile all *.es6 ECMAScript 6 files into *.js files 93 | %.es6js: %.es6 node_modules/babel 94 | @printf '\e[1;93m %-10s\e[m %s > %s\n' "babel" "$<" "$@" 95 | @$(BABEL) "$<" --source-maps-inline --optional runtime --experimental > "$@" 96 | 97 | # concats all the built `*.styl.css` CSS files 98 | editor.css: reset.css node_modules $(CSS_FILES) 99 | @printf '\e[1;93m %-10s\e[m %s\n' "concat" "editor.css" 100 | @cat reset.css $(wildcard $(shell node -pe "require('path').dirname(require.resolve('component-tip'))")/*.css) $(CSS_FILES) > $@ 101 | 102 | # bundle the `*.js` files into the `bundle.js` file 103 | example/bundle.js: example/app.js node_modules $(COMPILED_FILES) $(JS_FILES) 104 | @printf '\e[1;93m %-10s\e[m %s\n' "browserify" "$@" 105 | @$(BROWSERIFY) \ 106 | --extension=.jadejs \ 107 | --extension=.tsjs \ 108 | --extension=.es6js \ 109 | $(BROWSERIFY_FLAGS) \ 110 | example/app.js \ 111 | > $@ 112 | 113 | watch: node_modules/parallelshell 114 | @$(PARALLELSHELL) "$(MAKE) example-server" "$(MAKE) example-watch" 115 | 116 | example-server: node_modules/serve editor.css example/bundle.js 117 | @$(SERVE) --port 8888 "$(THIS_DIR)" 118 | 119 | # spawn a watcher to rebuild the files for the `example` dir 120 | # whenever a source file changes 121 | example-watch: node_modules/wr 122 | @$(WR) --chime 0 "$(MAKE) example" example/ lib/ plugins/ *.js *.css 123 | 124 | # helper rule 125 | example: editor.css example/bundle.js 126 | 127 | # the `clean` rule deletes all the files created from `make build` 128 | clean: 129 | rm -f editor.css \ 130 | example/bundle.js \ 131 | $(COMPILED_FILES) 132 | 133 | clean-typescript: 134 | rm -f $(TS_FILES:.ts=.tsjs) 135 | 136 | clean-jade: 137 | rm -f $(JADE_FILES:.jade=.jadejs) 138 | 139 | clean-styl: 140 | rm -f $(STYL_FILES:.styl=.styl.css) 141 | 142 | # the `distclean` rule deletes all the files created from `make install` 143 | distclean: 144 | rm -rf node_modules 145 | 146 | gh-pages: example 147 | @-git checkout -- package.json 148 | rm -rf $(GH_PAGES_TMP) \ 149 | && mkdir -p $(GH_PAGES_TMP) \ 150 | && rsync -rL example/* $(GH_PAGES_TMP) \ 151 | && cp editor.css $(GH_PAGES_TMP) \ 152 | && sed -i.bak 's/$(LOCAL_CLIENT_ID)/$(GH_PAGES_CLIENT_ID)/' $(GH_PAGES_TMP)/bundle.js \ 153 | && rm -f $(GH_PAGES_TMP)/bundle.js.bak \ 154 | && git checkout gh-pages \ 155 | && git fetch \ 156 | && git reset --hard origin/gh-pages \ 157 | && rm -rf * \ 158 | && rsync -rL $(GH_PAGES_TMP)/* . \ 159 | && echo "now run 'git add . && git commit -v'" 160 | 161 | # run `dox --api` to generate API docs for the Readme.md files for each submodule 162 | %/Readme.md: %/index.js node_modules 163 | $(eval OUTPUT := $(shell node -pe "require('fs').realpathSync('$@')")) 164 | @printf '\e[1;35m %-10s\e[m %s > %s\n' "dox --api" "$<" "$(OUTPUT)" 165 | @perl -0777 -i.bak -pe 's/API\n---.*/API\n---\n\n/igs' "$(OUTPUT)" \ 166 | && dox --api < "$<" >> "$(OUTPUT)" \ 167 | && rm "$(addsuffix .bak,$(OUTPUT))" 168 | 169 | docs: $(addsuffix Readme.md,$(dir $(JS_FILES))) 170 | 171 | test: 172 | @if [ "x$(BROWSER_NAME)" = "x" ]; then \ 173 | $(MAKE) test-local; \ 174 | else \ 175 | $(MAKE) test-zuul; \ 176 | fi 177 | 178 | test-local: 179 | @$(ZUUL) --local --ui mocha-bdd lib/*/test/*.js 180 | 181 | test-zuul: 182 | @$(ZUUL) \ 183 | --uimocha-bdd \ 184 | --browser-name $(BROWSER_NAME) \ 185 | --browser-version $(BROWSER_VERSION) \ 186 | --browser-platform "$(BROWSER_PLATFORM)" \ 187 | lib/*/test/*.js; \ 188 | fi 189 | 190 | 191 | .PHONY: install build example clean clean-typescript clean-jade clean-styl distclean gh-pages docs test test-local test-zuul watch example-server example-watch 192 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | zeditor 2 | ### zeditor 3 | 4 | Pluggable "Editor" API for contenteditable 5 | -------------------------------------------------------------------------------- /block.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /** 3 | * TypeScript dependencies 4 | */ 5 | import Editor = require('../editor/index'); 6 | import events = require('events'); 7 | declare class Block extends events.EventEmitter { 8 | /** 9 | * Private Fields 10 | */ 11 | private _editor; 12 | private _overlay; 13 | private _el; 14 | private _id; 15 | private _hold; 16 | private _holdX; 17 | private _holdY; 18 | constructor(overlay: HTMLElement, editor: Editor); 19 | /** 20 | * Returns the current overlay reference for the block, searching 21 | * through the DOM of the editor to do so. 22 | */ 23 | el: HTMLElement; 24 | /** 25 | * Returns the current overlay for the block 26 | */ 27 | overlay: HTMLElement; 28 | /** 29 | * Returns the bound editor for the block 30 | */ 31 | editor: Editor; 32 | /** 33 | * Binds the block to the editor instance 34 | */ 35 | bind(editor: Editor): void; 36 | /** 37 | * Fired when the mouse button is pressed on the block 38 | */ 39 | private onmousedown(e); 40 | /** 41 | * Fired when the mouse moves 42 | */ 43 | private onmousemove(e); 44 | /** 45 | * Fired when the mouse button is lifted 46 | */ 47 | private onmouseup(e); 48 | /** 49 | * Delete block through of `delete` button 50 | */ 51 | private onremove(e); 52 | /** 53 | * Destroy the block 54 | */ 55 | destroy(): void; 56 | /** 57 | * Sets the float direction of the block 58 | * @param {String} dir 59 | * @api public 60 | */ 61 | float(dir: string): void; 62 | /** 63 | * Serializes the block 64 | */ 65 | serialize(): Node; 66 | protected onDragEnter(e: DragEvent): void; 67 | protected onDragOver(e: DragEvent): void; 68 | protected onDragLeave(e: DragEvent): void; 69 | protected onDrop(e: DragEvent): void; 70 | } 71 | export = Block; 72 | -------------------------------------------------------------------------------- /block.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/block'); -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | if [ "x$INSIDE_MAKE" = "x" ]; then 4 | if [ "x$MAKE" = "x" ]; then 5 | export MAKE=make 6 | fi 7 | if [ "x$MAKEFLAGS" = "x" ]; then 8 | CPUS=$(node -p "Math.max(1, (require('os').cpus().length / 2) | 0)") 9 | export MAKEFLAGS="-j$CPUS" 10 | fi 11 | touch package.json; 12 | $MAKE $MAKEFLAGS; 13 | for i in `find lib -name *.jadejs`; do mv "$i" "`dirname $i`/`basename $i .jadejs`.js"; done 14 | for i in `find lib -name *.tsjs`; do mv "$i" "`dirname $i`/`basename $i .tsjs`.js"; done 15 | for i in `find lib -name *.es6js`; do mv "$i" "`dirname $i`/`basename $i .es6js`.js"; done 16 | fi 17 | -------------------------------------------------------------------------------- /example/app.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module dependencies 3 | */ 4 | 5 | var Zeditor = require('zeditor'); 6 | var ZeditorPaste = require('zeditor-paste'); 7 | var ZeditorNormalizer = require('zeditor-normalizer'); 8 | 9 | /** 10 | * Get DOM nodes 11 | */ 12 | 13 | var editorNode = document.getElementById('editor'); 14 | 15 | /** 16 | * Instantiate editor 17 | */ 18 | 19 | Zeditor(editorNode); 20 | ZeditorNormalizer(editorNode); 21 | ZeditorPaste(editorNode); 22 | 23 | /** 24 | * Other functionality 25 | */ 26 | 27 | Zeditor(editorNode).on('error', function (err) { 28 | // for now, any "error" event log to the console 29 | console.error('editor "error" event: %o', err); 30 | }); 31 | 32 | var outputNode = document.getElementById('output'); 33 | var showEditorButtonNode = document.getElementById('showEditor'); 34 | var showOutputButtonNode = document.getElementById('showOutput'); 35 | 36 | showEditorButtonNode.addEventListener('click', function (e) { 37 | e.preventDefault(); 38 | 39 | showEditorButtonNode.style.display = 'none'; 40 | editorNode.style.display = 'block'; 41 | 42 | showOutputButtonNode.style.display = 'inline'; 43 | outputNode.style.display = 'none'; 44 | }, false); 45 | 46 | showOutputButtonNode.addEventListener('click', function (e) { 47 | e.preventDefault(); 48 | 49 | showEditorButtonNode.style.display = 'inline'; 50 | editorNode.style.display = 'none'; 51 | 52 | showOutputButtonNode.style.display = 'none'; 53 | outputNode.style.display = 'block'; 54 | 55 | outputNode.textContent = editor.serializer.serializeRoot(); 56 | }, false); -------------------------------------------------------------------------------- /example/example.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | height: 100%; 3 | margin: 0; 4 | padding: 0; 5 | font-size: 100% 6 | } 7 | 8 | body { 9 | font-family: 'Open Sans'; 10 | font-weight: 300; 11 | margin: 0 auto; 12 | background: white; 13 | } 14 | 15 | #version { 16 | font-size: 0.5em; 17 | } 18 | 19 | #top { 20 | padding: 10px; 21 | padding-top: 20px; 22 | color: white; 23 | position: relative; 24 | height: 20px; 25 | } 26 | 27 | #top .admin-bar { 28 | position: absolute; 29 | top: 10px; 30 | right: 5px; 31 | } 32 | 33 | #top span.dashicons-wordpress { 34 | color: black; 35 | font-size: 30px; 36 | position: relative; 37 | float: left; 38 | margin-left: 30px; 39 | } 40 | 41 | #top .admin-bar button { 42 | color: #aaa; 43 | float: right; 44 | font-family: inherit; 45 | text-transform: uppercase; 46 | font-size: 12px; 47 | border: none; 48 | background: none; 49 | font-weight: 700; 50 | padding: 3px 8px; 51 | margin: 0 2px; 52 | letter-spacing: 0.5px; 53 | cursor: pointer; 54 | } 55 | 56 | #top .admin-bar button:hover { 57 | color: black; 58 | } 59 | 60 | #showEditor { 61 | display: none; 62 | } 63 | 64 | *:focus { 65 | outline: 0; 66 | } 67 | 68 | 69 | #editor, 70 | #output { 71 | /*Accomodate ~75characters on one line*/ 72 | padding: 20px 40px; 73 | max-width: 780px; 74 | margin: 0 auto; 75 | } 76 | 77 | #editor { 78 | min-height: 80%; 79 | } 80 | 81 | #output { 82 | box-sizing: border-box; 83 | display: none; 84 | font-family: monospace; 85 | margin-top: 90px; 86 | white-space: pre; 87 | } 88 | 89 | @media only screen and (max-width: 1000px) { 90 | html { 91 | font-size: 95%; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /example/fonts/wpeditor.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/zeditor/8a1b1cec2a0d8f02958aade90ca5b985da2a2034/example/fonts/wpeditor.eot -------------------------------------------------------------------------------- /example/fonts/wpeditor.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Copyright (C) 2014 by original authors @ fontello.com 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /example/fonts/wpeditor.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/zeditor/8a1b1cec2a0d8f02958aade90ca5b985da2a2034/example/fonts/wpeditor.ttf -------------------------------------------------------------------------------- /example/fonts/wpeditor.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Automattic/zeditor/8a1b1cec2a0d8f02958aade90ca5b985da2a2034/example/fonts/wpeditor.woff -------------------------------------------------------------------------------- /example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Editor Standalone Demo Page 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 |
18 | 19 | 20 |
21 |
22 |
23 |
24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/wpeditor.css: -------------------------------------------------------------------------------- 1 | @font-face { 2 | font-family: 'wpeditor'; 3 | src: url('https://cldup.com/kgwiKnjd3v.eot?64900402'); 4 | src: url('https://cldup.com/kgwiKnjd3v.eot?64900402#iefix') format('embedded-opentype'), 5 | url('https://cldup.com/A6Aay116zg.woff?64900402') format('woff'), 6 | url('https://cldup.com/PNaTJ9uXjD.ttf?64900402') format('truetype'), 7 | url('https://cldup.com/elC3Nvu62H.svg?64900402#wpeditor') format('svg'); 8 | font-weight: normal; 9 | font-style: normal; 10 | } 11 | /* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */ 12 | /* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */ 13 | /* 14 | @media screen and (-webkit-min-device-pixel-ratio:0) { 15 | @font-face { 16 | font-family: 'wpeditor'; 17 | src: url('fonts/wpeditor.svg?64900402#wpeditor') format('svg'); 18 | } 19 | } 20 | */ 21 | 22 | [class^="icon-"]:before, [class*=" icon-"]:before { 23 | font-family: "wpeditor"; 24 | font-style: normal; 25 | font-weight: normal; 26 | speak: none; 27 | 28 | display: inline-block; 29 | text-decoration: inherit; 30 | width: 1em; 31 | margin-right: .2em; 32 | text-align: center; 33 | /* opacity: .8; */ 34 | 35 | /* For safety - reset parent styles, that can break glyph codes*/ 36 | font-variant: normal; 37 | text-transform: none; 38 | 39 | /* fix buttons height, for twitter bootstrap */ 40 | line-height: 1em; 41 | 42 | /* Animation center compensation - margins should be symmetric */ 43 | /* remove if not needed */ 44 | margin-left: .2em; 45 | 46 | /* you can be more comfortable with increased icons size */ 47 | /* font-size: 120%; */ 48 | 49 | /* Uncomment for 3D effect */ 50 | /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */ 51 | } 52 | 53 | .icon-circle:before { content: '\e800'; } /* '' */ 54 | .icon-full:before { content: '\e801'; } /* '' */ 55 | .icon-left:before { content: '\e802'; } /* '' */ 56 | .icon-link:before { content: '\e803'; } /* '' */ 57 | .icon-mosaic:before { content: '\e804'; } /* '' */ 58 | .icon-right:before { content: '\e805'; } /* '' */ 59 | .icon-slideshow:before { content: '\e806'; } /* '' */ 60 | .icon-square:before { content: '\e807'; } /* '' */ 61 | .icon-unlink:before { content: '\e808'; } /* '' */ 62 | .icon-del:before { content: '\e809'; } /* '' */ 63 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import events = require('events'); 4 | import OverlayManager = require('../editor-overlay/index'); 5 | import TransactionManager = require('../transaction-manager/index'); 6 | import Tokenizer = require('../tokenizer/index'); 7 | import AutoScroll = require('../auto-scroll/index'); 8 | import Block = require('../block/index'); 9 | 10 | declare class Editor extends events.EventEmitter { 11 | public overlay: OverlayManager; 12 | public transactions: TransactionManager; 13 | public el: HTMLElement; 14 | public wrapper: HTMLElement; 15 | public drag: any; 16 | public site: any; 17 | public wpcom: any; 18 | public media: any; 19 | public mousetrap: any; 20 | public autoscroll: AutoScroll; 21 | public tokens: Tokenizer; 22 | public selection: Range; 23 | public backward: boolean; 24 | public focus(): void; 25 | public addStyles(styles: string): void; 26 | public edit(post: string): void; 27 | public edit(post: Object): void; 28 | public isEmpty(): boolean; 29 | public mousetrapStopCallback(e: Event, element: Node): boolean; 30 | public publish(); 31 | public publish(callback: (err: Error) => void); 32 | public publish(opts: any, callback: (err: Error) => void); 33 | public execute(name: string); 34 | public execute(name: string, val: any); 35 | public use(plugin: (editor: Editor) => void); 36 | public reset(): void; 37 | public on(event: 'contentchange', fn: () => void): Editor; 38 | public on(event: 'selectionchange', fn: () => void): Editor; 39 | public on(event: 'paste', fn: (content: DocumentFragment) => void): Editor; 40 | public on(event: string, fn: (...args: any[]) => any): Editor; 41 | public serialize(): string; 42 | public block(block: Block); 43 | } 44 | 45 | export = Editor; 46 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = require('./lib/editor'); -------------------------------------------------------------------------------- /lib/auto-scroll/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | import scrollTo = require('element-scroll-to'); 4 | 5 | import Editor = require('../editor/index'); 6 | 7 | var SCROLL_TARGET_SIZE = 100; 8 | 9 | class AutoScroll { 10 | 11 | private editor: Editor; 12 | private el: HTMLElement; 13 | private interval: any; 14 | private targetX: number; 15 | private targetY: number; 16 | private currentX: number; 17 | private currentY: number; 18 | private count: number = 0; 19 | 20 | constructor(editor: Editor) { 21 | this.editor = editor; 22 | this.el = document.createElement('div'); 23 | this.el.className = 'scroll-target'; 24 | this.el.style.zIndex = '-1'; 25 | this.el.style.position = 'absolute'; 26 | this.el.style.height = SCROLL_TARGET_SIZE + 'px'; 27 | this.el.style.width = SCROLL_TARGET_SIZE + 'px'; 28 | } 29 | 30 | public target(x: number, y: number): void { 31 | this.targetX = x; 32 | this.targetY = y; 33 | if (typeof this.currentX === 'undefined') this.currentX = x; 34 | if (typeof this.currentY === 'undefined') this.currentY = y; 35 | } 36 | 37 | private update() { 38 | if (typeof this.targetX === 'undefined' || typeof this.targetY === 'undefined') return; 39 | this.currentX = this.currentX * 0.9 + this.targetX * 0.1; 40 | this.currentY = this.currentY * 0.9 + this.targetY * 0.1; 41 | var wrapperRect = this.editor.wrapper.getBoundingClientRect(); 42 | if (this.currentX < wrapperRect.left + SCROLL_TARGET_SIZE / 2) { 43 | this.currentX = wrapperRect.left + SCROLL_TARGET_SIZE / 2; 44 | } 45 | if (this.currentY < wrapperRect.top + SCROLL_TARGET_SIZE / 2) { 46 | this.currentY = SCROLL_TARGET_SIZE / 2; 47 | } 48 | if (this.currentX > wrapperRect.right - SCROLL_TARGET_SIZE / 2) { 49 | this.currentX = wrapperRect.right - SCROLL_TARGET_SIZE / 2; 50 | } 51 | if (this.currentY > wrapperRect.bottom - SCROLL_TARGET_SIZE / 2) { 52 | this.currentY = wrapperRect.bottom - SCROLL_TARGET_SIZE / 2; 53 | } 54 | this.el.style.top = (this.currentY - SCROLL_TARGET_SIZE / 2 - wrapperRect.top) + 'px'; 55 | this.el.style.left = (this.currentX - SCROLL_TARGET_SIZE / 2 - wrapperRect.left) + 'px'; 56 | scrollTo(this.el); 57 | } 58 | 59 | public start(): void { 60 | this.count++; 61 | if (this.count > 0 && !this.interval) { 62 | this.interval = setInterval(() => this.update(), 20); 63 | if (this.el.parentNode != this.editor.wrapper) { 64 | this.editor.wrapper.appendChild(this.el); 65 | } 66 | } 67 | } 68 | 69 | public stop(): void { 70 | this.count--; 71 | if (this.count <= 0 && this.interval) { 72 | clearInterval(this.interval); 73 | this.interval = null; 74 | this.currentX = this.currentY = this.targetX = this.targetY = undefined; 75 | this.editor.wrapper.removeChild(this.el); 76 | } 77 | } 78 | } 79 | 80 | export = AutoScroll; -------------------------------------------------------------------------------- /lib/block-code/code-block.jade: -------------------------------------------------------------------------------- 1 | .block.code-block 2 | include ../block/common 3 | .body 4 | .code-wrapper 5 | .options 6 | select.language 7 | option(value='') Plain Text 8 | optgroup(label="───────") 9 | option(value='css') CSS 10 | option(value='html', data-mode='htmlmixed') HTML 11 | option(value='javascript') JavaScript 12 | option(value='java', data-mode='text/x-java') Java 13 | option(value='c', data-mode='text/x-csrc') C 14 | option(value='cpp', data-mode='text/x-c++src') C++ 15 | option(value='csharp', data-mode='text/x-csharp') C# 16 | option(value='php', data-mode='text/x-php') PHP 17 | option(value='markdown', data-mode='text/x-markdown') Markdown 18 | option(value='diff', data-mode='text/x-diff') Diff 19 | select.indentation 20 | optgroup(label="Spaces") 21 | option 2 22 | option 3 23 | option 4 24 | option 8 25 | optgroup(label="Tabs") 26 | option ⇥ 2 27 | option ⇥ 3 28 | option ⇥ 4 29 | option ⇥ 8 30 | -------------------------------------------------------------------------------- /lib/block-code/code-block.styl: -------------------------------------------------------------------------------- 1 | .automattic-editor-wrapper 2 | .block.code-block 3 | &::before 4 | content: '' 5 | .toolbar 6 | text-align: right 7 | margin: 10px 8 | .code-wrapper 9 | margin: 10px 10px 10px 20px 10 | .options 11 | height: 24px 12 | select 13 | float: left 14 | select.language 15 | width: 120px; 16 | select.indentation 17 | width: 60px; -------------------------------------------------------------------------------- /lib/block-code/codemirror.css: -------------------------------------------------------------------------------- 1 | /* BASICS */ 2 | 3 | .automattic-editor-wrapper .CodeMirror { 4 | /* Set height, width, borders, and global font properties here */ 5 | font-family: 'Menlo', 'Monaco', monospace; 6 | height: auto; 7 | font-size: 13px; 8 | background: transparent !important; 9 | line-height: 1.5 !important; 10 | } 11 | 12 | /* PADDING */ 13 | 14 | .automattic-editor-wrapper .CodeMirror-lines { 15 | padding: 4px 0; /* Vertical padding around content */ 16 | background: white; 17 | } 18 | .automattic-editor-wrapper .CodeMirror pre { 19 | padding: 0 4px; /* Horizontal padding of content */ 20 | } 21 | 22 | .automattic-editor-wrapper .CodeMirror-scrollbar-filler, .automattic-editor-wrapper .CodeMirror-gutter-filler { 23 | background-color: white; /* The little square between H and V scrollbars */ 24 | } 25 | 26 | /* GUTTER */ 27 | 28 | .automattic-editor-wrapper .CodeMirror-gutters { 29 | border-right: 1px solid #ddd; 30 | white-space: nowrap; 31 | } 32 | .automattic-editor-wrapper .CodeMirror-linenumbers {} 33 | .automattic-editor-wrapper .CodeMirror-linenumber { 34 | padding: 2px 3px 0 5px; 35 | min-width: 20px; 36 | text-align: right; 37 | color: #999; 38 | font-size: 10px; 39 | -moz-box-sizing: content-box; 40 | box-sizing: content-box; 41 | } 42 | 43 | .automattic-editor-wrapper .CodeMirror-guttermarker { color: black; } 44 | .automattic-editor-wrapper .CodeMirror-guttermarker-subtle { color: #999; } 45 | 46 | /* CURSOR */ 47 | 48 | .automattic-editor-wrapper .CodeMirror div.CodeMirror-cursor { 49 | border-left: 1px solid black; 50 | } 51 | /* Shown when moving in bi-directional text */ 52 | .automattic-editor-wrapper .CodeMirror div.CodeMirror-secondarycursor { 53 | border-left: 1px solid silver; 54 | } 55 | .automattic-editor-wrapper .CodeMirror.cm-fat-cursor div.CodeMirror-cursor { 56 | width: auto; 57 | border: 0; 58 | background: #7e7; 59 | } 60 | .automattic-editor-wrapper .CodeMirror.cm-fat-cursor div.CodeMirror-cursors { 61 | z-index: 1; 62 | } 63 | 64 | .automattic-editor-wrapper .cm-animate-fat-cursor { 65 | width: auto; 66 | border: 0; 67 | -webkit-animation: automattic-editor-codemirror-blink 1.06s steps(1) infinite; 68 | -moz-animation: automattic-editor-codemirror-blink 1.06s steps(1) infinite; 69 | animation: automattic-editor-codemirror-blink 1.06s steps(1) infinite; 70 | } 71 | @-moz-keyframes automattic-editor-codemirror-blink { 72 | 0% { background: #7e7; } 73 | 50% { background: none; } 74 | 100% { background: #7e7; } 75 | } 76 | @-webkit-keyframes automattic-editor-codemirror-blink { 77 | 0% { background: #7e7; } 78 | 50% { background: none; } 79 | 100% { background: #7e7; } 80 | } 81 | @keyframes automattic-editor-codemirror-blink { 82 | 0% { background: #7e7; } 83 | 50% { background: none; } 84 | 100% { background: #7e7; } 85 | } 86 | 87 | /* Can style cursor different in overwrite (non-insert) mode */ 88 | .automattic-editor-wrapper div.CodeMirror-overwrite div.CodeMirror-cursor {} 89 | 90 | .automattic-editor-wrapper .cm-tab { display: inline-block; text-decoration: inherit; } 91 | 92 | .automattic-editor-wrapper .CodeMirror-ruler { 93 | border-left: 1px solid #ccc; 94 | position: absolute; 95 | } 96 | 97 | /* DEFAULT THEME */ 98 | 99 | .automattic-editor-wrapper .cm-s-default .cm-keyword {font-weight: bold;} 100 | .automattic-editor-wrapper .cm-s-default .cm-atom {color: #F28E20;} 101 | .automattic-editor-wrapper .cm-s-default .cm-number {color: #015A8D;} 102 | .automattic-editor-wrapper .cm-s-default .cm-def {color: #0292C6;} 103 | .automattic-editor-wrapper .cm-s-default .cm-variable, 104 | .automattic-editor-wrapper .cm-s-default .cm-punctuation {} 105 | .automattic-editor-wrapper .cm-s-default .cm-property { font-style: italic; color: #394A60;} 106 | .automattic-editor-wrapper .cm-s-default .cm-operator { color: #F28E20 } 107 | .automattic-editor-wrapper .cm-s-default .cm-variable-2 {} 108 | .automattic-editor-wrapper .cm-s-default .cm-variable-3 {} 109 | .automattic-editor-wrapper .cm-s-default .cm-comment {color: #56BD23;} 110 | .automattic-editor-wrapper .cm-s-default .cm-string {color: #DA5924;} 111 | .automattic-editor-wrapper .cm-s-default .cm-string-2 {color: #0292C6;} 112 | .automattic-editor-wrapper .cm-s-default .cm-meta {color: #02B3E0} 113 | .automattic-editor-wrapper .cm-s-default .cm-qualifier {} 114 | .automattic-editor-wrapper .cm-s-default .cm-builtin {color: #02B3E0;} 115 | .automattic-editor-wrapper .cm-s-default .cm-bracket {color: #FCC000;} 116 | .automattic-editor-wrapper .cm-s-default .cm-tag {color: #0292C6;} 117 | .automattic-editor-wrapper .cm-s-default .cm-attribute {color: #F28E20;} 118 | .automattic-editor-wrapper .cm-s-default .cm-header {color: #015A8D;} 119 | .automattic-editor-wrapper .cm-s-default .cm-quote {color: #394A60;} 120 | .automattic-editor-wrapper .cm-s-default .cm-hr {color: #394A60;} 121 | .automattic-editor-wrapper .cm-s-default .cm-link {color: #394A60;} 122 | 123 | .automattic-editor-wrapper .cm-negative {color: #E1463E;} 124 | .automattic-editor-wrapper .cm-positive {color: #56BD23;} 125 | .automattic-editor-wrapper .cm-header, .automattic-editor-wrapper .cm-strong {font-weight: bold;} 126 | .automattic-editor-wrapper .cm-em {font-style: italic;} 127 | .automattic-editor-wrapper .cm-link {text-decoration: underline;} 128 | .automattic-editor-wrapper .cm-strikethrough {text-decoration: line-through;} 129 | 130 | .automattic-editor-wrapper .cm-s-default .cm-error {color: #E1463E;} 131 | .automattic-editor-wrapper .cm-invalidchar {color: #E1463E;} 132 | 133 | /* Default styles for common addons */ 134 | 135 | .automattic-editor-wrapper div.CodeMirror span.CodeMirror-matchingbracket {color: #0f0;} 136 | .automattic-editor-wrapper div.CodeMirror span.CodeMirror-nonmatchingbracket {color: #f22;} 137 | .automattic-editor-wrapper .CodeMirror-matchingtag { background: rgba(255, 150, 0, .3); } 138 | .automattic-editor-wrapper .CodeMirror-activeline-background {background: #e8f2ff;} 139 | 140 | /* STOP */ 141 | 142 | /* The rest of this file contains styles related to the mechanics of 143 | the editor. You probably shouldn't touch them. */ 144 | 145 | .automattic-editor-wrapper .CodeMirror { 146 | line-height: 1; 147 | position: relative; 148 | overflow: hidden; 149 | background: white; 150 | color: black; 151 | } 152 | 153 | .automattic-editor-wrapper .CodeMirror-scroll { 154 | overflow: scroll !important; /* Things will break if this is overridden */ 155 | /* 30px is the magic margin used to hide the element's real scrollbars */ 156 | /* See overflow: hidden in .CodeMirror */ 157 | margin-bottom: -30px; margin-right: -30px; 158 | padding-bottom: 30px; 159 | height: 100%; 160 | outline: none; /* Prevent dragging from highlighting the element */ 161 | position: relative; 162 | -moz-box-sizing: content-box; 163 | box-sizing: content-box; 164 | } 165 | .automattic-editor-wrapper .CodeMirror-sizer { 166 | position: relative; 167 | border-right: 30px solid transparent; 168 | -moz-box-sizing: content-box; 169 | box-sizing: content-box; 170 | } 171 | 172 | /* The fake, visible scrollbars. Used to force redraw during scrolling 173 | before actuall scrolling happens, thus preventing shaking and 174 | flickering artifacts. */ 175 | .automattic-editor-wrapper .CodeMirror-vscrollbar, .automattic-editor-wrapper .CodeMirror-hscrollbar, .automattic-editor-wrapper .CodeMirror-scrollbar-filler, .automattic-editor-wrapper .CodeMirror-gutter-filler { 176 | position: absolute; 177 | z-index: 6; 178 | display: none; 179 | } 180 | .automattic-editor-wrapper .CodeMirror-vscrollbar { 181 | right: 0; top: 0; 182 | overflow-x: hidden; 183 | overflow-y: scroll; 184 | } 185 | .automattic-editor-wrapper .CodeMirror-hscrollbar { 186 | bottom: 0; left: 0; 187 | overflow-y: hidden; 188 | overflow-x: scroll; 189 | } 190 | .automattic-editor-wrapper .CodeMirror-scrollbar-filler { 191 | right: 0; bottom: 0; 192 | } 193 | .automattic-editor-wrapper .CodeMirror-gutter-filler { 194 | left: 0; bottom: 0; 195 | } 196 | 197 | .automattic-editor-wrapper .CodeMirror-gutters { 198 | position: absolute; left: 0; top: 0; 199 | z-index: 3; 200 | } 201 | .automattic-editor-wrapper .CodeMirror-gutter { 202 | white-space: normal; 203 | height: 100%; 204 | -moz-box-sizing: content-box; 205 | box-sizing: content-box; 206 | display: inline-block; 207 | margin-bottom: -30px; 208 | /* Hack to make IE7 behave */ 209 | *zoom:1; 210 | *display:inline; 211 | } 212 | .automattic-editor-wrapper .CodeMirror-gutter-wrapper { 213 | position: absolute; 214 | z-index: 4; 215 | height: 100%; 216 | } 217 | .automattic-editor-wrapper .CodeMirror-gutter-elt { 218 | position: absolute; 219 | cursor: default; 220 | z-index: 4; 221 | } 222 | 223 | .automattic-editor-wrapper .CodeMirror-lines { 224 | cursor: text; 225 | min-height: 1px; /* prevents collapsing before first draw */ 226 | } 227 | .automattic-editor-wrapper .CodeMirror pre { 228 | /* Reset some styles that the rest of the page might have set */ 229 | -moz-border-radius: 0; -webkit-border-radius: 0; border-radius: 0; 230 | border-width: 0; 231 | background: transparent; 232 | font-family: inherit; 233 | font-size: inherit; 234 | margin: 0; 235 | white-space: pre; 236 | word-wrap: normal; 237 | line-height: inherit; 238 | color: inherit; 239 | z-index: 2; 240 | position: relative; 241 | overflow: visible; 242 | } 243 | .automattic-editor-wrapper .CodeMirror-wrap pre { 244 | word-wrap: break-word; 245 | white-space: pre-wrap; 246 | word-break: normal; 247 | } 248 | 249 | .automattic-editor-wrapper .CodeMirror-linebackground { 250 | position: absolute; 251 | left: 0; right: 0; top: 0; bottom: 0; 252 | z-index: 0; 253 | } 254 | 255 | .automattic-editor-wrapper .CodeMirror-linewidget { 256 | position: relative; 257 | z-index: 2; 258 | overflow: auto; 259 | } 260 | 261 | .automattic-editor-wrapper .CodeMirror-widget {} 262 | 263 | .automattic-editor-wrapper .CodeMirror-measure { 264 | position: absolute; 265 | width: 100%; 266 | height: 0; 267 | overflow: hidden; 268 | visibility: hidden; 269 | } 270 | .automattic-editor-wrapper .CodeMirror-measure pre { position: static; } 271 | 272 | .automattic-editor-wrapper .CodeMirror div.CodeMirror-cursor { 273 | position: absolute; 274 | border-right: none; 275 | width: 0; 276 | } 277 | 278 | .automattic-editor-wrapper div.CodeMirror-cursors { 279 | visibility: hidden; 280 | position: relative; 281 | z-index: 3; 282 | } 283 | .automattic-editor-wrapper .CodeMirror-focused div.CodeMirror-cursors { 284 | visibility: visible; 285 | } 286 | 287 | .automattic-editor-wrapper .CodeMirror-selected { background: Highlight; } 288 | .automattic-editor-wrapper .CodeMirror-focused .CodeMirror-selected { background: Highlight; } 289 | .automattic-editor-wrapper .CodeMirror-crosshair { cursor: crosshair; } 290 | 291 | .automattic-editor-wrapper .cm-searching { 292 | background: #ffa; 293 | background: rgba(255, 255, 0, .4); 294 | } 295 | 296 | /* IE7 hack to prevent it from returning funny offsetTops on the spans */ 297 | .automattic-editor-wrapper .CodeMirror span { *vertical-align: text-bottom; } 298 | 299 | /* Used to force a border model for a node */ 300 | .automattic-editor-wrapper .cm-force-border { padding-right: .1px; } 301 | 302 | @media print { 303 | /* Hide the cursor when printing */ 304 | .automattic-editor-wrapper .CodeMirror div.CodeMirror-cursors { 305 | visibility: hidden; 306 | } 307 | } 308 | 309 | /* See issue #2901 */ 310 | .automattic-editor-wrapper .cm-tab-wrap-hack:after { content: ''; } 311 | 312 | /* Help users use markselection to safely style text background */ 313 | .automattic-editor-wrapper span.CodeMirror-selectedtext { background: none; } 314 | -------------------------------------------------------------------------------- /lib/block-code/command.es6: -------------------------------------------------------------------------------- 1 | import CodeBlock from '.'; 2 | import AbstractCommand from 'abstract-command'; 3 | import WrapCommand from 'wrap-command'; 4 | 5 | class CodeCommand extends AbstractCommand { 6 | 7 | constructor (editor) { 8 | super(document); 9 | this.editor = editor; 10 | this.wrapCommand = new WrapCommand('code'); 11 | } 12 | 13 | _execute(range, value) { 14 | // TODO: add full line/multi line selection check 15 | // to allow converting existing multi-line blocks 16 | if (range.collapsed) { 17 | this.editor.block(new CodeBlock()); 18 | } else { 19 | this.wrapCommand.execute(range, value); 20 | } 21 | } 22 | 23 | _queryState(range) { 24 | return this.wrapCommand.queryState(range); 25 | } 26 | 27 | _queryEnabled(range) { 28 | return this.wrapCommand.queryEnabled(range); 29 | } 30 | } 31 | 32 | export default CodeCommand; 33 | -------------------------------------------------------------------------------- /lib/block-code/index.es6: -------------------------------------------------------------------------------- 1 | import CodeMirror from 'codemirror'; 2 | import Block from '../block'; 3 | import template from './code-block'; 4 | import domify from 'domify'; 5 | import inserted from 'inserted'; 6 | import classes from 'component-classes'; 7 | import {} from 'codemirror/mode/javascript/javascript'; 8 | import {} from 'codemirror/mode/css/css'; 9 | import {} from 'codemirror/mode/htmlmixed/htmlmixed'; 10 | import {} from 'codemirror/mode/clike/clike'; 11 | import {} from 'codemirror/mode/php/php'; 12 | import {} from 'codemirror/mode/markdown/markdown'; 13 | import {} from 'codemirror/mode/diff/diff'; 14 | import Controls from '../block-controls'; 15 | import dataset from 'dataset'; 16 | 17 | var el = domify(template()); 18 | 19 | class CodeBlock extends Block { 20 | constructor() { 21 | super(el.cloneNode(true)); 22 | inserted(this.overlay, this.oninserted.bind(this)); 23 | this.controls = new Controls(); 24 | } 25 | 26 | oninserted() { 27 | // We need to wait here until the overlay is actually visible on the page. 28 | // coremirror will fail to produce a proper layout for the editor if it's 29 | // nested inside an element with `display: none`. 30 | var interval = setInterval(() => { 31 | if (this.overlay.style.display != 'block') { 32 | return; 33 | } 34 | clearInterval(interval); 35 | this.cm = new CodeMirror(this.overlay.querySelector('.code-wrapper'), { 36 | lineNumbers: true, 37 | lineWrapping: true, 38 | tabSize: 2, 39 | mode: '' 40 | }); 41 | this.cm.on('focus', () => classes(this.overlay).add('inner-focused')); 42 | this.cm.on('blur', () => classes(this.overlay).remove('inner-focused')); 43 | this.cm.focus(); 44 | this.body = this.overlay.querySelector('.body'); 45 | this.body.appendChild(this.controls.el); 46 | this.language = this.overlay.querySelector('select.language'); 47 | this.language.value = localStorage.automatticEditorCodeBlockLastUsedLanguage || ''; 48 | this.language.addEventListener('change', (e) => { 49 | localStorage.automatticEditorCodeBlockLastUsedLanguage = this.language.value; 50 | this.languageChanged() 51 | }); 52 | this.languageChanged(); 53 | this.controls.add(this.overlay.querySelector('.options'), false, false); 54 | }, 0); 55 | } 56 | 57 | serialize(context) { 58 | if (context == 'post') { 59 | var result; 60 | if (this.language.value) { 61 | result = `[code language="${this.language.value}"]\n`; 62 | } else { 63 | result = `[code]\n`; 64 | } 65 | result += this.cm.getValue(); 66 | result += `\n[/code]`; 67 | 68 | return result; 69 | } 70 | 71 | var pre = document.createElement('pre'); 72 | pre.appendChild(document.createTextNode(this.cm.getValue())); 73 | return pre; 74 | } 75 | 76 | languageChanged() { 77 | var selected = this.language.querySelector(`option[value=${this.language.value || '""'}]`); 78 | var mode = dataset(selected, 'mode') || this.language.value; 79 | this.cm.setOption('mode', mode); 80 | } 81 | } 82 | 83 | export default CodeBlock; -------------------------------------------------------------------------------- /lib/block-controls/block-controls.styl: -------------------------------------------------------------------------------- 1 | .automattic-editor-wrapper 2 | .block 3 | .controls 4 | background: white 5 | border: 1px solid #ccc 6 | border-radius: 3px 7 | display: inline-block 8 | position: absolute 9 | left: 50% 10 | bottom: 0 11 | padding: 2px 4px 12 | line-height: 0 13 | opacity: 0 14 | transition: 0.5s linear opacity 15 | transform: translate(-50%, 50%) 16 | // the following bogus rule is needed to work around a Firefox rendering bug. 17 | // when translating an element 50% of its width or height, sometimes Firefox 18 | // will miscalculate the bounding box of the element for painting purposes. 19 | // (Most likely due to a rounding error.) Adding an invisible 1px shadow 20 | // extends the bounding box just a bit so it doesn't cut out our border 21 | // See issue #557. 22 | -moz-box-shadow: 0 0 1px transparent; 23 | 24 | &.right 25 | right: 20px 26 | left: auto 27 | transform: translate(0, 50%) 28 | 29 | select 30 | border: none 31 | background: transparent 32 | color: #555 33 | font-family: 'Open Sans' 34 | font-size: 13px 35 | 36 | // control button 37 | a.control 38 | line-height: 15px 39 | display: inline-block 40 | text-align: center 41 | text-decoration: none 42 | color: #ccc 43 | padding: 5px 44 | cursor: default 45 | width: 30px 46 | &:hover 47 | color: #bbb 48 | &.current-action 49 | color: #2ba1cb 50 | 51 | &::before 52 | font-family: 'wpeditor' 53 | content: '❉' 54 | font-size: 120% 55 | 56 | &[data-action='left']::before 57 | content: '\e802' 58 | 59 | &[data-action='right']::before 60 | content: '\e805' 61 | 62 | &[data-action='none']::before 63 | content: '\e801' 64 | 65 | &[data-action='add']::before 66 | content: '+' 67 | position: relative 68 | top: -1px 69 | 70 | &[data-action='rectangular']::before 71 | content: '\e804' 72 | 73 | &[data-action='square']::before 74 | content: '\e807' 75 | 76 | &[data-action='circle']::before 77 | content: '\e800' 78 | 79 | &[data-action='slideshow']::before 80 | content: '\e806' 81 | 82 | &.focused .controls, &.inner-focused .controls 83 | border: 1px solid #2ba1cb 84 | 85 | &:hover .controls 86 | opacity: 1 87 | -------------------------------------------------------------------------------- /lib/block-controls/index.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies 4 | */ 5 | 6 | var classes = require('component-classes'); 7 | var empty = require('component-empty'); 8 | var inherits = require('inherits'); 9 | var EventEmitter = require('events').EventEmitter; 10 | var debug = require('debug')('editor:block-controls'); 11 | var dataset = require('dataset'); 12 | 13 | /** 14 | * Expose `BlockControls` module 15 | */ 16 | 17 | module.exports = BlockControls; 18 | 19 | /** 20 | * Gallery block controls 21 | * 22 | * @api public 23 | */ 24 | 25 | function BlockControls(className){ 26 | EventEmitter.call(this); 27 | this.el = document.createElement('div'); 28 | this.el.className = 'controls ' + (className || ''); 29 | this.el.addEventListener('click', this.onactive.bind(this), false); 30 | } 31 | 32 | /** 33 | * Inherit from `EventEmitter` 34 | */ 35 | 36 | inherits(BlockControls, EventEmitter); 37 | 38 | /** 39 | * Reset layout 40 | */ 41 | 42 | BlockControls.prototype.reset = function(){ 43 | empty(this.el); 44 | }; 45 | 46 | /** 47 | * Add action button 48 | * 49 | * @param {String} action description 50 | * @param {Boolean} [active] 51 | * @param {Boolean} [selectable] 52 | * @api public 53 | */ 54 | 55 | BlockControls.prototype.add = function(action, active, selectable){ 56 | active = !!active; 57 | selectable = false !== selectable; 58 | 59 | debug('add %o button. active: %o. selectable: %o', action, active, selectable); 60 | 61 | var a; 62 | if (typeof action == 'string') { 63 | a = document.createElement('a'); 64 | dataset(a, 'action', action); 65 | a.className = 'control '; 66 | a.className += (selectable ? 'selectable ' : ' ' ) + (active ? 'current-action' : ''); 67 | } else { 68 | a = action; 69 | } 70 | 71 | this.el.appendChild(a); 72 | }; 73 | 74 | /** 75 | * Add action buttons to control 76 | * 77 | * @param {Array} actions 78 | * @param {Number} [active] 79 | * @api public 80 | */ 81 | 82 | BlockControls.prototype.actions = function(actions, active){ 83 | active = active || 0; 84 | for (var i = 0; i < actions.length; i++) { 85 | this.add(actions[i], i == active); 86 | } 87 | }; 88 | 89 | /** 90 | * Set `active` admin element 91 | * 92 | * @param {String} action 93 | * @api public 94 | */ 95 | 96 | BlockControls.prototype.set = function(action){ 97 | var el = this.getElementByAction(action); 98 | if (el) { 99 | debug('set %o action', action); 100 | this.setCurrentAction(el); 101 | 102 | debug('emit %o action event', action); 103 | this.emit(action); 104 | } 105 | }; 106 | 107 | /** 108 | * Set `current` action 109 | * 110 | * @param {Object} el 111 | * @api private 112 | */ 113 | 114 | BlockControls.prototype.setCurrentAction = function(el){ 115 | if (!classes(el).has('selectable')) return; 116 | 117 | var els = el.parentNode.getElementsByClassName('control'); 118 | for (var i = 0; i < els.length; i++) { 119 | classes(els[i]).remove('current-action'); 120 | } 121 | classes(el).add('current-action'); 122 | }; 123 | 124 | /** 125 | * Get element by `data-action` value 126 | * 127 | * @param {String} v 128 | * @api private 129 | */ 130 | 131 | BlockControls.prototype.getElementByAction = function(v){ 132 | var el; 133 | var els = this.el.getElementsByClassName('control'); 134 | 135 | for (var i = 0; i < els.length; i++) { 136 | if (els[i] && dataset(els[i], 'action') == v) { 137 | el = els[i]; 138 | continue; 139 | } 140 | } 141 | return el; 142 | }; 143 | 144 | /** 145 | * Bind `click` event 146 | * 147 | * @param {Object} e 148 | * @api private 149 | */ 150 | 151 | BlockControls.prototype.onactive = function(e){ 152 | if (classes(e.target).has('control')) { 153 | var action = dataset(e.target, 'action'); 154 | this.set(action); 155 | } 156 | }; 157 | -------------------------------------------------------------------------------- /lib/block-html/html-block.jade: -------------------------------------------------------------------------------- 1 | .block.html-block 2 | include ../block/common 3 | .body 4 | -------------------------------------------------------------------------------- /lib/block-html/html-block.styl: -------------------------------------------------------------------------------- 1 | .automattic-editor-wrapper 2 | .block.html-block 3 | &::before 4 | content: '\f123' 5 | min-height: 40px; 6 | padding: 20px 30px; 7 | .body 8 | background: repeating-linear-gradient(to right, 9 | rgba(#d5d5d5,0.4), 10 | rgba(#d5d5d5,0.4) 1px, 11 | transparent 1px, 12 | transparent 6px) 13 | background-size: 30px 30px 14 | background-color: #fafafa 15 | &::after 16 | content: "READ-ONLY" 17 | position: relative 18 | bottom: 3px 19 | right: 8px 20 | float: right 21 | font-weight: bold 22 | opacity: 0.4 23 | letter-spacing: 2px 24 | font-size: 12px 25 | -------------------------------------------------------------------------------- /lib/block-html/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Module Dependencies 3 | */ 4 | 5 | var domify = require('domify'); 6 | var query = require('component-query'); 7 | var inserted = require('inserted'); 8 | var inherits = require('inherits'); 9 | 10 | var Block = require('../block'); 11 | 12 | var el = domify(require('./html-block')()); 13 | 14 | /** 15 | * Represents a Block Holding arbitrary HTML content 16 | */ 17 | 18 | function HTMLBlock(node) { 19 | if (!(this instanceof HTMLBlock)) return new HTMLBlock(node); 20 | 21 | Block.call(this, el.cloneNode(true)); 22 | 23 | this.body = query('.body', this.overlay); 24 | this.node = node; 25 | 26 | inserted(this.overlay, onInserted.bind(this)); 27 | } 28 | 29 | inherits(HTMLBlock, Block); 30 | 31 | /** 32 | * Called whenever we're inserted into the DOM 33 | */ 34 | 35 | function onInserted() { 36 | this.body.appendChild(this.node); 37 | } 38 | 39 | /** 40 | * Serializes the raw HTML inside the block 41 | */ 42 | 43 | HTMLBlock.prototype.serialize = function (context) { 44 | return this.node; 45 | } 46 | 47 | /** 48 | * Exports 49 | */ 50 | 51 | module.exports = HTMLBlock; 52 | -------------------------------------------------------------------------------- /lib/block/block.jade: -------------------------------------------------------------------------------- 1 | .block 2 | include ./common 3 | -------------------------------------------------------------------------------- /lib/block/block.styl: -------------------------------------------------------------------------------- 1 | @keyframes automattic-editor-appear 2 | 0% 3 | opacity: 0 4 | 50% 5 | opacity: 0 6 | 100% 7 | opacity: 1 8 | 9 | .automattic-editor-wrapper 10 | .block 11 | position: relative 12 | width: calc(100% - 2 * 10px) 13 | line-height: inherit 14 | border: 1px solid rgba(0,0,0,0) 15 | border-radius: 3px 16 | background: #fafafa; 17 | transition: background 0.5s linear, border 0.5s linear; 18 | z-index: 1 19 | animation: automattic-editor-appear 0.4s 1 ease-out; 20 | 21 | &.active 22 | .footer 23 | height: 35px 24 | opacity: 1 25 | 26 | .placeholder 27 | color: #777 28 | &:hover 29 | p 30 | color: #444 31 | 32 | /** 33 | * Structure 34 | */ 35 | 36 | & > .gutter, 37 | & > .delete 38 | float: left 39 | margin-left: -40px 40 | width: 40px 41 | 42 | & > .body 43 | float: left 44 | width: 100% 45 | min-height: 25px 46 | 47 | p 48 | margin: 0 49 | padding: 0 50 | width: 100% 51 | min-height: 1em 52 | 53 | & > .footer 54 | width: 100% 55 | text-align: center 56 | -webkit-transition: all .2s linear 57 | transition: all .2s linear 58 | height: 0 59 | overflow: hidden 60 | 61 | /** 62 | * Editor block admin buttons 63 | */ 64 | 65 | .grabber, 66 | .delete-button 67 | display: none 68 | color: #ccc 69 | position: absolute 70 | cursor: -webkit-grab 71 | cursor: -moz-grab 72 | cursor: grab 73 | left: 7px 74 | width: 15px 75 | height: 20px 76 | line-height: 20px 77 | text-align: center 78 | -webkit-touch-callout: none 79 | outline-style:none /*IE*/ 80 | z-index: 100 81 | &:before 82 | font: normal 14px/1 'dashicons' 83 | 84 | 85 | // Grabber button 86 | .grabber 87 | cursor: move 88 | top: 8px 89 | &:before 90 | font-size: 22px 91 | content: '\022EE' 92 | 93 | // Delete button 94 | .delete-button 95 | cursor: pointer 96 | bottom: 6px 97 | &:before 98 | content: '\f335' 99 | 100 | &:hover 101 | border: 1px solid #ccc 102 | background: white 103 | 104 | &:hover .grabber, 105 | &:hover .delete-button 106 | display: block 107 | 108 | &.focused, &.inner-focused 109 | box-shadow: 0 0 0 1px #2ba1cb 110 | transition: background 0.5s linear 111 | 112 | &.focused .grabber, &.inner-focused .grabber 113 | display: block 114 | color: #2ba1cb 115 | 116 | &.selected::after 117 | content: '' 118 | position: absolute 119 | left: 0 120 | top: 0 121 | width: 100% 122 | height: 100% 123 | background: Highlight 124 | mix-blend-mode: multiply; 125 | opacity: 0.75 126 | border-radius: 4px 127 | 128 | &.focused:hover, &.inner-focused, &.selected:hover 129 | border: 1px solid transparent 130 | 131 | .delete-button, .grabber 132 | &:hover 133 | color: black 134 | 135 | // &.grab-hover 136 | 137 | &.dragging 138 | box-shadow: none 139 | background: none !important 140 | 141 | & .body 142 | transition: 0.4s cubic-bezier(0.5,-0.5,0.5,1.5) transform, 0.4s ease opacity 143 | opacity: 1 144 | transform: perspective(900px) 145 | 146 | &.dragging 147 | .body 148 | opacity: 0.3 149 | transform: perspective(900px) /*rotateX(-10deg)*/ scale(0.95) 150 | .grabber, .delete-button 151 | opacity: 0 152 | &::before 153 | font-family: 'dashicons' 154 | content: '\f138' 155 | position: absolute 156 | right: 8px 157 | top: 8px 158 | font-size: 14pt 159 | opacity: 0.2 160 | transition: 0.5s linear opacity 161 | &:hover::before 162 | opacity: 0.5 163 | 164 | & .caret-before-button, & .caret-after-button 165 | position: absolute 166 | width: 100% 167 | left: 0 168 | cursor: text 169 | 170 | & .caret-before-button 171 | bottom: 100% 172 | height: 20px 173 | 174 | & .caret-after-button 175 | top: 100% 176 | height: 30px 177 | -------------------------------------------------------------------------------- /lib/block/common.jade: -------------------------------------------------------------------------------- 1 | .grabber 2 | .delete 3 | .delete-button.dashicons.dashicons-no-alt 4 | .caret-before-button 5 | .caret-after-button 6 | -------------------------------------------------------------------------------- /lib/block/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | /** 3 | * TypeScript dependencies 4 | */ 5 | import Editor = require('../editor/index'); 6 | import events = require('events'); 7 | declare class Block extends events.EventEmitter { 8 | /** 9 | * Private Fields 10 | */ 11 | private _editor; 12 | private _overlay; 13 | private _el; 14 | private _id; 15 | private _hold; 16 | private _holdX; 17 | private _holdY; 18 | constructor(overlay: HTMLElement, editor: Editor); 19 | /** 20 | * Returns the current overlay reference for the block, searching 21 | * through the DOM of the editor to do so. 22 | */ 23 | el: HTMLElement; 24 | /** 25 | * Returns the current overlay for the block 26 | */ 27 | overlay: HTMLElement; 28 | /** 29 | * Returns the bound editor for the block 30 | */ 31 | editor: Editor; 32 | /** 33 | * Binds the block to the editor instance 34 | */ 35 | bind(editor: Editor): void; 36 | /** 37 | * Fired when the mouse button is pressed on the block 38 | */ 39 | private onmousedown(e); 40 | /** 41 | * Fired when the mouse moves 42 | */ 43 | private onmousemove(e); 44 | /** 45 | * Fired when the mouse button is lifted 46 | */ 47 | private onmouseup(e); 48 | /** 49 | * Delete block through of `delete` button 50 | */ 51 | private onremove(e); 52 | /** 53 | * Destroy the block 54 | */ 55 | destroy(): void; 56 | /** 57 | * Sets the float direction of the block 58 | * @param {String} dir 59 | * @api public 60 | */ 61 | float(dir: string): void; 62 | /** 63 | * Serializes the block 64 | */ 65 | serialize(): Node; 66 | protected onDragEnter(e: DragEvent): void; 67 | protected onDragOver(e: DragEvent): void; 68 | protected onDragLeave(e: DragEvent): void; 69 | protected onDrop(e: DragEvent): void; 70 | } 71 | export = Block; 72 | -------------------------------------------------------------------------------- /lib/editor-keyboard-shortcuts/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /** 4 | * Module dependencies. 5 | */ 6 | 7 | import Command = require('webmodules-command'); 8 | import DEBUG = require('debug'); 9 | 10 | var debug = DEBUG('editor:editor-keyboard-shortcuts'); 11 | 12 | /** 13 | * `Editor` plugin that maps keyboard shortcuts to Command invokations. 14 | * i.e. "super + b" would map to the "bold" command (`editor.commands.bold`). 15 | * 16 | * @param {Object} map - map of shortcut keys to command names 17 | * @return {Function} the editor plugin function 18 | * @public 19 | */ 20 | 21 | function setup (map): (editor)=>void { 22 | 23 | return function (editor) { 24 | var command: Command; 25 | var name: string; 26 | 27 | for (var key in map) { 28 | name = map[key]; 29 | if (!name) { 30 | debug('skipping %o since no command name given', key); 31 | continue; 32 | } 33 | 34 | command = editor.commands[name]; 35 | if (!command) { 36 | debug('skipping %o since it is %o', key, command); 37 | continue; 38 | } 39 | 40 | // bind key combo listener 41 | editor.mousetrap.bind(key, listener(key, command)); 42 | } 43 | }; 44 | 45 | } 46 | 47 | function listener (key: string, command: Command): (e: Event)=>void { 48 | return function (e) { 49 | e.preventDefault(); 50 | 51 | if (command.queryEnabled()) { 52 | debug('executing %o %o command', key, command); 53 | // `editor.focus()` isn't actually necessary here, since the keypress 54 | // listener wouldn't have been invoked in the first place if the 55 | // editor wasn't focused when the shortcut keys were pressed in. 56 | command.execute(); 57 | } 58 | }; 59 | } 60 | 61 | export = setup; 62 | -------------------------------------------------------------------------------- /lib/editor-link-tooltip/link-tooltip.jade: -------------------------------------------------------------------------------- 1 | form.link-tooltip 2 | a.link(href="#", target="_blank") 3 | input(type="text", value="", name="link", placeholder="Add a Link", autocomplete="off") 4 | span.state 5 | a.done(href="#") 6 | .dashicons.dashicons-yes 7 | a.change(href="#") 8 | .noticon.noticon-edit 9 | -------------------------------------------------------------------------------- /lib/editor-link-tooltip/link-tooltip.styl: -------------------------------------------------------------------------------- 1 | .link-tooltip 2 | height: 44px 3 | line-height: 44px 4 | padding-left: 8px 5 | .state 6 | padding-left: 10px 7 | 8 | .link-tooltip 9 | input[type=text], .done 10 | display: none 11 | box-shadow: none 12 | .link, .change 13 | display: inline-block 14 | color: #2ba1cb 15 | .change 16 | font-weight: 600 17 | 18 | .link-tooltip input[type=text]::-ms-clear 19 | display: none 20 | 21 | .link-tooltip.edit 22 | input[type=text], .done 23 | display: inline-block 24 | .link, .change 25 | display: none 26 | 27 | .link-tooltip > .state 28 | float: right 29 | color: black 30 | 31 | .link-tooltip > .state > a 32 | text-decoration: none 33 | color: black 34 | 35 | .link-tooltip > .state > .done > div 36 | height: inherit 37 | width: inherit 38 | 39 | .link-tooltip > .state > .done > .dashicons-yes 40 | font-size: 24px 41 | position: relative 42 | line-height: 44px 43 | width: 40px 44 | 45 | .link-tooltip > .state > .change > .noticon-edit 46 | font-size: 18px 47 | line-height: 44px 48 | width: 40px 49 | 50 | .link-tooltip > .state > .done.disabled 51 | opacity: 0.4 52 | 53 | .link-tooltip > .link 54 | max-width: 300px 55 | white-space: nowrap 56 | text-overflow: ellipsis 57 | overflow: hidden 58 | 59 | .link-tooltip > input[type=text] 60 | background-color: transparent 61 | border: none 62 | outline: none 63 | color: #333 64 | margin: 0 65 | padding: 0 66 | width: inherit 67 | 68 | .link-tooltip 69 | > input[type=text], > a 70 | height: inherit 71 | padding-left: 2px 72 | font-size: 15px 73 | font-family: 'Open Sans', sans-serif 74 | font-weight: normal 75 | letter-spacing: 0.02em 76 | -------------------------------------------------------------------------------- /lib/editor-overlay/editor-overlay.styl: -------------------------------------------------------------------------------- 1 | .automattic-editor-wrapper 2 | .overlay-reference 3 | margin: 0 0 28px 0 4 | clear: both 5 | opacity: 0 6 | 7 | &.left 8 | margin-right: 20px 9 | width: calc(60% - 2 * 10px) 10 | float: left 11 | 12 | &.right 13 | margin-left: 20px 14 | width: calc(60% - 2 * 10px) 15 | float: right 16 | 17 | /** 18 | * Clearfix 19 | */ 20 | 21 | &:after, 22 | &:before 23 | content: " " 24 | display: table 25 | 26 | &:after 27 | clear: both 28 | -------------------------------------------------------------------------------- /lib/editor-overlay/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /** 4 | * Module dependencies 5 | */ 6 | 7 | import Editor = require('../editor/index'); 8 | import classes = require('component-classes'); 9 | import query = require('component-query'); 10 | import computedStyle = require('computed-style'); 11 | import raf = require('raf'); 12 | import uid = require('component-uid'); 13 | import dataset = require('dataset'); 14 | import hacks = require('../hacks/index'); 15 | 16 | var TRANSFORM = ['transform', 'webkitTransform', 'MozTransform', 'msTransform', 'OTransform'].filter((prop) => document.body.style[prop] != null)[0]; 17 | 18 | /** 19 | * Editor overlay 20 | * 21 | * @param {Element} el 22 | * @api public 23 | */ 24 | 25 | class EditorOverlayManager { 26 | 27 | private editor: Editor; 28 | private ref: HTMLElement; 29 | private el: HTMLElement; 30 | private overlays: Object; 31 | private interval: number; 32 | private raf: any; 33 | private timer: any; 34 | 35 | constructor(editor: Editor) { 36 | if (!(this instanceof EditorOverlayManager)) return new EditorOverlayManager(editor); 37 | 38 | this.editor = editor; 39 | this.ref = editor.el; 40 | this.el = document.createElement('div'); 41 | this.el.className = 'editor-overlay'; 42 | this.overlays = {}; 43 | this.interval = 0; 44 | this.raf = null; 45 | this.timer = null; 46 | 47 | // cache the bound callback, to avoid binding it every time 48 | this['callback'] = this.callback.bind(this); 49 | 50 | this.el.addEventListener('resize', () => { 51 | this.update(true); 52 | }); 53 | 54 | this.timeout(); 55 | } 56 | 57 | /** 58 | * Creates a reference element for an overlay element 59 | */ 60 | 61 | public reference(overlay: HTMLElement): HTMLElement { 62 | var el = document.createElement('div'); 63 | el.className = 'overlay-reference'; 64 | var br = document.createElement('br'); 65 | el.appendChild(br); 66 | var id = uid(8); 67 | dataset(el, 'id', id); 68 | this.add(id, overlay); 69 | return el; 70 | } 71 | 72 | /** 73 | * Adds an overlay element to the overlay manager 74 | */ 75 | 76 | private add(id: string, overlay: HTMLElement): void { 77 | overlay.style.display = 'none'; 78 | this.overlays[id] = overlay; 79 | this.el.appendChild(overlay); 80 | } 81 | 82 | /** 83 | * Get the overlay for the given reference 84 | */ 85 | 86 | public 'for'(el: HTMLElement): HTMLElement { 87 | return this.overlays[dataset(el, 'id')]; 88 | } 89 | 90 | /** 91 | * Update the position of the overlays based on the 92 | * positions of the references 93 | */ 94 | 95 | public update(changed: boolean = false): void { 96 | var i: number; 97 | var id: string; 98 | var ref: HTMLElement; 99 | var overlay: HTMLElement; 100 | var refBox: ClientRect; 101 | 102 | // mark all overlays as not present 103 | for (id in this.overlays) { 104 | overlay = this.overlays[id]; 105 | overlay['present'] = false; 106 | } 107 | 108 | var refs = query.all('[data-id]', this.ref); 109 | 110 | // iterate through all overlay references and 111 | // set the overlay widths based on their widths 112 | for (i = 0; i < refs.length; i++) { 113 | ref = refs[i]; 114 | id = ref.getAttribute('data-id'); 115 | overlay = this.overlays[id]; 116 | 117 | if (overlay) { 118 | refBox = ref.getBoundingClientRect(); 119 | if (overlay.style.display != 'block') { 120 | overlay.style.display = 'block'; 121 | changed = true; 122 | } 123 | var width = refBox.width + 'px'; 124 | if (overlay.style.width != width) { 125 | overlay.style.width = width; 126 | changed = true; 127 | } 128 | // mark found overlay as present 129 | overlay['present'] = true; 130 | } 131 | } 132 | 133 | // iterate through all overlay references 134 | // and set their heights based on the overlay heights 135 | for (i = 0; i < refs.length; i++) { 136 | ref = refs[i]; 137 | id = ref.getAttribute('data-id'); 138 | overlay = this.overlays[id]; 139 | 140 | if (overlay) { 141 | var overlayBox = overlay.getBoundingClientRect(); 142 | 143 | if (hacks.overlayReferenceUsePadding) { 144 | refBox = ref.getBoundingClientRect(); 145 | var paddingBottom = Math.max(0, parseInt(ref.style.paddingBottom || '0px', 10) + Math.round(overlayBox.height - refBox.height)) + 'px'; 146 | if (ref.style.paddingBottom != paddingBottom) { 147 | this.editor.transactions.runAndSquash(() => { 148 | ref.style.paddingBottom = paddingBottom; 149 | }); 150 | changed = true; 151 | } 152 | } else { 153 | var height = overlayBox.height + 'px'; 154 | if (ref.style.height != height) { 155 | this.editor.transactions.runAndSquash(() => { 156 | ref.style.height = height; 157 | }); 158 | changed = true; 159 | } 160 | } 161 | } 162 | } 163 | 164 | // iterate through all overlay references and 165 | // set their heights based on the overlay heights 166 | var externalBox = this.el.getBoundingClientRect(); 167 | for (i = 0; i < refs.length; i++) { 168 | ref = refs[i]; 169 | id = ref.getAttribute('data-id'); 170 | overlay = this.overlays[id]; 171 | if (overlay) { 172 | refBox = ref.getBoundingClientRect(); 173 | if (overlay.style.position != 'absolute') { 174 | overlay.style.position = 'absolute'; 175 | changed = true; 176 | } 177 | var top = (refBox.top - externalBox.top) + 'px'; 178 | var left = (refBox.left - externalBox.left) + 'px'; 179 | var transform = 'translate(' + left + ', ' + top + ')'; 180 | 181 | if (overlay.style[TRANSFORM] != transform) { 182 | overlay.style[TRANSFORM] = transform; 183 | changed = true; 184 | } 185 | } 186 | } 187 | 188 | // hide all non-present overlays 189 | for (id in this.overlays) { 190 | overlay = this.overlays[id]; 191 | if (!overlay['present']) { 192 | if (overlay.style.display != 'none') { 193 | overlay.style.display = 'none'; 194 | changed = true; 195 | } 196 | } 197 | } 198 | 199 | this.updateSelection(); 200 | 201 | this.timeout(changed); 202 | } 203 | 204 | /** 205 | * Updates the overlay classes to make them react to 206 | * text selections 207 | */ 208 | 209 | public updateSelection(): void { 210 | 211 | function rangeIntersectsNode(range: Range, node: Node): boolean { 212 | var nodeRange = node.ownerDocument.createRange(); 213 | try { 214 | nodeRange.selectNode(node); 215 | } catch (e) { 216 | nodeRange.selectNodeContents(node); 217 | } 218 | 219 | var rangeStartRange = range.cloneRange(); 220 | rangeStartRange.collapse(true); 221 | 222 | var rangeEndRange = range.cloneRange(); 223 | rangeEndRange.collapse(false); 224 | 225 | var nodeStartRange = nodeRange.cloneRange(); 226 | nodeStartRange.collapse(true); 227 | 228 | var nodeEndRange = nodeRange.cloneRange(); 229 | nodeEndRange.collapse(false); 230 | 231 | return rangeStartRange.compareBoundaryPoints(Range.START_TO_START, nodeEndRange) == -1 && 232 | rangeEndRange.compareBoundaryPoints(Range.START_TO_START, nodeStartRange) == 1; 233 | } 234 | 235 | var ref; 236 | var i; 237 | var refs = query.all('[data-id]', this.ref); 238 | var id; 239 | 240 | // iterate through all overlay references and 241 | // mark all overlays as not selected or focused 242 | for (i = 0; i < refs.length; i++) { 243 | ref = refs[i]; 244 | id = ref.getAttribute('data-id'); 245 | overlay = this.overlays[id]; 246 | classes(overlay).remove('focused').remove('selected'); 247 | } 248 | 249 | var selection = window.getSelection(); 250 | if (selection.rangeCount == 0) return; 251 | var range = selection.getRangeAt(0); 252 | if (range.collapsed) { 253 | // check for focused overlay references 254 | var el = range.startContainer; 255 | do { 256 | if (el.nodeType == Node.ELEMENT_NODE && 257 | (id = (el).getAttribute('data-id'))) { 258 | var overlay = this.overlays[id]; 259 | if (overlay) { 260 | classes(overlay).add('focused'); 261 | } 262 | } 263 | } while (el = el.parentNode); 264 | } else { 265 | // iterate through all overlay references and 266 | // check which of them intersect the range 267 | for (i = 0; i < refs.length; i++) { 268 | ref = refs[i]; 269 | if (rangeIntersectsNode(range, ref)) { 270 | id = ref.getAttribute('data-id'); 271 | overlay = this.overlays[id]; 272 | classes(overlay).add('selected'); 273 | } 274 | } 275 | } 276 | } 277 | 278 | /** 279 | * Called when the timer or raf fires 280 | */ 281 | 282 | private callback() { 283 | this.raf = null; 284 | this.timer = null; 285 | this.update(); 286 | } 287 | 288 | /** 289 | * Sets a timeout to update the overlay positions in the future 290 | */ 291 | 292 | private timeout(changed: boolean = false): void { 293 | if (changed) { 294 | this.interval = 0; 295 | if (this.raf && window.cancelAnimationFrame) { 296 | raf.cancel(this.raf); 297 | } 298 | if (this.timer) { 299 | clearTimeout(this.timer); 300 | } 301 | } else if (this.timer || this.raf) return; 302 | 303 | if (this.interval <= 200) { 304 | // intervals lower or equal to 200ms trigger an animation frame 305 | this.raf = raf(this.callback); 306 | } else { 307 | // intervals higher than that will trigger a regular timeout 308 | this.timer = setTimeout(this.callback, this.interval); 309 | } 310 | 311 | // increase interval, but max at 1000ms 312 | if (this.interval < 1000) { 313 | this.interval += 25; 314 | } 315 | } 316 | } 317 | 318 | /** 319 | * Expose `EditorOverlayManager` 320 | */ 321 | 322 | export = EditorOverlayManager; 323 | -------------------------------------------------------------------------------- /lib/editor-selection/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /** 4 | * TypeScript imports 5 | */ 6 | 7 | import currentRange = require('current-range'); 8 | import currentSelection = require('current-selection'); 9 | import getDocument = require('get-document'); 10 | import normalize = require('range-normalize'); 11 | import contains = require('node-contains'); 12 | import isBackward = require('selection-is-backward'); 13 | import selectionSetRange = require('selection-set-range'); 14 | import selectionchangePolyfill = require('selectionchange-polyfill'); 15 | import rangeEquals = require('range-equals'); 16 | import DEBUG = require('debug'); 17 | 18 | import Editor = require('../editor/index'); 19 | 20 | var debug = DEBUG('editor:editor-selectionchange'); 21 | 22 | /** 23 | * Editor plugin that emits a `selectionchange` event on the Editor instance 24 | * whenever the user changes the selection within the editor instance. 25 | * 26 | * The Range gets "normalized" via `range-normalize` module before being set 27 | * as the document's Selection, and set on the editor at `editor.selection`. 28 | * 29 | * @public 30 | */ 31 | 32 | function setup (): (editor: Editor)=>void { 33 | return function (editor: Editor): void { 34 | editor.on('focus', onfocus); 35 | editor.once('destroy', cleanup); 36 | 37 | var doc = getDocument(editor.el); 38 | var setting: boolean = false; 39 | 40 | // start the "selectionchange" event polyfill (for older browsers) 41 | selectionchangePolyfill.start(doc); 42 | 43 | var previousSelection: Range = null; 44 | 45 | doc.addEventListener('selectionchange', onselectionchange); 46 | 47 | function onfocus () { 48 | if (previousSelection) { 49 | var backward = (previousSelection).backward || false; 50 | var selection = currentSelection(this.el); 51 | debug('restoring previous selection: %o backward=%o', previousSelection.toString(), backward); 52 | selectionSetRange(selection, previousSelection, backward); 53 | } 54 | } 55 | 56 | function onselectionchange (e): void { 57 | if (setting) { 58 | return debug('ignoring "selectionchange" event since in the middle of setting the Selection'); 59 | } 60 | 61 | // flag to indicate that we're currently in the middle of setting 62 | // the Selection. That way, if another "selectionchange" event 63 | // fires from us modifying this selection, then we won't get into 64 | // a recursive loop. 65 | setting = true; 66 | debug('setting = true'); 67 | 68 | var selection: Selection = currentSelection(doc); 69 | if (!selection) { 70 | setting = false; 71 | debug('setting = false'); 72 | return debug('bailing, no current Selection'); 73 | } 74 | 75 | var range: Range = currentRange(selection); 76 | if (!range) { 77 | setting = false; 78 | debug('setting = false'); 79 | return debug('bailing, no current Range'); 80 | } 81 | 82 | var oldRange: Range = editor.selection; 83 | var needsEmit: boolean = false; 84 | 85 | if (contains(editor.el, range.commonAncestorContainer)) { 86 | previousSelection = null; 87 | range = normalize(range.cloneRange()); 88 | needsEmit = !rangeEquals(range, oldRange); 89 | if (needsEmit) { 90 | editor.selection = range; 91 | editor.backward = (editor.selection).backward = isBackward(selection); 92 | } 93 | } else { 94 | debug('document Selection is not inside the Editor'); 95 | previousSelection = editor.selection; 96 | editor.selection = null; 97 | needsEmit = !!oldRange; 98 | } 99 | 100 | if (needsEmit) editor.emit('selectionchange'); 101 | 102 | setting = false; 103 | debug('setting = false'); 104 | } 105 | 106 | function cleanup (): void { 107 | debug('editor-selectionchange "cleanup"'); 108 | selectionchangePolyfill.stop(doc); 109 | doc.removeEventListener('selectionchange', onselectionchange); 110 | } 111 | } 112 | } 113 | 114 | export = setup; 115 | -------------------------------------------------------------------------------- /lib/editor-serializer/index.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /** 4 | * Module dependencies 5 | */ 6 | 7 | import dataset = require('dataset'); 8 | import query = require('component-query'); 9 | import matches = require('matches-selector'); 10 | import domSerialize = require('dom-serialize'); 11 | import DEBUG = require('debug'); 12 | 13 | /** 14 | * Local imports 15 | */ 16 | 17 | import Editor = require('../editor/index') 18 | 19 | var debug = DEBUG('editor:serializer'); 20 | 21 | class Serializer { 22 | 23 | private editor: Editor; 24 | 25 | // a selector to match temporary objects that should be 26 | // removed from the markup during serialization 27 | private temporary: string; 28 | 29 | public constructor(editor: Editor) { 30 | this.editor = editor; 31 | this.temporary = 'span.join-hint, span.gallery-tmp-placeholder'; 32 | } 33 | 34 | private spaces(s: string, replaceFirst: boolean, replaceLast: boolean): string { 35 | if (replaceFirst) { 36 | s = s.replace(/^ /g, ' '); 37 | } 38 | if (replaceLast) { 39 | s = s.replace(/ $/g, ' '); 40 | } 41 | s = s.replace(/ /g, '  '); 42 | return s; 43 | } 44 | 45 | /** 46 | * Serializes a DOM node. Emits a "node" event, and a node-specific event 47 | * (i.e. "text", "element") for plugins to hook in to. 48 | * 49 | * Plugins may specify a different node to apply the serialization rules to 50 | * by setting the `serialize` property on the `Event` object provided. 51 | * 52 | * Useful for plugins. 53 | * 54 | * @param {Node} node - DOM node (TextNode, HTMLElement, etc.) 55 | * @return {String} serialized string of `node` 56 | * @public 57 | */ 58 | 59 | public serialize(node: Node|NodeList|Array, context: string = 'post'): string { 60 | return domSerialize(node, context, (e) => { 61 | var target: Node = e.serializeTarget; 62 | 63 | if (target.nodeType === 1 /* element */) { 64 | // `data-serialize` is a raw string to use as the serialized content 65 | var data; 66 | if (data = dataset(target, 'serialize')) { 67 | debug('using `data-serialize` attribute for %o: %o', target, data); 68 | e.detail.serialize = data; 69 | 70 | } else if (matches(target, this.temporary)) { 71 | // don't render anything for "temporary" nodes 72 | // TODO: move this logic to whoever is responsible for these "temporary 73 | // elements" 74 | e.preventDefault(); 75 | } 76 | 77 | } else if (target.nodeType === 3 /* text node */) { 78 | // use our "text node" serializer logic 79 | e.detail.serialize = this.serializeTextNode(target); 80 | 81 | } else { 82 | debug('ignoring serialization of Node: %o', node); 83 | e.preventDefault(); 84 | } 85 | 86 | }); 87 | } 88 | 89 | public serializeTextNode(text: Text): string { 90 | var content: string = domSerialize.serializeText(text, { named: false }); 91 | content = this.spaces(content, !text.previousSibling, !text.nextSibling); 92 | return content; 93 | } 94 | 95 | /** 96 | * Processes the root node of the Editor instance's children, 97 | * producing the final HTML string to be saved. 98 | * 99 | * @return {String} serialized editor contents 100 | * @public 101 | */ 102 | 103 | public serializeRoot(): string { 104 | 105 | // 106 | // IMPORTANT: this function shouldn't modify the markup it's operating on 107 | // as it's going to potentially be called async via a timer for auto-save 108 | // 109 | 110 | var result = this.serialize(this.editor.el.childNodes); 111 | 112 | // remove BR nodes 113 | result = result.replace(/<\/?br\s?>/g, ''); 114 | 115 | return result; 116 | } 117 | 118 | } 119 | 120 | export = Serializer; 121 | -------------------------------------------------------------------------------- /lib/editor-tip/editor-tip.styl: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * "editor-tip" theme for `component/tip`. 4 | * 5 | * .tip.tip-[direction] 6 | * .tip-inner 7 | * .tip-arrow 8 | */ 9 | 10 | .editor-tip 11 | font-size: 11px 12 | 13 | .editor-tip * 14 | box-shadow: none 15 | 16 | .editor-tip .tip-inner 17 | background-color: #fff 18 | border: 1px solid #bababa 19 | border-radius: 4px 20 | box-shadow: 0px 2px 5px rgba(0,0,0,0.2) 21 | text-align: center 22 | 23 | .editor-tip .tip-arrow 24 | position: absolute 25 | width: 0 26 | height: 0 27 | line-height: 0 28 | border: 8px dashed #bababa 29 | 30 | .editor-tip.tip-top .tip-arrow, 31 | .editor-tip.tip-top-left .tip-arrow, 32 | .editor-tip.tip-top-right .tip-arrow 33 | bottom: -2px 34 | left: 50% 35 | margin-left: -8px 36 | border-top-style: solid 37 | border-bottom: none 38 | border-left-color: transparent 39 | border-right-color: transparent 40 | &::before 41 | content: " " 42 | position: absolute 43 | border: 6px solid white 44 | bottom: 2px 45 | left: 50% 46 | margin-left: -6px 47 | border-top-style: solid 48 | border-bottom: none 49 | border-left-color: transparent 50 | border-right-color: transparent 51 | 52 | .editor-tip.tip-bottom .tip-arrow, 53 | .editor-tip.tip-bottom-left .tip-arrow, 54 | .editor-tip.tip-bottom-right .tip-arrow 55 | top: -2px 56 | left: 50% 57 | margin-left: -8px 58 | border-bottom-style: solid 59 | border-top: none 60 | border-left-color: transparent 61 | border-right-color: transparent 62 | &::before 63 | content: " " 64 | position: absolute 65 | border: 6px solid white 66 | top: 2px 67 | left: 50% 68 | margin-left: -6px 69 | border-bottom-style: solid 70 | border-top: none 71 | border-left-color: transparent 72 | border-right-color: transparent 73 | 74 | .editor-tip.tip-left .tip-arrow 75 | right: -2px 76 | top: 50% 77 | margin-top: -8px 78 | border-left-style: solid 79 | border-right: none 80 | border-top-color: transparent 81 | border-bottom-color: transparent 82 | &::before 83 | content: " " 84 | position: absolute 85 | border: 6px solid white 86 | right: 2px 87 | top: 50% 88 | margin-top: -6px 89 | border-left-style: solid 90 | border-right: none 91 | border-top-color: transparent 92 | border-bottom-color: transparent 93 | 94 | .editor-tip.tip-right .tip-arrow 95 | left: -2px 96 | top: 50% 97 | margin-top: -8px 98 | border-right-style: solid 99 | border-left: none 100 | border-top-color: transparent 101 | border-bottom-color: transparent 102 | &::before 103 | content: " " 104 | position: absolute 105 | border: 6px solid white 106 | left: 2px 107 | top: 50% 108 | margin-top: -6px 109 | border-right-style: solid 110 | border-left: none 111 | border-top-color: transparent 112 | border-bottom-color: transparent 113 | 114 | .editor-tip.tip-top-left .tip-arrow, 115 | .editor-tip.tip-bottom-left .tip-arrow 116 | right: 25px 117 | 118 | .editor-tip.tip-top-right .tip-arrow, 119 | .editor-tip.tip-bottom-right .tip-arrow 120 | left: 25px 121 | -------------------------------------------------------------------------------- /lib/editor-tip/index.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var Tip = require('component-tip'); 7 | var inherits = require('inherits'); 8 | var clickOutside = require('click-outside'); 9 | var debug = require('debug')('editor:editor-tip'); 10 | 11 | /** 12 | * Module exports. 13 | */ 14 | 15 | module.exports = EditorTip; 16 | 17 | /** 18 | * Editor's `Tip` extends the `component/tip` module. 19 | * 20 | * Our version implements the "click outside" logic to hide the Tip when the user 21 | * clicks outside. 22 | * 23 | * @param {String} html - HTML string to render inside of the Tip 24 | * @param {String} classname - the CSS classname to use for the Tip 25 | * @public 26 | */ 27 | 28 | function EditorTip (html, classname) { 29 | if (!(this instanceof EditorTip)) return new EditorTip(html); 30 | Tip.call(this, html); 31 | 32 | // this value defined in `editor-tip.styl` CSS 33 | this.pad = 25; 34 | 35 | this.classname = classname || 'editor-tip'; 36 | 37 | // not sure why the main Tip doesn't keep track of this… 38 | this.shown = false; 39 | 40 | this.unbindClickOutside = null; 41 | this.clickOutside = this.clickOutside.bind(this); 42 | } 43 | 44 | /** 45 | * Inherits from `Tip`. 46 | */ 47 | 48 | inherits(EditorTip, Tip); 49 | 50 | /** 51 | * Shows the tooltip on the given `el` DOM element. 52 | * Adds the "click outside" watcher. 53 | * 54 | * @param {Node|Range} el - DOM element to make the tip point to 55 | * @public 56 | */ 57 | 58 | EditorTip.prototype.show = function (el) { 59 | if (this.shown && el === this.target) return; 60 | 61 | debug('showing tooltip'); 62 | var r = Tip.prototype.show.apply(this, arguments); 63 | 64 | this.shown = true; 65 | return r; 66 | }; 67 | 68 | /** 69 | * Hides the tooltip. 70 | * Uninstalls the "click outside" handler. 71 | * 72 | * @public 73 | */ 74 | 75 | EditorTip.prototype.hide = function () { 76 | if (!this.shown) return; 77 | 78 | debug('hiding tooltip'); 79 | var r = Tip.prototype.hide.apply(this, arguments); 80 | 81 | if (this.unbindClickOutside) { 82 | debug('invoking unbindClickOutside()'); 83 | this.unbindClickOutside(); 84 | this.unbindClickOutside = null; 85 | } 86 | 87 | this.shown = false; 88 | return r; 89 | }; 90 | 91 | /** 92 | * Toggles the hide/shown state of the Tip. 93 | * 94 | * @param {DOMElement} element - the element to have the tip "target" upon show 95 | * @public 96 | */ 97 | 98 | EditorTip.prototype.toggle = function (element) { 99 | debug('toggling state of tooltip'); 100 | if (this.shown) { 101 | return this.hide(); 102 | } else { 103 | return this.show(element); 104 | } 105 | }; 106 | 107 | /** 108 | * Adds the "click outside" watch handler to hide the Tooltip upon 109 | * clicks outside the `.inner`
on the tooltip. 110 | * 111 | * @public 112 | */ 113 | 114 | EditorTip.prototype.addClickOutside = function () { 115 | debug('addClickOutside()'); 116 | if (this.unbindClickOutside) { 117 | debug('invoking unbindClickOutside()'); 118 | this.unbindClickOutside(); 119 | } 120 | this.unbindClickOutside = clickOutside(this.el, this.clickOutside); 121 | }; 122 | 123 | /** 124 | * "click outside" callback function. Hides the tooltip. 125 | * 126 | * @private 127 | */ 128 | 129 | EditorTip.prototype.clickOutside = function () { 130 | debug('click outside tooltip'); 131 | this.hide(); 132 | }; 133 | -------------------------------------------------------------------------------- /lib/editor-toolbar-tooltips/index.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Module dependencies. 4 | */ 5 | 6 | var dataset = require('dataset'); 7 | var event = require('component-event'); 8 | var query = require('component-query'); 9 | var classes = require('component-classes'); 10 | var throttle = require('per-frame'); 11 | var EditorToolbar = require('../editor-toolbar'); 12 | var debug = require('debug')('editor:editor-toolbar-tooltips'); 13 | 14 | /** 15 | * Module exports. 16 | */ 17 | 18 | module.exports = setup; 19 | 20 | /** 21 | * This is a Editor plugin that sets up an `EditorTip` instance 22 | * for the given `button` DOM element, which is assumed to be a 23 | *