├── .npmrc ├── src ├── wrappers │ ├── end.js │ └── start.js ├── sass │ ├── util │ │ └── _clearfix.scss │ ├── animations │ │ ├── _image-loading.scss │ │ └── _pop-upwards.scss │ ├── _settings.scss │ ├── components │ │ ├── _file-dragging.scss │ │ ├── _anchor-preview.scss │ │ ├── _placeholder.scss │ │ ├── _toolbar-form.scss │ │ └── _toolbar.scss │ ├── medium-editor.scss │ └── themes │ │ ├── default.scss │ │ ├── flat.scss │ │ ├── roman.scss │ │ ├── mani.scss │ │ ├── bootstrap.scss │ │ ├── tim.scss │ │ └── beagle.scss └── js │ ├── globals.js │ ├── defaults │ └── options.js │ ├── version.js │ └── extensions │ ├── deprecated │ └── image-dragging.js │ ├── keyboard-commands.js │ ├── file-dragging.js │ ├── form.js │ ├── placeholder.js │ ├── WALKTHROUGH-EXTENSION.md │ ├── fontsize.js │ └── fontname.js ├── demo ├── img │ ├── flat.jpg │ ├── mani.jpg │ ├── roman.jpg │ ├── boostrap.jpg │ └── medium-editor.jpg ├── textarea.html ├── table-extension.html ├── multi-editor.html ├── relative-toolbar.html ├── css │ └── demo.css ├── extension-example.html ├── button-example.html ├── clean-paste.html ├── nested-editable.html ├── multi-paragraph.html ├── custom-toolbar.html ├── pass-instance.html ├── auto-link.html ├── absolute-container.html ├── js │ └── extension-table.js └── index.html ├── index.js ├── .editorconfig ├── .gitignore ├── .jshintrc ├── .github ├── PULL_REQUEST_TEMPLATE.md └── ISSUE_TEMPLATE.md ├── .travis.yml ├── spec ├── vendor │ └── jasmine-jsreporter-script.js ├── delay.spec.js ├── serialize.spec.js ├── version.spec.js ├── header-tags.spec.js ├── exploits.spec.js ├── full-content.spec.js ├── setup.spec.js ├── elements.spec.js └── drag-and-drop.spec.js ├── bower.json ├── CODE_OF_CONDUCT.md ├── dist └── css │ ├── themes │ ├── flat.min.css │ ├── roman.min.css │ ├── mani.min.css │ ├── bootstrap.min.css │ ├── tim.min.css │ ├── default.min.css │ ├── beagle.min.css │ ├── flat.css │ ├── roman.css │ ├── mani.css │ ├── default.css │ ├── bootstrap.css │ ├── tim.css │ └── beagle.css │ └── medium-editor.min.css ├── LICENSE ├── karma.dev.conf.js ├── MAINTAINERS.md ├── CONTRIBUTING.md ├── package.json ├── .jscsrc └── karma.conf.js /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | -------------------------------------------------------------------------------- /src/wrappers/end.js: -------------------------------------------------------------------------------- 1 | return MediumEditor; 2 | }())); 3 | -------------------------------------------------------------------------------- /demo/img/flat.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flowerskitchen/medium-editor/HEAD/demo/img/flat.jpg -------------------------------------------------------------------------------- /demo/img/mani.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flowerskitchen/medium-editor/HEAD/demo/img/mani.jpg -------------------------------------------------------------------------------- /demo/img/roman.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flowerskitchen/medium-editor/HEAD/demo/img/roman.jpg -------------------------------------------------------------------------------- /demo/img/boostrap.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flowerskitchen/medium-editor/HEAD/demo/img/boostrap.jpg -------------------------------------------------------------------------------- /demo/img/medium-editor.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/flowerskitchen/medium-editor/HEAD/demo/img/medium-editor.jpg -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | var connect = require('connect'); 2 | var serveStatic = require('serve-static'); 3 | connect().use(serveStatic(__dirname)).listen(8088); 4 | -------------------------------------------------------------------------------- /src/sass/util/_clearfix.scss: -------------------------------------------------------------------------------- 1 | %clearfix { 2 | &:after { 3 | clear: both; 4 | content: ""; 5 | display: table; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/sass/animations/_image-loading.scss: -------------------------------------------------------------------------------- 1 | @keyframes medium-editor-image-loading { 2 | 0% { 3 | transform: scale(0) 4 | } 5 | 100% { 6 | transform: scale(1); 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/js/globals.js: -------------------------------------------------------------------------------- 1 | /*jshint unused: false */ 2 | function MediumEditor(elements, options) { 3 | 'use strict'; 4 | return this.init(elements, options); 5 | } 6 | 7 | MediumEditor.extensions = {}; 8 | /*jshint unused: true */ -------------------------------------------------------------------------------- /src/sass/_settings.scss: -------------------------------------------------------------------------------- 1 | // typography 2 | $font-fixed: Consolas, "Liberation Mono", Menlo, Courier, monospace !default; 3 | $font-sans-serif: "Helvetica Neue", Helvetica, Arial, sans-serif !default; 4 | 5 | // ui / positioning 6 | $z-toolbar: 2000 !default; 7 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: http://EditorConfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = false 8 | indent_style = space 9 | indent_size = 4 10 | 11 | [*.json] 12 | indent_size = 2 13 | 14 | [.*rc] 15 | indent_size = 2 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.swp 3 | .DS_Store 4 | *.swo 5 | node_modules/ 6 | .env 7 | .sass-cache/ 8 | npm-debug.log 9 | .grunt/ 10 | _SpecRunner.html 11 | reports/ 12 | coverage/ 13 | ._* 14 | local.log 15 | browserstack.err 16 | 17 | # IDE 18 | .idea/ 19 | package-lock.json 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/sass/components/_file-dragging.scss: -------------------------------------------------------------------------------- 1 | .medium-editor-dragover { 2 | background: #ddd; 3 | } 4 | 5 | .medium-editor-image-loading { 6 | animation: medium-editor-image-loading 1s infinite ease-in-out; 7 | background-color: #333; 8 | border-radius: 100%; 9 | display: inline-block; 10 | height: 40px; 11 | width: 40px; 12 | } 13 | -------------------------------------------------------------------------------- /src/sass/animations/_pop-upwards.scss: -------------------------------------------------------------------------------- 1 | @keyframes medium-editor-pop-upwards { 2 | 0% { 3 | opacity: 0; 4 | transform: matrix(.97, 0, 0, 1, 0, 12); 5 | } 6 | 7 | 20% { 8 | opacity: .7; 9 | transform: matrix(.99, 0, 0, 1, 0, 2); 10 | } 11 | 12 | 40% { 13 | opacity: 1; 14 | transform: matrix(1, 0, 0, 1, 0, -1); 15 | } 16 | 17 | 100% { 18 | transform: matrix(1, 0, 0, 1, 0, 0); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "boss": true, 3 | "browser": true, 4 | "curly": true, 5 | "eqeqeq": true, 6 | "eqnull": true, 7 | "immed": true, 8 | "latedef": "nofunc", 9 | "newcap": false, 10 | "noarg": true, 11 | "predef": [ "MediumEditor", 12 | "afterAll", "afterEach", "beforeAll", "beforeEach", "describe", "expect", "it", "jasmine", "spyOn", 13 | "setupTestHelpers" ], 14 | "sub": true, 15 | "undef": true, 16 | "unused": true, 17 | "validthis": true 18 | } 19 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | | Q | A 2 | | ---------------- | --- 3 | | Bug fix? | yes/no 4 | | New feature? | yes/no 5 | | BC breaks? | yes/no 6 | | Deprecations? | yes/no 7 | | New tests added? | yes/not needed 8 | | Fixed tickets | comma-separated list of tickets fixed by the PR, if any 9 | | License | MIT 10 | 11 | ### Description 12 | 13 | [Description of the bug or feature] 14 | 15 | -- 16 | 17 | #### Please, don't submit `/dist` files with your PR! 18 | -------------------------------------------------------------------------------- /src/wrappers/start.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { 2 | 'use strict'; 3 | var isElectron = typeof module === 'object' && typeof process !== 'undefined' && process && process.versions && process.versions.electron; 4 | if (!isElectron && typeof module === 'object') { 5 | module.exports = factory; 6 | } else if (typeof define === 'function' && define.amd) { 7 | define(function () { 8 | return factory; 9 | }); 10 | } else { 11 | root.MediumEditor = factory; 12 | } 13 | }(this, function () { 14 | 15 | 'use strict'; 16 | -------------------------------------------------------------------------------- /src/sass/components/_anchor-preview.scss: -------------------------------------------------------------------------------- 1 | .medium-editor-anchor-preview { 2 | font-family: $font-sans-serif; 3 | font-size: 16px; 4 | left: 0; 5 | line-height: 1.4; 6 | max-width: 280px; 7 | position: absolute; 8 | text-align: center; 9 | top: 0; 10 | word-break: break-all; 11 | word-wrap: break-word; 12 | visibility: hidden; 13 | z-index: $z-toolbar; 14 | 15 | a { 16 | color: #fff; 17 | display: inline-block; 18 | margin: 5px 5px 10px; 19 | } 20 | } 21 | 22 | .medium-editor-anchor-preview-active { 23 | visibility: visible; 24 | } 25 | -------------------------------------------------------------------------------- /src/js/defaults/options.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | // summary: The default options hash used by the Editor 3 | 4 | MediumEditor.prototype.defaults = { 5 | activeButtonClass: 'medium-editor-button-active', 6 | buttonLabels: false, 7 | delay: 0, 8 | disableReturn: false, 9 | disableDoubleReturn: false, 10 | disableExtraSpaces: false, 11 | disableEditing: false, 12 | autoLink: false, 13 | elementsContainer: false, 14 | contentWindow: window, 15 | ownerDocument: document, 16 | targetBlank: false, 17 | extensions: {}, 18 | spellcheck: true 19 | }; 20 | })(); 21 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | # faster builds on new travis setup not using sudo 2 | sudo: false 3 | 4 | # cache vendor dirs 5 | cache: 6 | directories: 7 | - node_modules 8 | 9 | language: node_js 10 | 11 | node_js: 12 | - "12" 13 | 14 | notifications: 15 | email: false 16 | webhooks: 17 | urls: 18 | - https://webhooks.gitter.im/e/0913a4ced1f3322b4c40 19 | on_success: change # options: [always|never|change] default: always 20 | on_failure: always # options: [always|never|change] default: always 21 | on_start: false # default: false 22 | 23 | before_script: 24 | - npm install -g grunt-cli 25 | 26 | script: 27 | - npm run test:ci -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Description 2 | 3 | [Description of the bug or feature] 4 | 5 | ### Steps to reproduce 6 | 7 | 1. [First step] 8 | 2. [Second step] 9 | 3. [and so on...] 10 | 11 | **Expected behavior:** [What you expected to happen] 12 | 13 | **Actual behavior:** [What actually happened] 14 | 15 | **Link to an example:** [If you're reporting a bug that's not reproducible on our [demo page](https://yabwe.github.io/medium-editor/demo.html), please try to reproduce it on [JSFiddle](https://jsfiddle.net/), [JS Bin](https://jsbin.com), [CodePen](http://codepen.io/) or a similar service and paste a link here] 16 | 17 | ### Versions 18 | 19 | - medium-editor: 20 | - browser: 21 | - OS: 22 | -------------------------------------------------------------------------------- /src/sass/components/_placeholder.scss: -------------------------------------------------------------------------------- 1 | .medium-editor-placeholder { 2 | position: relative; 3 | 4 | &:after { 5 | content: attr(data-placeholder) !important; 6 | font-style: italic; 7 | position: absolute; 8 | left: 0; 9 | top: 0; 10 | white-space: pre; 11 | padding: inherit; 12 | margin: inherit; 13 | } 14 | } 15 | 16 | .medium-editor-placeholder-relative { 17 | position: relative; 18 | 19 | &:after { 20 | content: attr(data-placeholder) !important; 21 | font-style: italic; 22 | position: relative; 23 | white-space: pre; 24 | padding: inherit; 25 | margin: inherit; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/sass/medium-editor.scss: -------------------------------------------------------------------------------- 1 | @import "settings"; 2 | @import "animations/image-loading"; 3 | @import "animations/pop-upwards"; 4 | @import "components/anchor-preview"; 5 | @import "components/file-dragging"; 6 | @import "components/placeholder"; 7 | @import "components/toolbar"; 8 | @import "components/toolbar-form"; 9 | @import "util/clearfix"; 10 | 11 | // contenteditable rules 12 | .medium-editor-element { 13 | word-wrap: break-word; 14 | min-height: 30px; 15 | 16 | img { 17 | max-width: 100%; 18 | } 19 | 20 | sub { 21 | vertical-align: sub; 22 | } 23 | 24 | sup { 25 | vertical-align: super; 26 | } 27 | } 28 | 29 | .medium-editor-hidden { 30 | display: none; 31 | } 32 | -------------------------------------------------------------------------------- /src/js/version.js: -------------------------------------------------------------------------------- 1 | MediumEditor.parseVersionString = function (release) { 2 | var split = release.split('-'), 3 | version = split[0].split('.'), 4 | preRelease = (split.length > 1) ? split[1] : ''; 5 | return { 6 | major: parseInt(version[0], 10), 7 | minor: parseInt(version[1], 10), 8 | revision: parseInt(version[2], 10), 9 | preRelease: preRelease, 10 | toString: function () { 11 | return [version[0], version[1], version[2]].join('.') + (preRelease ? '-' + preRelease : ''); 12 | } 13 | }; 14 | }; 15 | 16 | MediumEditor.version = MediumEditor.parseVersionString.call(this, ({ 17 | // grunt-bump looks for this: 18 | 'version': '5.23.3' 19 | }).version); 20 | -------------------------------------------------------------------------------- /spec/vendor/jasmine-jsreporter-script.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | jasmine.getEnv().addReporter(new jasmine.JSReporter2()); //< for jsreporter 3 | 4 | var oldFunc = window.jasmine.getJSReport; 5 | 6 | window.jasmine.getJSReport = function () { 7 | var results = oldFunc(); 8 | return removePassing(results); 9 | }; 10 | 11 | function removePassing(results) { 12 | if (typeof results === "undefined") { 13 | return false; 14 | } 15 | 16 | var suites = []; 17 | 18 | for (var i = 0; i < results.length; i++) { 19 | if (!results.suites[i].passed) { 20 | suites.push(results.suites[i]); 21 | } 22 | } 23 | 24 | results.suites = suites; 25 | 26 | return results; 27 | } 28 | })(); 29 | -------------------------------------------------------------------------------- /src/sass/components/_toolbar-form.scss: -------------------------------------------------------------------------------- 1 | .medium-editor-toolbar-form { 2 | display: none; 3 | 4 | input, 5 | a { 6 | font-family: $font-sans-serif; 7 | } 8 | 9 | .medium-editor-toolbar-form-row { 10 | line-height: 14px; 11 | margin-left: 5px; 12 | padding-bottom: 5px; 13 | } 14 | 15 | .medium-editor-toolbar-input, 16 | label { 17 | border: none; 18 | box-sizing: border-box; 19 | font-size: 14px; 20 | margin: 0; 21 | padding: 6px; 22 | width: 316px; 23 | display: inline-block; 24 | 25 | &:focus { 26 | appearance: none; 27 | border: none; 28 | box-shadow: none; 29 | outline: 0; 30 | } 31 | } 32 | 33 | a { 34 | display: inline-block; 35 | font-size: 24px; 36 | font-weight: bolder; 37 | margin: 0 10px; 38 | text-decoration: none; 39 | } 40 | } 41 | 42 | .medium-editor-toolbar-form-active { 43 | display: block; 44 | } 45 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "medium-editor", 3 | "homepage": "http://yabwe.github.io/medium-editor/", 4 | "authors": [ 5 | "Davi Ferreira ", 6 | "Nate Mielnik ", 7 | "Noah Chase ", 8 | "Jeremy Benoist " 9 | ], 10 | "description": "Medium.com WYSIWYG editor clone written in pure JavaScript.", 11 | "main": ["dist/js/medium-editor.js", 12 | "dist/css/medium-editor.css", 13 | "dist/css/themes/default.css"], 14 | "keywords": [ 15 | "contenteditable", 16 | "wysiwyg", 17 | "medium", 18 | "rich-text", 19 | "editor" 20 | ], 21 | "license": "MIT", 22 | "ignore": [ 23 | "**/.*", 24 | "node_modules", 25 | "bower_components", 26 | "spec", 27 | "coverage", 28 | "reports", 29 | "_SpecRunner.html", 30 | "Gruntfile.js", 31 | "demo", 32 | "package.json", 33 | "CHANGES.md", 34 | "MAINTAINERS.md", 35 | "CODE_OF_CONDUCT.md", 36 | "CONTRIBUTING.md", 37 | "UPGRADE-5.md" 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /spec/delay.spec.js: -------------------------------------------------------------------------------- 1 | describe('Delay TestCase', function () { 2 | 'use strict'; 3 | 4 | beforeEach(function () { 5 | setupTestHelpers.call(this); 6 | this.el = this.createElement('div', 'editor', 'lore ipsum'); 7 | }); 8 | 9 | afterEach(function () { 10 | this.cleanupTest(); 11 | }); 12 | 13 | it('should call function after delay', function () { 14 | var editor, spy; 15 | 16 | editor = this.newMediumEditor('.editor', { delay: 100 }); 17 | spy = jasmine.createSpy('spy'); 18 | editor.delay(spy); 19 | jasmine.clock().tick(50); 20 | expect(spy).not.toHaveBeenCalled(); 21 | jasmine.clock().tick(150); 22 | expect(spy).toHaveBeenCalled(); 23 | }); 24 | it('should not call function if editor not active', function () { 25 | var editor, spy; 26 | 27 | editor = this.newMediumEditor('.editor', { delay: 1 }); 28 | spy = jasmine.createSpy('spy'); 29 | 30 | editor.destroy(); 31 | editor.delay(spy); 32 | jasmine.clock().tick(100); 33 | expect(spy).not.toHaveBeenCalled(); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /demo/textarea.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | medium editor | demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | Fork me on GitHub 13 |
14 |

Medium Editor

15 | 16 |
17 | 18 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /spec/serialize.spec.js: -------------------------------------------------------------------------------- 1 | describe('Anchor Button TestCase', function () { 2 | 'use strict'; 3 | 4 | beforeEach(function () { 5 | setupTestHelpers.call(this); 6 | this.el = this.createElement('div', 'editor', '

lorem ipsum

'); 7 | this.el.id = 'medium-editor-test'; 8 | }); 9 | 10 | afterEach(function () { 11 | this.cleanupTest(); 12 | }); 13 | 14 | it('should return the editor content as a JSON object', function () { 15 | var editor = this.newMediumEditor('.editor'), 16 | json = editor.serialize(); 17 | expect(json).toEqual({ 18 | 'medium-editor-test': { 19 | value: '

lorem ipsum

' 20 | } 21 | }); 22 | }); 23 | 24 | it('should set a custom id when elements have no ids', function () { 25 | this.el.removeAttribute('id'); 26 | var editor = this.newMediumEditor('.editor'), 27 | json = editor.serialize(); 28 | expect(json).toEqual({ 29 | 'element-0': { 30 | value: '

lorem ipsum

' 31 | } 32 | }); 33 | }); 34 | 35 | }); 36 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | As contributors and maintainers of MediumEditor, we pledge to respect all people who contribute through reporting issues, posting feature requests, updating documentation, submitting pull requests or patches, and other activities. 4 | 5 | We are committed to making participation in this project a harassment-free experience for everyone, regardless of level of experience, gender, gender identity and expression, sexual orientation, disability, personal appearance, body size, race, age, or religion. 6 | 7 | Examples of unacceptable behavior by participants include the use of sexual language or imagery, derogatory comments or personal attacks, trolling, public or private harassment, insults, or other unprofessional conduct. 8 | 9 | Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct. Project maintainers who do not follow the Code of Conduct may be removed from the project team. 10 | 11 | Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by opening an issue or contacting one or more of the project maintainers. 12 | 13 | This Code of Conduct is adapted from the [Contributor Covenant](http://contributor-covenant.org), version 1.0.0, available at [http://contributor-covenant.org/version/1/0/0/](http://contributor-covenant.org/version/1/0/0/) 14 | -------------------------------------------------------------------------------- /dist/css/themes/flat.min.css: -------------------------------------------------------------------------------- 1 | .medium-toolbar-arrow-under:after{top:60px;border-color:#57ad68 transparent transparent}.medium-toolbar-arrow-over:before{top:-8px;border-color:transparent transparent #57ad68}.medium-editor-toolbar{background-color:#57ad68}.medium-editor-toolbar li{padding:0}.medium-editor-toolbar li button{min-width:60px;height:60px;border:none;border-right:1px solid #9ccea6;background-color:transparent;color:#fff;-webkit-transition:background-color .2s ease-in,color .2s ease-in;transition:background-color .2s ease-in,color .2s ease-in}.medium-editor-toolbar li button:hover{background-color:#346a3f;color:#fff}.medium-editor-toolbar li .medium-editor-button-active{background-color:#23482a;color:#fff}.medium-editor-toolbar li .medium-editor-button-last{border-right:none}.medium-editor-toolbar-form .medium-editor-toolbar-input{height:60px;background:#57ad68;color:#fff}.medium-editor-toolbar-form .medium-editor-toolbar-input::-webkit-input-placeholder{color:#fff;color:rgba(255,255,255,.8)}.medium-editor-toolbar-form .medium-editor-toolbar-input:-moz-placeholder{color:#fff;color:rgba(255,255,255,.8)}.medium-editor-toolbar-form .medium-editor-toolbar-input::-moz-placeholder{color:#fff;color:rgba(255,255,255,.8)}.medium-editor-toolbar-form .medium-editor-toolbar-input:-ms-input-placeholder{color:#fff;color:rgba(255,255,255,.8)}.medium-editor-toolbar-form a{color:#fff}.medium-editor-toolbar-anchor-preview{background:#57ad68;color:#fff}.medium-editor-placeholder:after{color:#9ccea6} -------------------------------------------------------------------------------- /dist/css/themes/roman.min.css: -------------------------------------------------------------------------------- 1 | .medium-toolbar-arrow-over:before,.medium-toolbar-arrow-under:after{display:none}.medium-editor-toolbar{background-color:#fff;background-color:rgba(255,255,255,.95);border-radius:5px;box-shadow:0 2px 6px rgba(0,0,0,.45)}.medium-editor-toolbar li button{min-width:50px;height:50px;border:none;border-right:1px solid #a8a8a8;background-color:transparent;color:#889aac;box-shadow:inset 0 0 3px #f8f8e6;background:-webkit-linear-gradient(top,#fff,rgba(0,0,0,.2));background:linear-gradient(to bottom,#fff,rgba(0,0,0,.2));text-shadow:1px 4px 6px #def,0 0 0 #000,1px 4px 6px #def;-webkit-transition:background-color .2s ease-in;transition:background-color .2s ease-in}.medium-editor-toolbar li button:hover{background-color:#fff;color:#fff;color:rgba(0,0,0,.8)}.medium-editor-toolbar li .medium-editor-button-first{border-top-left-radius:5px;border-bottom-left-radius:5px}.medium-editor-toolbar li .medium-editor-button-last{border-top-right-radius:5px;border-bottom-right-radius:5px}.medium-editor-toolbar li .medium-editor-button-active{background-color:#ccc;color:#000;color:rgba(0,0,0,.8);background:-webkit-linear-gradient(bottom,#fff,rgba(0,0,0,.1));background:linear-gradient(to top,#fff,rgba(0,0,0,.1))}.medium-editor-toolbar-form{background:#fff;color:#999;border-radius:5px}.medium-editor-toolbar-form .medium-editor-toolbar-input{margin:0;height:50px;background:#fff;color:#a8a8a8}.medium-editor-toolbar-form a{color:#889aac}.medium-editor-toolbar-anchor-preview{background:#fff;color:#889aac;border-radius:5px}.medium-editor-placeholder:after{color:#a8a8a8} -------------------------------------------------------------------------------- /dist/css/themes/mani.min.css: -------------------------------------------------------------------------------- 1 | .medium-toolbar-arrow-over:before,.medium-toolbar-arrow-under:after{display:none}.medium-editor-toolbar{border:1px solid #cdd6e0;background-color:#dee7f0;background-color:rgba(222,231,240,.95);background:-webkit-linear-gradient(bottom,#dee7f0,#fff);background:linear-gradient(to top,#dee7f0,#fff);border-radius:2px;box-shadow:0 2px 6px rgba(0,0,0,.45)}.medium-editor-toolbar li button{min-width:50px;height:50px;border:none;border-right:1px solid #cdd6e0;background-color:transparent;color:#40648a;-webkit-transition:background-color .2s ease-in,color .2s ease-in;transition:background-color .2s ease-in,color .2s ease-in}.medium-editor-toolbar li button:hover{background-color:#5c90c7;background-color:rgba(92,144,199,.45);color:#fff}.medium-editor-toolbar li .medium-editor-button-first{border-top-left-radius:2px;border-bottom-left-radius:2px}.medium-editor-toolbar li .medium-editor-button-last{border-top-right-radius:2px;border-bottom-right-radius:2px}.medium-editor-toolbar li .medium-editor-button-active{background-color:#5c90c7;background-color:rgba(92,144,199,.45);color:#000;background:-webkit-linear-gradient(top,#dee7f0,rgba(0,0,0,.1));background:linear-gradient(to bottom,#dee7f0,rgba(0,0,0,.1))}.medium-editor-toolbar-form{background:#dee7f0;color:#999;border-radius:2px}.medium-editor-toolbar-form .medium-editor-toolbar-input{height:50px;background:#dee7f0;color:#40648a;box-sizing:border-box}.medium-editor-toolbar-form a{color:#40648a}.medium-editor-toolbar-anchor-preview{background:#dee7f0;color:#40648a;border-radius:2px}.medium-editor-placeholder:after{color:#cdd6e0} -------------------------------------------------------------------------------- /demo/table-extension.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | medium editor | demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | Fork me on GitHub 13 |
14 |

Medium Editor Extension Form

15 |

Medium Editor allows you to add custom forms for your extensions. This example demonstrates a form for creating a table.

16 |
17 | 18 |
19 |
20 | 21 | 22 | 23 | 35 | 36 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Davi Ferreira, https://www.daviferreira.com/ 2 | 3 | This software consists of voluntary contributions made by many 4 | individuals. For exact contribution history, see the revision history 5 | available at https://github.com/yabwe/medium-editor 6 | 7 | The following license applies to all parts of this software except as 8 | documented below: 9 | 10 | ==== 11 | 12 | Permission is hereby granted, free of charge, to any person obtaining 13 | a copy of this software and associated documentation files (the 14 | "Software"), to deal in the Software without restriction, including 15 | without limitation the rights to use, copy, modify, merge, publish, 16 | distribute, sublicense, and/or sell copies of the Software, and to 17 | permit persons to whom the Software is furnished to do so, subject to 18 | the following conditions: 19 | 20 | The above copyright notice and this permission notice shall be 21 | included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 24 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 25 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 26 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 27 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 28 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 29 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 30 | 31 | ==== 32 | 33 | All files located in the node_modules directory are 34 | externally maintained libraries used by this software which have their 35 | own licenses; we recommend you read them, as their terms may differ from 36 | the terms above. 37 | -------------------------------------------------------------------------------- /dist/css/themes/bootstrap.min.css: -------------------------------------------------------------------------------- 1 | .medium-toolbar-arrow-under:after{border-color:#428bca transparent transparent;top:60px}.medium-toolbar-arrow-over:before{border-color:transparent transparent #428bca}.medium-editor-toolbar{background-color:#428bca;border:1px solid #357ebd;border-radius:4px}.medium-editor-toolbar li button{background-color:transparent;border:none;border-right:1px solid #357ebd;box-sizing:border-box;color:#fff;height:60px;min-width:60px;-webkit-transition:background-color .2s ease-in,color .2s ease-in;transition:background-color .2s ease-in,color .2s ease-in}.medium-editor-toolbar li .medium-editor-button-active,.medium-editor-toolbar li button:hover{background-color:#3276b1;color:#fff}.medium-editor-toolbar li .medium-editor-button-first{border-bottom-left-radius:4px;border-top-left-radius:4px}.medium-editor-toolbar li .medium-editor-button-last{border-bottom-right-radius:4px;border-right:none;border-top-right-radius:4px}.medium-editor-toolbar-form{background:#428bca;border-radius:4px;color:#fff}.medium-editor-toolbar-form .medium-editor-toolbar-input{background:#428bca;color:#fff;height:60px}.medium-editor-toolbar-form .medium-editor-toolbar-input::-webkit-input-placeholder{color:#fff;color:rgba(255,255,255,.8)}.medium-editor-toolbar-form .medium-editor-toolbar-input:-moz-placeholder{color:#fff;color:rgba(255,255,255,.8)}.medium-editor-toolbar-form .medium-editor-toolbar-input::-moz-placeholder{color:#fff;color:rgba(255,255,255,.8)}.medium-editor-toolbar-form .medium-editor-toolbar-input:-ms-input-placeholder{color:#fff;color:rgba(255,255,255,.8)}.medium-editor-toolbar-form a{color:#fff}.medium-editor-toolbar-anchor-preview{background:#428bca;border-radius:4px;color:#fff}.medium-editor-placeholder:after{color:#357ebd} -------------------------------------------------------------------------------- /dist/css/themes/tim.min.css: -------------------------------------------------------------------------------- 1 | .medium-toolbar-arrow-under:after{border-color:#2f1e07 transparent transparent;top:60px}.medium-toolbar-arrow-over:before{border-color:transparent transparent #2f1e07}.medium-editor-toolbar{background-color:#2f1e07;border:1px solid #5b3a0e;border-radius:6px}.medium-editor-toolbar li button{background-color:transparent;border:none;border-right:1px solid #5b3a0e;box-sizing:border-box;color:#ffedd5;height:60px;min-width:60px;-webkit-transition:background-color .2s ease-in,color .2s ease-in;transition:background-color .2s ease-in,color .2s ease-in}.medium-editor-toolbar li .medium-editor-button-active,.medium-editor-toolbar li button:hover{background-color:#030200;color:#ffedd5}.medium-editor-toolbar li .medium-editor-button-first{border-bottom-left-radius:6px;border-top-left-radius:6px}.medium-editor-toolbar li .medium-editor-button-last{border-bottom-right-radius:6px;border-right:none;border-top-right-radius:6px}.medium-editor-toolbar-form{background:#2f1e07;border-radius:6px;color:#ffedd5}.medium-editor-toolbar-form .medium-editor-toolbar-input{background:#2f1e07;color:#ffedd5;height:60px}.medium-editor-toolbar-form .medium-editor-toolbar-input::-webkit-input-placeholder{color:#ffedd5;color:rgba(255,237,213,.8)}.medium-editor-toolbar-form .medium-editor-toolbar-input:-moz-placeholder{color:#ffedd5;color:rgba(255,237,213,.8)}.medium-editor-toolbar-form .medium-editor-toolbar-input::-moz-placeholder{color:#ffedd5;color:rgba(255,237,213,.8)}.medium-editor-toolbar-form .medium-editor-toolbar-input:-ms-input-placeholder{color:#ffedd5;color:rgba(255,237,213,.8)}.medium-editor-toolbar-form a{color:#ffedd5}.medium-editor-toolbar-anchor-preview{background:#2f1e07;border-radius:6px;color:#ffedd5}.medium-editor-placeholder:after{color:#5b3a0e} -------------------------------------------------------------------------------- /dist/css/themes/default.min.css: -------------------------------------------------------------------------------- 1 | .medium-toolbar-arrow-under:after{border-color:#242424 transparent transparent;top:50px}.medium-toolbar-arrow-over:before{border-color:transparent transparent #242424;top:-8px}.medium-editor-toolbar{background-color:#242424;background:-webkit-linear-gradient(top,#242424,rgba(36,36,36,.75));background:linear-gradient(to bottom,#242424,rgba(36,36,36,.75));border:1px solid #000;border-radius:5px;box-shadow:0 0 3px #000}.medium-editor-toolbar li button{background-color:#242424;background:-webkit-linear-gradient(top,#242424,rgba(36,36,36,.89));background:linear-gradient(to bottom,#242424,rgba(36,36,36,.89));border:0;border-right:1px solid #000;border-left:1px solid #333;border-left:1px solid rgba(255,255,255,.1);box-shadow:0 2px 2px rgba(0,0,0,.3);color:#fff;height:50px;min-width:50px;-webkit-transition:background-color .2s ease-in;transition:background-color .2s ease-in}.medium-editor-toolbar li button:hover{background-color:#000;color:#ff0}.medium-editor-toolbar li .medium-editor-button-first{border-bottom-left-radius:5px;border-top-left-radius:5px}.medium-editor-toolbar li .medium-editor-button-last{border-bottom-right-radius:5px;border-top-right-radius:5px}.medium-editor-toolbar li .medium-editor-button-active{background-color:#000;background:-webkit-linear-gradient(top,#242424,rgba(0,0,0,.89));background:linear-gradient(to bottom,#242424,rgba(0,0,0,.89));color:#fff}.medium-editor-toolbar-form{background:#242424;border-radius:5px;color:#999}.medium-editor-toolbar-form .medium-editor-toolbar-input{background:#242424;box-sizing:border-box;color:#ccc;height:50px}.medium-editor-toolbar-form a{color:#fff}.medium-editor-toolbar-anchor-preview{background:#242424;border-radius:5px;color:#fff}.medium-editor-placeholder:after{color:#b3b3b1} -------------------------------------------------------------------------------- /spec/version.spec.js: -------------------------------------------------------------------------------- 1 | describe('Core MediumEditor', function () { 2 | 3 | it('exists', function () { 4 | expect(MediumEditor).toBeTruthy(); 5 | }); 6 | 7 | describe('MediumEditor.version', function () { 8 | 9 | it('exists', function () { 10 | expect(MediumEditor.version).toBeTruthy(); 11 | }); 12 | 13 | it('has major/minor/revision ints', function () { 14 | expect(MediumEditor.version.major).toBeDefined(); 15 | expect(MediumEditor.version.minor).toBeDefined(); 16 | expect(MediumEditor.version.revision).toBeDefined(); 17 | expect(MediumEditor.version.preRelease).toBeDefined(); 18 | }); 19 | 20 | it('exposes the major/minor/revison as a string', function () { 21 | var v = '' + MediumEditor.version; 22 | expect(typeof v).toEqual('string'); 23 | }); 24 | }); 25 | 26 | describe('MediumEditor.parseVersionString', function () { 27 | 28 | it('exists', function () { 29 | expect(MediumEditor.parseVersionString).toBeTruthy(); 30 | }); 31 | 32 | it('parses a normal version string', function () { 33 | var info = MediumEditor.parseVersionString('1.2.3'); 34 | 35 | expect(info.major).toBe(1); 36 | expect(info.minor).toBe(2); 37 | expect(info.revision).toBe(3); 38 | expect(info.preRelease).toBe(''); 39 | expect(info.toString()).toBe('1.2.3'); 40 | }); 41 | 42 | it('parses pre-release versions', function () { 43 | var info = MediumEditor.parseVersionString('5.0.0-alpha.1'); 44 | 45 | expect(info.major).toBe(5); 46 | expect(info.minor).toBe(0); 47 | expect(info.revision).toBe(0); 48 | expect(info.preRelease).toBe('alpha.1'); 49 | expect(info.toString()).toBe('5.0.0-alpha.1'); 50 | }); 51 | }); 52 | 53 | }); 54 | -------------------------------------------------------------------------------- /dist/css/themes/beagle.min.css: -------------------------------------------------------------------------------- 1 | .medium-toolbar-arrow-under:after{border-color:#000 transparent transparent;top:40px}.medium-toolbar-arrow-over:before{border-color:transparent transparent #000}.medium-editor-toolbar{background-color:#000;border:none;border-radius:50px}.medium-editor-toolbar li button{background-color:transparent;border:none;box-sizing:border-box;color:#ccc;height:40px;min-width:40px;padding:5px 12px;-webkit-transition:background-color .2s ease-in,color .2s ease-in;transition:background-color .2s ease-in,color .2s ease-in}.medium-editor-toolbar li .medium-editor-button-active,.medium-editor-toolbar li button:hover{background-color:#000;color:#a2d7c7}.medium-editor-toolbar li .medium-editor-button-first{border-bottom-left-radius:50px;border-top-left-radius:50px;padding-left:24px}.medium-editor-toolbar li .medium-editor-button-last{border-bottom-right-radius:50px;border-right:none;border-top-right-radius:50px;padding-right:24px}.medium-editor-toolbar-form{background:#000;border-radius:50px;color:#ccc;overflow:hidden}.medium-editor-toolbar-form .medium-editor-toolbar-input{background:#000;box-sizing:border-box;color:#ccc;height:40px;padding-left:16px;width:220px}.medium-editor-toolbar-form .medium-editor-toolbar-input::-webkit-input-placeholder{color:#f8f5f3;color:rgba(248,245,243,.8)}.medium-editor-toolbar-form .medium-editor-toolbar-input:-moz-placeholder{color:#f8f5f3;color:rgba(248,245,243,.8)}.medium-editor-toolbar-form .medium-editor-toolbar-input::-moz-placeholder{color:#f8f5f3;color:rgba(248,245,243,.8)}.medium-editor-toolbar-form .medium-editor-toolbar-input:-ms-input-placeholder{color:#f8f5f3;color:rgba(248,245,243,.8)}.medium-editor-toolbar-form a{color:#ccc;-webkit-transform:translateY(2px);transform:translateY(2px)}.medium-editor-toolbar-form .medium-editor-toolbar-close{margin-right:16px}.medium-editor-toolbar-anchor-preview{background:#000;border-radius:50px;padding:5px 12px}.medium-editor-anchor-preview a{color:#ccc;text-decoration:none}.medium-editor-toolbar-actions button,.medium-editor-toolbar-actions li{border-radius:50px} -------------------------------------------------------------------------------- /demo/multi-editor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | medium editor | demo 6 | 7 | 8 | 9 | 10 | 11 | Fork me on GitHub 12 |
13 |

Medium Multi Editor

14 |
15 |

First Editor

16 |

This text is a paragraph with some tags elements

17 |
18 |
19 |

Second Editor

20 |

This text is another paragraph in the same instance.

21 |
22 |
23 |

Third Editor in another instance

24 |

This text is another paragraph in another editor instance.
25 |

26 |
27 |
28 |

Disabled Toolbar Editor

29 |

This text is a paragraph in a
data-disable-toolbar="true"

30 |
31 |
32 |

Non editable Div

33 |

This text is another paragraph is a normal div.

34 |
35 | 36 |
37 | 38 | 39 | 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /dist/css/themes/flat.css: -------------------------------------------------------------------------------- 1 | .medium-toolbar-arrow-under:after { 2 | top: 60px; 3 | border-color: #57ad68 transparent transparent transparent; } 4 | 5 | .medium-toolbar-arrow-over:before { 6 | top: -8px; 7 | border-color: transparent transparent #57ad68 transparent; } 8 | 9 | .medium-editor-toolbar { 10 | background-color: #57ad68; } 11 | .medium-editor-toolbar li { 12 | padding: 0; } 13 | .medium-editor-toolbar li button { 14 | min-width: 60px; 15 | height: 60px; 16 | border: none; 17 | border-right: 1px solid #9ccea6; 18 | background-color: transparent; 19 | color: #fff; 20 | -webkit-transition: background-color .2s ease-in, color .2s ease-in; 21 | transition: background-color .2s ease-in, color .2s ease-in; } 22 | .medium-editor-toolbar li button:hover { 23 | background-color: #346a3f; 24 | color: #fff; } 25 | .medium-editor-toolbar li .medium-editor-button-active { 26 | background-color: #23482a; 27 | color: #fff; } 28 | .medium-editor-toolbar li .medium-editor-button-last { 29 | border-right: none; } 30 | 31 | .medium-editor-toolbar-form .medium-editor-toolbar-input { 32 | height: 60px; 33 | background: #57ad68; 34 | color: #fff; } 35 | .medium-editor-toolbar-form .medium-editor-toolbar-input::-webkit-input-placeholder { 36 | color: #fff; 37 | color: rgba(255, 255, 255, 0.8); } 38 | .medium-editor-toolbar-form .medium-editor-toolbar-input:-moz-placeholder { 39 | /* Firefox 18- */ 40 | color: #fff; 41 | color: rgba(255, 255, 255, 0.8); } 42 | .medium-editor-toolbar-form .medium-editor-toolbar-input::-moz-placeholder { 43 | /* Firefox 19+ */ 44 | color: #fff; 45 | color: rgba(255, 255, 255, 0.8); } 46 | .medium-editor-toolbar-form .medium-editor-toolbar-input:-ms-input-placeholder { 47 | color: #fff; 48 | color: rgba(255, 255, 255, 0.8); } 49 | 50 | .medium-editor-toolbar-form a { 51 | color: #fff; } 52 | 53 | .medium-editor-toolbar-anchor-preview { 54 | background: #57ad68; 55 | color: #fff; } 56 | 57 | .medium-editor-placeholder:after { 58 | color: #9ccea6; } 59 | -------------------------------------------------------------------------------- /dist/css/themes/roman.css: -------------------------------------------------------------------------------- 1 | .medium-toolbar-arrow-under:after, 2 | .medium-toolbar-arrow-over:before { 3 | display: none; } 4 | 5 | .medium-editor-toolbar { 6 | background-color: #fff; 7 | background-color: rgba(255, 255, 255, 0.95); 8 | border-radius: 5px; 9 | box-shadow: 0 2px 6px rgba(0, 0, 0, 0.45); } 10 | .medium-editor-toolbar li button { 11 | min-width: 50px; 12 | height: 50px; 13 | border: none; 14 | border-right: 1px solid #a8a8a8; 15 | background-color: transparent; 16 | color: #889aac; 17 | box-shadow: inset 0 0 3px #f8f8e6; 18 | background: -webkit-linear-gradient(top, #fff, rgba(0, 0, 0, 0.2)); 19 | background: linear-gradient(to bottom, #fff, rgba(0, 0, 0, 0.2)); 20 | text-shadow: 1px 4px 6px #def, 0 0 0 #000, 1px 4px 6px #def; 21 | -webkit-transition: background-color .2s ease-in; 22 | transition: background-color .2s ease-in; } 23 | .medium-editor-toolbar li button:hover { 24 | background-color: #fff; 25 | color: #fff; 26 | color: rgba(0, 0, 0, 0.8); } 27 | .medium-editor-toolbar li .medium-editor-button-first { 28 | border-top-left-radius: 5px; 29 | border-bottom-left-radius: 5px; } 30 | .medium-editor-toolbar li .medium-editor-button-last { 31 | border-top-right-radius: 5px; 32 | border-bottom-right-radius: 5px; } 33 | .medium-editor-toolbar li .medium-editor-button-active { 34 | background-color: #ccc; 35 | color: #000; 36 | color: rgba(0, 0, 0, 0.8); 37 | background: -webkit-linear-gradient(bottom, #fff, rgba(0, 0, 0, 0.1)); 38 | background: linear-gradient(to top, #fff, rgba(0, 0, 0, 0.1)); } 39 | 40 | .medium-editor-toolbar-form { 41 | background: #fff; 42 | color: #999; 43 | border-radius: 5px; } 44 | .medium-editor-toolbar-form .medium-editor-toolbar-input { 45 | margin: 0; 46 | height: 50px; 47 | background: #fff; 48 | color: #a8a8a8; } 49 | .medium-editor-toolbar-form a { 50 | color: #889aac; } 51 | 52 | .medium-editor-toolbar-anchor-preview { 53 | background: #fff; 54 | color: #889aac; 55 | border-radius: 5px; } 56 | 57 | .medium-editor-placeholder:after { 58 | color: #a8a8a8; } 59 | -------------------------------------------------------------------------------- /dist/css/themes/mani.css: -------------------------------------------------------------------------------- 1 | .medium-toolbar-arrow-under:after, 2 | .medium-toolbar-arrow-over:before { 3 | display: none; } 4 | 5 | .medium-editor-toolbar { 6 | border: 1px solid #cdd6e0; 7 | background-color: #dee7f0; 8 | background-color: rgba(222, 231, 240, 0.95); 9 | background: -webkit-linear-gradient(bottom, #dee7f0, white); 10 | background: linear-gradient(to top, #dee7f0, white); 11 | border-radius: 2px; 12 | box-shadow: 0 2px 6px rgba(0, 0, 0, 0.45); } 13 | .medium-editor-toolbar li button { 14 | min-width: 50px; 15 | height: 50px; 16 | border: none; 17 | border-right: 1px solid #cdd6e0; 18 | background-color: transparent; 19 | color: #40648a; 20 | -webkit-transition: background-color .2s ease-in, color .2s ease-in; 21 | transition: background-color .2s ease-in, color .2s ease-in; } 22 | .medium-editor-toolbar li button:hover { 23 | background-color: #5c90c7; 24 | background-color: rgba(92, 144, 199, 0.45); 25 | color: #fff; } 26 | .medium-editor-toolbar li .medium-editor-button-first { 27 | border-top-left-radius: 2px; 28 | border-bottom-left-radius: 2px; } 29 | .medium-editor-toolbar li .medium-editor-button-last { 30 | border-top-right-radius: 2px; 31 | border-bottom-right-radius: 2px; } 32 | .medium-editor-toolbar li .medium-editor-button-active { 33 | background-color: #5c90c7; 34 | background-color: rgba(92, 144, 199, 0.45); 35 | color: #000; 36 | background: -webkit-linear-gradient(top, #dee7f0, rgba(0, 0, 0, 0.1)); 37 | background: linear-gradient(to bottom, #dee7f0, rgba(0, 0, 0, 0.1)); } 38 | 39 | .medium-editor-toolbar-form { 40 | background: #dee7f0; 41 | color: #999; 42 | border-radius: 2px; } 43 | .medium-editor-toolbar-form .medium-editor-toolbar-input { 44 | height: 50px; 45 | background: #dee7f0; 46 | color: #40648a; 47 | box-sizing: border-box; } 48 | .medium-editor-toolbar-form a { 49 | color: #40648a; } 50 | 51 | .medium-editor-toolbar-anchor-preview { 52 | background: #dee7f0; 53 | color: #40648a; 54 | border-radius: 2px; } 55 | 56 | .medium-editor-placeholder:after { 57 | color: #cdd6e0; } 58 | -------------------------------------------------------------------------------- /demo/relative-toolbar.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | medium editor | demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | Fork me on GitHub 13 |
14 |

Medium Editor

15 |
16 |

Relative Toolbar Container

17 |

My father’s family name being Pirrip, and my Christian name Philip, my infant tongue could make of both names nothing longer or more explicit than Pip. So, I called myself Pip, and came to be called Pip.

18 | 19 |

I give Pirrip as my father’s family name, on the authority of his tombstone and my sister,—Mrs. Joe Gargery, who married the blacksmith. As I never saw my father or my mother, and never saw any likeness of either of them (for their days were long before the days of photographs), my first fancies regarding what they were like were unreasonably derived from their tombstones...

20 |
21 |
22 |
23 |
24 | 25 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/sass/components/_toolbar.scss: -------------------------------------------------------------------------------- 1 | %medium-toolbar-arrow { 2 | border-style: solid; 3 | content: ''; 4 | display: block; 5 | height: 0; 6 | left: 50%; 7 | margin-left: -8px; 8 | position: absolute; 9 | width: 0; 10 | } 11 | 12 | .medium-toolbar-arrow-under:after { 13 | @extend %medium-toolbar-arrow; 14 | border-width: 8px 8px 0 8px; 15 | } 16 | 17 | .medium-toolbar-arrow-over:before { 18 | @extend %medium-toolbar-arrow; 19 | border-width: 0 8px 8px 8px; 20 | top: -8px; 21 | } 22 | 23 | .medium-editor-toolbar { 24 | font-family: $font-sans-serif; 25 | font-size: 16px; 26 | left: 0; 27 | position: absolute; 28 | top: 0; 29 | visibility: hidden; 30 | z-index: $z-toolbar; 31 | 32 | ul { 33 | margin: 0; 34 | padding: 0; 35 | } 36 | 37 | li { 38 | float: left; 39 | list-style: none; 40 | margin: 0; 41 | padding: 0; 42 | 43 | button { 44 | box-sizing: border-box; 45 | cursor: pointer; 46 | display: block; 47 | font-size: 14px; 48 | line-height: 1.33; 49 | margin: 0; 50 | padding: 15px; 51 | text-decoration: none; 52 | 53 | &:focus { 54 | outline: none; 55 | } 56 | } 57 | 58 | .medium-editor-action-underline { 59 | text-decoration: underline; 60 | } 61 | 62 | .medium-editor-action-pre { 63 | font-family: $font-fixed; 64 | font-size: 12px; 65 | font-weight: 100; 66 | padding: 15px 0; 67 | } 68 | } 69 | } 70 | 71 | .medium-editor-toolbar-active { 72 | visibility: visible; 73 | } 74 | 75 | .medium-editor-sticky-toolbar { 76 | position: fixed; 77 | top: 1px; 78 | } 79 | 80 | .medium-editor-relative-toolbar { 81 | position: relative; 82 | } 83 | 84 | .medium-editor-toolbar-active.medium-editor-stalker-toolbar { 85 | animation: medium-editor-pop-upwards 160ms forwards linear; 86 | } 87 | 88 | .medium-editor-toolbar-actions { 89 | @extend %clearfix; 90 | } 91 | 92 | .medium-editor-action-bold { 93 | font-weight: bolder; 94 | } 95 | 96 | .medium-editor-action-italic { 97 | font-style: italic; 98 | } 99 | -------------------------------------------------------------------------------- /karma.dev.conf.js: -------------------------------------------------------------------------------- 1 | /* global module */ 2 | 3 | module.exports = function (config) { 4 | config.set({ 5 | 6 | basePath: '', 7 | frameworks: ['jasmine'], 8 | 9 | files: [ 10 | 'dist/css/*.css', 11 | 'node_modules/lodash/lodash.js', 12 | 'src/js/polyfills.js', 13 | 'src/js/globals.js', 14 | 'src/js/util.js', 15 | 'src/js/extension.js', 16 | 'src/js/selection.js', 17 | 'src/js/events.js', 18 | 'src/js/extensions/button.js', 19 | 'src/js/defaults/buttons.js', 20 | 'src/js/extensions/form.js', 21 | 'src/js/extensions/anchor.js', 22 | 'src/js/extensions/anchor-preview.js', 23 | 'src/js/extensions/auto-link.js', 24 | 'src/js/extensions/file-dragging.js', 25 | 'src/js/extensions/keyboard-commands.js', 26 | 'src/js/extensions/fontname.js', 27 | 'src/js/extensions/fontsize.js', 28 | 'src/js/extensions/paste.js', 29 | 'src/js/extensions/placeholder.js', 30 | 'src/js/extensions/toolbar.js', 31 | 'src/js/extensions/deprecated/image-dragging.js', 32 | 'src/js/core.js', 33 | 'src/js/defaults/options.js', 34 | 'src/js/version.js', 35 | 'spec/helpers/util.js', 36 | 'spec/*.spec.js' 37 | ], 38 | 39 | exclude: [ 40 | 'src/js/extensions/deprecated/*' 41 | ], 42 | 43 | preprocessors: { 44 | }, 45 | 46 | browsers: [ 47 | 'Chrome' 48 | ], 49 | plugins: [ 50 | 'karma-jasmine', 51 | 'karma-spec-reporter', 52 | 'karma-jasmine-html-reporter', 53 | 'karma-browserstack-launcher', 54 | 'karma-phantomjs-launcher', 55 | 'karma-chrome-launcher' 56 | ], 57 | reporters: ['progress', 'BrowserStack', 'dots', 'spec', 'kjhtml'], 58 | 59 | port: 9876, 60 | 61 | logLevel: config.LOG_INFO, 62 | colors: true, 63 | 64 | autoWatch: false, 65 | 66 | client: { 67 | clearContext: false 68 | }, 69 | 70 | singleRun: true, 71 | 72 | concurrency: Infinity 73 | }); 74 | }; -------------------------------------------------------------------------------- /dist/css/themes/default.css: -------------------------------------------------------------------------------- 1 | .medium-toolbar-arrow-under:after { 2 | border-color: #242424 transparent transparent transparent; 3 | top: 50px; } 4 | 5 | .medium-toolbar-arrow-over:before { 6 | border-color: transparent transparent #242424 transparent; 7 | top: -8px; } 8 | 9 | .medium-editor-toolbar { 10 | background-color: #242424; 11 | background: -webkit-linear-gradient(top, #242424, rgba(36, 36, 36, 0.75)); 12 | background: linear-gradient(to bottom, #242424, rgba(36, 36, 36, 0.75)); 13 | border: 1px solid #000; 14 | border-radius: 5px; 15 | box-shadow: 0 0 3px #000; } 16 | .medium-editor-toolbar li button { 17 | background-color: #242424; 18 | background: -webkit-linear-gradient(top, #242424, rgba(36, 36, 36, 0.89)); 19 | background: linear-gradient(to bottom, #242424, rgba(36, 36, 36, 0.89)); 20 | border: 0; 21 | border-right: 1px solid #000; 22 | border-left: 1px solid #333; 23 | border-left: 1px solid rgba(255, 255, 255, 0.1); 24 | box-shadow: 0 2px 2px rgba(0, 0, 0, 0.3); 25 | color: #fff; 26 | height: 50px; 27 | min-width: 50px; 28 | -webkit-transition: background-color .2s ease-in; 29 | transition: background-color .2s ease-in; } 30 | .medium-editor-toolbar li button:hover { 31 | background-color: #000; 32 | color: yellow; } 33 | .medium-editor-toolbar li .medium-editor-button-first { 34 | border-bottom-left-radius: 5px; 35 | border-top-left-radius: 5px; } 36 | .medium-editor-toolbar li .medium-editor-button-last { 37 | border-bottom-right-radius: 5px; 38 | border-top-right-radius: 5px; } 39 | .medium-editor-toolbar li .medium-editor-button-active { 40 | background-color: #000; 41 | background: -webkit-linear-gradient(top, #242424, rgba(0, 0, 0, 0.89)); 42 | background: linear-gradient(to bottom, #242424, rgba(0, 0, 0, 0.89)); 43 | color: #fff; } 44 | 45 | .medium-editor-toolbar-form { 46 | background: #242424; 47 | border-radius: 5px; 48 | color: #999; } 49 | .medium-editor-toolbar-form .medium-editor-toolbar-input { 50 | background: #242424; 51 | box-sizing: border-box; 52 | color: #ccc; 53 | height: 50px; } 54 | .medium-editor-toolbar-form a { 55 | color: #fff; } 56 | 57 | .medium-editor-toolbar-anchor-preview { 58 | background: #242424; 59 | border-radius: 5px; 60 | color: #fff; } 61 | 62 | .medium-editor-placeholder:after { 63 | color: #b3b3b1; } 64 | -------------------------------------------------------------------------------- /demo/css/demo.css: -------------------------------------------------------------------------------- 1 | *:focus { 2 | outline: none; 3 | } 4 | 5 | body { 6 | font-family: Helvetica, Arial, sans-serif; 7 | font-size: 22px; 8 | line-height: 30px; 9 | } 10 | 11 | .top-bar { 12 | position: fixed; 13 | top: 0; 14 | left: 0; 15 | width: auto; 16 | z-index: 10; 17 | padding: 10px; 18 | background-color: #000; 19 | background-color: rgba(0, 0, 0, .8); 20 | box-shadow: 0 0 4px #000; 21 | box-sizing: border-box; 22 | color: #ccc; 23 | font-size: 12px; 24 | font-weight: bold; 25 | text-align: center; 26 | text-transform: uppercase; 27 | } 28 | 29 | h1 { 30 | font-size: 60px; 31 | font-weight: bold; 32 | text-align: center; 33 | margin-bottom: 40px; 34 | padding-bottom: 40px; 35 | letter-spacing: -2px; 36 | border-bottom: 1px solid #dbdbdb; 37 | } 38 | 39 | h2 { 40 | font-size: 32px; 41 | line-height: 42px; 42 | } 43 | 44 | h3 { 45 | font-size: 26px; 46 | line-height: 32px; 47 | } 48 | 49 | h4 { 50 | font-size: 24px; 51 | line-height: 28px; 52 | } 53 | 54 | p { 55 | margin-bottom: 40px; 56 | } 57 | 58 | a { 59 | color:black; 60 | } 61 | 62 | a:hover { 63 | color:green; 64 | } 65 | 66 | pre { 67 | font-family: 'Menlo', monospace; 68 | font-size: 15px; 69 | background-color: #f0f0f0; 70 | padding: 15px; 71 | border: 1px solid #ccc; 72 | border-radius: 5px; 73 | color: #666; 74 | } 75 | 76 | 77 | blockquote { 78 | display: block; 79 | padding-left: 20px; 80 | border-left: 6px solid #df0d32; 81 | margin-left: -15px; 82 | padding-left: 15px; 83 | font-style: italic; 84 | color: #555; 85 | } 86 | 87 | #container { 88 | width: 960px; 89 | margin: 30px auto; 90 | } 91 | 92 | #all-demos { 93 | text-align: center; 94 | border-bottom: 1px solid #dbdbdb; 95 | padding-bottom: 40px; 96 | } 97 | 98 | .editable, 99 | .secondEditable 100 | { 101 | outline: none; 102 | margin: 0 0 20px 0; 103 | padding: 0 0 20px 0; 104 | border-bottom: 1px solid #dbdbdb; 105 | } 106 | 107 | #columns { 108 | width: 90%; 109 | margin: 30px auto; 110 | } 111 | 112 | .column-container { 113 | 114 | } 115 | 116 | .column { 117 | vertical-align: top; 118 | display: inline-block; 119 | width: 30%; 120 | margin: 10px 1%; 121 | } 122 | 123 | -------------------------------------------------------------------------------- /dist/css/themes/bootstrap.css: -------------------------------------------------------------------------------- 1 | .medium-toolbar-arrow-under:after { 2 | border-color: #428bca transparent transparent transparent; 3 | top: 60px; } 4 | 5 | .medium-toolbar-arrow-over:before { 6 | border-color: transparent transparent #428bca transparent; } 7 | 8 | .medium-editor-toolbar { 9 | background-color: #428bca; 10 | border: 1px solid #357ebd; 11 | border-radius: 4px; } 12 | .medium-editor-toolbar li button { 13 | background-color: transparent; 14 | border: none; 15 | border-right: 1px solid #357ebd; 16 | box-sizing: border-box; 17 | color: #fff; 18 | height: 60px; 19 | min-width: 60px; 20 | -webkit-transition: background-color .2s ease-in, color .2s ease-in; 21 | transition: background-color .2s ease-in, color .2s ease-in; } 22 | .medium-editor-toolbar li button:hover { 23 | background-color: #3276b1; 24 | color: #fff; } 25 | .medium-editor-toolbar li .medium-editor-button-first { 26 | border-bottom-left-radius: 4px; 27 | border-top-left-radius: 4px; } 28 | .medium-editor-toolbar li .medium-editor-button-last { 29 | border-bottom-right-radius: 4px; 30 | border-right: none; 31 | border-top-right-radius: 4px; } 32 | .medium-editor-toolbar li .medium-editor-button-active { 33 | background-color: #3276b1; 34 | color: #fff; } 35 | 36 | .medium-editor-toolbar-form { 37 | background: #428bca; 38 | border-radius: 4px; 39 | color: #fff; } 40 | .medium-editor-toolbar-form .medium-editor-toolbar-input { 41 | background: #428bca; 42 | color: #fff; 43 | height: 60px; } 44 | .medium-editor-toolbar-form .medium-editor-toolbar-input::-webkit-input-placeholder { 45 | color: #fff; 46 | color: rgba(255, 255, 255, 0.8); } 47 | .medium-editor-toolbar-form .medium-editor-toolbar-input:-moz-placeholder { 48 | /* Firefox 18- */ 49 | color: #fff; 50 | color: rgba(255, 255, 255, 0.8); } 51 | .medium-editor-toolbar-form .medium-editor-toolbar-input::-moz-placeholder { 52 | /* Firefox 19+ */ 53 | color: #fff; 54 | color: rgba(255, 255, 255, 0.8); } 55 | .medium-editor-toolbar-form .medium-editor-toolbar-input:-ms-input-placeholder { 56 | color: #fff; 57 | color: rgba(255, 255, 255, 0.8); } 58 | .medium-editor-toolbar-form a { 59 | color: #fff; } 60 | 61 | .medium-editor-toolbar-anchor-preview { 62 | background: #428bca; 63 | border-radius: 4px; 64 | color: #fff; } 65 | 66 | .medium-editor-placeholder:after { 67 | color: #357ebd; } 68 | -------------------------------------------------------------------------------- /dist/css/themes/tim.css: -------------------------------------------------------------------------------- 1 | .medium-toolbar-arrow-under:after { 2 | border-color: #2f1e07 transparent transparent transparent; 3 | top: 60px; } 4 | 5 | .medium-toolbar-arrow-over:before { 6 | border-color: transparent transparent #2f1e07 transparent; } 7 | 8 | .medium-editor-toolbar { 9 | background-color: #2f1e07; 10 | border: 1px solid #5b3a0e; 11 | border-radius: 6px; } 12 | .medium-editor-toolbar li button { 13 | background-color: transparent; 14 | border: none; 15 | border-right: 1px solid #5b3a0e; 16 | box-sizing: border-box; 17 | color: #ffedd5; 18 | height: 60px; 19 | min-width: 60px; 20 | -webkit-transition: background-color .2s ease-in, color .2s ease-in; 21 | transition: background-color .2s ease-in, color .2s ease-in; } 22 | .medium-editor-toolbar li button:hover { 23 | background-color: #030200; 24 | color: #ffedd5; } 25 | .medium-editor-toolbar li .medium-editor-button-first { 26 | border-bottom-left-radius: 6px; 27 | border-top-left-radius: 6px; } 28 | .medium-editor-toolbar li .medium-editor-button-last { 29 | border-bottom-right-radius: 6px; 30 | border-right: none; 31 | border-top-right-radius: 6px; } 32 | .medium-editor-toolbar li .medium-editor-button-active { 33 | background-color: #030200; 34 | color: #ffedd5; } 35 | 36 | .medium-editor-toolbar-form { 37 | background: #2f1e07; 38 | border-radius: 6px; 39 | color: #ffedd5; } 40 | .medium-editor-toolbar-form .medium-editor-toolbar-input { 41 | background: #2f1e07; 42 | color: #ffedd5; 43 | height: 60px; } 44 | .medium-editor-toolbar-form .medium-editor-toolbar-input::-webkit-input-placeholder { 45 | color: #ffedd5; 46 | color: rgba(255, 237, 213, 0.8); } 47 | .medium-editor-toolbar-form .medium-editor-toolbar-input:-moz-placeholder { 48 | /* Firefox 18- */ 49 | color: #ffedd5; 50 | color: rgba(255, 237, 213, 0.8); } 51 | .medium-editor-toolbar-form .medium-editor-toolbar-input::-moz-placeholder { 52 | /* Firefox 19+ */ 53 | color: #ffedd5; 54 | color: rgba(255, 237, 213, 0.8); } 55 | .medium-editor-toolbar-form .medium-editor-toolbar-input:-ms-input-placeholder { 56 | color: #ffedd5; 57 | color: rgba(255, 237, 213, 0.8); } 58 | .medium-editor-toolbar-form a { 59 | color: #ffedd5; } 60 | 61 | .medium-editor-toolbar-anchor-preview { 62 | background: #2f1e07; 63 | border-radius: 6px; 64 | color: #ffedd5; } 65 | 66 | .medium-editor-placeholder:after { 67 | color: #5b3a0e; } 68 | -------------------------------------------------------------------------------- /src/js/extensions/deprecated/image-dragging.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | var ImageDragging = MediumEditor.Extension.extend({ 5 | init: function () { 6 | MediumEditor.Extension.prototype.init.apply(this, arguments); 7 | 8 | this.subscribe('editableDrag', this.handleDrag.bind(this)); 9 | this.subscribe('editableDrop', this.handleDrop.bind(this)); 10 | }, 11 | 12 | handleDrag: function (event) { 13 | var className = 'medium-editor-dragover'; 14 | event.preventDefault(); 15 | event.dataTransfer.dropEffect = 'copy'; 16 | 17 | if (event.type === 'dragover') { 18 | event.target.classList.add(className); 19 | } else if (event.type === 'dragleave') { 20 | event.target.classList.remove(className); 21 | } 22 | }, 23 | 24 | handleDrop: function (event) { 25 | var className = 'medium-editor-dragover', 26 | files; 27 | event.preventDefault(); 28 | event.stopPropagation(); 29 | 30 | // IE9 does not support the File API, so prevent file from opening in a new window 31 | // but also don't try to actually get the file 32 | if (event.dataTransfer.files) { 33 | files = Array.prototype.slice.call(event.dataTransfer.files, 0); 34 | files.some(function (file) { 35 | if (file.type.match('image')) { 36 | var fileReader, id; 37 | fileReader = new FileReader(); 38 | fileReader.readAsDataURL(file); 39 | 40 | id = 'medium-img-' + (+new Date()); 41 | MediumEditor.util.insertHTMLCommand(this.document, ''); 42 | 43 | fileReader.onload = function () { 44 | var img = this.document.getElementById(id); 45 | if (img) { 46 | img.removeAttribute('id'); 47 | img.removeAttribute('class'); 48 | img.src = fileReader.result; 49 | } 50 | }.bind(this); 51 | } 52 | }.bind(this)); 53 | } 54 | event.target.classList.remove(className); 55 | } 56 | }); 57 | 58 | MediumEditor.extensions.imageDragging = ImageDragging; 59 | }()); 60 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | ## STEPS TO RELEASE: 2 | 3 | 1. Find the last release commit in log history. Look through all the commits or PR history and see all the stuff that has happened since the last release. 4 | 2. Add a row describing each high-level change into `CHANGES.md`. Looking at `CHANGES.md` would be a good stepping off point. 5 | 3. Depending upon the changes, decide if it is a major/minor/patch release. _Read more about [semantic versioning](http://semver.org/)_. 6 | 4. Depending upon the type of release, run `grunt major`, `grunt minor`, `grunt patch` to update the version number and generate all the dist files. 7 | 5. Commit all your changes (**including `CHANGES.md`**) into your commit. Add the new release number into your commit message. And push it up to the remote master branch. 8 | 6. Go [here](https://github.com/yabwe/medium-editor/releases) and ‘Draft a new release’. Title the release as the new release number (ex: `5.11.0`). Copy/paste the entries you made in `CHANGES.md` into the release summary. **Make sure the release is against the master branch.** 9 | 7. Once the release is created, go back to your git and run `npm publish`. 10 | 11 | 12 | ## RUNNING TESTS FOR FORK BRANCHES IN SAUCELABS: 13 | 14 | For pull requests submitted from a forked version of the repo, the test suite won't run in Saucelabs so we haven't been able to know if tests fail in various browsers until after the PR is merged into master. This is deliberate by Saucelabs as a security measure to prevent external forks from doing malicious things to the repo. 15 | 16 | There is a workaround however, so when a PR is submitted from an external fork, follow these steps to verify the tests don't fail in Saucelabs before merging the PR into master. 17 | 18 | For this example, let's assume there's a new pull request (#123) from a branch of an external user's fork (external-user/new-branch) 19 | 20 | 1. Create a new local branch for the pull request 21 | * ```git checkout -b integration-123``` 22 | 2. Add a remote that points to the external fork 23 | * ```git remote add external-user git@github.com:external-user/medium-editor.git``` 24 | 3. Fetch the remote repo 25 | * ```git fetch external-user``` 26 | 4. Merge the external branch into your local branch 27 | * ```git merge external-user/new-branch``` 28 | 5. Push your local branch up to the main repo 29 | * ```git push``` 30 | 31 | That's it. Pushing the branch up should kick off a travis build, which will cause the tests to run in Saucelabs. Github is smart enough to link the existing pull request to that build and reflect the status of the build (including the results from Saucelabs) on the PR summary page! 32 | 33 | 34 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ## To contribute and end up in this [list](https://github.com/yabwe/medium-editor/graphs/contributors): 4 | 5 | [Kill some bugs :)](https://github.com/yabwe/medium-editor/issues?q=is%3Aopen+is%3Aissue+label%3Abug) 6 | 7 | 1. Fork it 8 | 2. Create your feature branch (`git checkout -b my-new-feature`) 9 | 3. Test your changes to the best of your ability. 10 | 4. Update the documentation to reflect your changes if they add or changes current functionality. 11 | 5. Commit your changes (`git commit -am 'Added some feature'`) **without files from the _dist_ directory**. 12 | 6. Push to the branch (`git push origin my-new-feature`) 13 | 7. Create new Pull Request 14 | 15 | ## Code Consitency 16 | 17 | To help create consistent looking code throughout the project, we use a few tools to help us. They have plugins for most popular editors/IDEs to make coding for our project, but you should use them in your project as well! 18 | 19 | #### JSHint 20 | 21 | We use [JSHint](http://jshint.com/) on each build to find easy-to-catch errors and potential problems in our js. You can find our JSHint settings in the `.jshintrc` file in the root of the project. 22 | 23 | #### jscs 24 | 25 | We use [jscs](http://jscs.info/) on each build to enforce some code style rules we have for our project. You can find our jscs settings in the `.jscsrc` file in the root of the project. 26 | 27 | #### EditorConfig 28 | 29 | We use [EditorConfig](http://EditorConfig.org) to maintain consistent coding styles between various editors and IDEs. You can find our settings in the `.editorconfig` file in the root of the project. 30 | 31 | ## Easy First Bugs 32 | 33 | Looking for something simple for a first contribution? Try fixing an [easy first bug](https://github.com/yabwe/medium-editor/issues?q=is%3Aopen+is%3Aissue+label%3A%22easy+first+bug%22)! 34 | 35 | ## Development 36 | 37 | MediumEditor development tasks are managed by Grunt. To install all the necessary packages, just invoke: 38 | 39 | ```bash 40 | npm install 41 | ``` 42 | 43 | To run all the test and build the dist files for testing on demo pages, just invoke: 44 | ```bash 45 | grunt 46 | ``` 47 | 48 | These are the other available grunt tasks: 49 | 50 | * __js__: runs jslint and jasmine tests and creates minified and concatenated versions of the script; 51 | * __css__: runs autoprefixer and csslint 52 | * __test__: runs jasmine tests, jslint and csslint 53 | * __watch__: watch for modifications on script/scss files 54 | * __spec__: runs a task against a specified file 55 | 56 | The source files are located inside the __src__ directory. Be sure to make changes to these files and not files in the dist directory. 57 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "medium-editor", 3 | "version": "5.23.3", 4 | "author": "Davi Ferreira ", 5 | "contributors": [ 6 | { 7 | "name": "Nate Mielnik", 8 | "email": "nathan@outlook.com" 9 | }, 10 | { 11 | "name": "Noah Chase", 12 | "email": "nchase@gmail.com" 13 | }, 14 | { 15 | "name": "Jeremy Benoist", 16 | "email": "jeremy.benoist@gmail.com" 17 | } 18 | ], 19 | "description": "Medium.com WYSIWYG editor clone.", 20 | "main": "dist/js/medium-editor.js", 21 | "repository": { 22 | "type": "git", 23 | "url": "https://github.com/yabwe/medium-editor" 24 | }, 25 | "bugs": { 26 | "url": "https://github.com/yabwe/medium-editor/issues", 27 | "email": "hi@daviferreira.com" 28 | }, 29 | "homepage": "http://yabwe.github.io/medium-editor/", 30 | "keywords": [ 31 | "contenteditable", 32 | "editor", 33 | "medium", 34 | "wysiwyg", 35 | "rich-text" 36 | ], 37 | "publishConfig": { 38 | "registry": "http://registry.npmjs.org/" 39 | }, 40 | "license": "MIT", 41 | "devDependencies": { 42 | "brfs": "2.0.2", 43 | "connect": "3.7.0", 44 | "grunt": "1.2.1", 45 | "grunt-autoprefixer": "3.0.4", 46 | "grunt-bump": "0.8.0", 47 | "grunt-cli": "1.3.2", 48 | "grunt-contrib-concat": "1.0.1", 49 | "grunt-contrib-connect": "2.1.0", 50 | "grunt-contrib-csslint": "2.0.0", 51 | "grunt-contrib-cssmin": "3.0.0", 52 | "grunt-contrib-jasmine": "1.0.3", 53 | "grunt-contrib-jshint": "2.1.0", 54 | "grunt-contrib-uglify": "4.0.1", 55 | "grunt-contrib-watch": "1.1.0", 56 | "grunt-coveralls": "2.0.0", 57 | "grunt-jscs": "3.0.1", 58 | "grunt-karma": "4.0.0", 59 | "grunt-plato": "1.4.0", 60 | "grunt-sass": "3.1.0", 61 | "grunt-template-jasmine-istanbul": "0.4.0", 62 | "jasmine": "3.6.1", 63 | "jasmine-console-reporter": "3.1.0", 64 | "jasmine-core": "3.6.0", 65 | "jshint-stylish": "2.2.1", 66 | "karma": "5.1.0", 67 | "karma-browserstack-launcher": "1.6.0", 68 | "karma-chrome-launcher": "3.1.0", 69 | "karma-coverage": "2.0.3", 70 | "karma-coveralls": "2.1.0", 71 | "karma-firefox-launcher": "1.3.0", 72 | "karma-jasmine": "3.3.1", 73 | "karma-jasmine-html-reporter": "1.5.4", 74 | "karma-phantomjs-launcher": "1.0.4", 75 | "karma-spec-reporter": "0.0.32", 76 | "load-grunt-tasks": "5.1.0", 77 | "lodash": "4.17.19", 78 | "open-cli": "6.0.1", 79 | "phantomjs-prebuilt": "2.1.16", 80 | "serve-static": "1.14.1", 81 | "time-grunt": "2.0.0" 82 | }, 83 | "scripts": { 84 | "test": "node node_modules/grunt-cli/bin/grunt test --verbose", 85 | "test:ci": "node node_modules/grunt-cli/bin/grunt travis --verbose", 86 | "start": "open-cli ./demo/index.html", 87 | "build": "node node_modules/grunt-cli/bin/grunt" 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/sass/themes/default.scss: -------------------------------------------------------------------------------- 1 | // theme settings 2 | $medium-editor-bgcolor: #242424; 3 | $medium-editor-button-size: 50px; 4 | $medium-editor-border-radius: 5px; 5 | 6 | // theme rules 7 | .medium-toolbar-arrow-under:after { 8 | border-color: $medium-editor-bgcolor transparent transparent transparent; 9 | top: $medium-editor-button-size; 10 | } 11 | 12 | .medium-toolbar-arrow-over:before { 13 | border-color: transparent transparent $medium-editor-bgcolor transparent; 14 | top: -8px; 15 | } 16 | 17 | .medium-editor-toolbar { 18 | background-color: $medium-editor-bgcolor; 19 | background: linear-gradient(to bottom, $medium-editor-bgcolor, rgba($medium-editor-bgcolor, 0.75)); 20 | border: 1px solid #000; 21 | border-radius: $medium-editor-border-radius; 22 | box-shadow: 0 0 3px #000; 23 | 24 | li { 25 | button { 26 | background-color: $medium-editor-bgcolor; 27 | background: linear-gradient(to bottom, $medium-editor-bgcolor, rgba($medium-editor-bgcolor, 0.89)); 28 | border: 0; 29 | border-right: 1px solid #000; 30 | border-left: 1px solid #333; 31 | border-left: 1px solid rgba(#fff, .1); 32 | box-shadow: 0 2px 2px rgba(0,0,0,0.3); 33 | color: #fff; 34 | height: $medium-editor-button-size; 35 | min-width: $medium-editor-button-size; 36 | transition: background-color .2s ease-in; 37 | 38 | &:hover { 39 | background-color: #000; 40 | color: yellow; 41 | } 42 | } 43 | 44 | .medium-editor-button-first { 45 | border-bottom-left-radius: $medium-editor-border-radius; 46 | border-top-left-radius: $medium-editor-border-radius; 47 | } 48 | 49 | .medium-editor-button-last { 50 | border-bottom-right-radius: $medium-editor-border-radius; 51 | border-top-right-radius: $medium-editor-border-radius; 52 | } 53 | 54 | .medium-editor-button-active { 55 | background-color: #000; 56 | background: linear-gradient(to bottom, $medium-editor-bgcolor, rgba(#000, 0.89)); 57 | color: #fff; 58 | } 59 | } 60 | } 61 | 62 | .medium-editor-toolbar-form { 63 | background: $medium-editor-bgcolor; 64 | border-radius: $medium-editor-border-radius; 65 | color: #999; 66 | 67 | .medium-editor-toolbar-input { 68 | background: $medium-editor-bgcolor; 69 | box-sizing: border-box; 70 | color: #ccc; 71 | height: $medium-editor-button-size; 72 | } 73 | 74 | a { 75 | color: #fff; 76 | } 77 | } 78 | 79 | .medium-editor-toolbar-anchor-preview { 80 | background: $medium-editor-bgcolor; 81 | border-radius: $medium-editor-border-radius; 82 | color: #fff; 83 | } 84 | 85 | .medium-editor-placeholder:after { 86 | color: #b3b3b1; 87 | } 88 | -------------------------------------------------------------------------------- /dist/css/themes/beagle.css: -------------------------------------------------------------------------------- 1 | .medium-toolbar-arrow-under:after { 2 | border-color: #000 transparent transparent transparent; 3 | top: 40px; } 4 | 5 | .medium-toolbar-arrow-over:before { 6 | border-color: transparent transparent #000 transparent; } 7 | 8 | .medium-editor-toolbar { 9 | background-color: #000; 10 | border: none; 11 | border-radius: 50px; } 12 | .medium-editor-toolbar li button { 13 | background-color: transparent; 14 | border: none; 15 | box-sizing: border-box; 16 | color: #ccc; 17 | height: 40px; 18 | min-width: 40px; 19 | padding: 5px 12px; 20 | -webkit-transition: background-color .2s ease-in, color .2s ease-in; 21 | transition: background-color .2s ease-in, color .2s ease-in; } 22 | .medium-editor-toolbar li button:hover { 23 | background-color: #000; 24 | color: #a2d7c7; } 25 | .medium-editor-toolbar li .medium-editor-button-first { 26 | border-bottom-left-radius: 50px; 27 | border-top-left-radius: 50px; 28 | padding-left: 24px; } 29 | .medium-editor-toolbar li .medium-editor-button-last { 30 | border-bottom-right-radius: 50px; 31 | border-right: none; 32 | border-top-right-radius: 50px; 33 | padding-right: 24px; } 34 | .medium-editor-toolbar li .medium-editor-button-active { 35 | background-color: #000; 36 | color: #a2d7c7; } 37 | 38 | .medium-editor-toolbar-form { 39 | background: #000; 40 | border-radius: 50px; 41 | color: #ccc; 42 | overflow: hidden; } 43 | .medium-editor-toolbar-form .medium-editor-toolbar-input { 44 | background: #000; 45 | box-sizing: border-box; 46 | color: #ccc; 47 | height: 40px; 48 | padding-left: 16px; 49 | width: 220px; } 50 | .medium-editor-toolbar-form .medium-editor-toolbar-input::-webkit-input-placeholder { 51 | color: #f8f5f3; 52 | color: rgba(248, 245, 243, 0.8); } 53 | .medium-editor-toolbar-form .medium-editor-toolbar-input:-moz-placeholder { 54 | /* Firefox 18- */ 55 | color: #f8f5f3; 56 | color: rgba(248, 245, 243, 0.8); } 57 | .medium-editor-toolbar-form .medium-editor-toolbar-input::-moz-placeholder { 58 | /* Firefox 19+ */ 59 | color: #f8f5f3; 60 | color: rgba(248, 245, 243, 0.8); } 61 | .medium-editor-toolbar-form .medium-editor-toolbar-input:-ms-input-placeholder { 62 | color: #f8f5f3; 63 | color: rgba(248, 245, 243, 0.8); } 64 | .medium-editor-toolbar-form a { 65 | color: #ccc; 66 | -webkit-transform: translateY(2px); 67 | transform: translateY(2px); } 68 | .medium-editor-toolbar-form .medium-editor-toolbar-close { 69 | margin-right: 16px; } 70 | 71 | .medium-editor-toolbar-anchor-preview { 72 | background: #000; 73 | border-radius: 50px; 74 | padding: 5px 12px; } 75 | 76 | .medium-editor-anchor-preview a { 77 | color: #ccc; 78 | text-decoration: none; } 79 | 80 | .medium-editor-toolbar-actions li, .medium-editor-toolbar-actions button { 81 | border-radius: 50px; } 82 | -------------------------------------------------------------------------------- /src/sass/themes/flat.scss: -------------------------------------------------------------------------------- 1 | // theme settings 2 | $medium-editor-bgcolor: #57ad68; 3 | $medium-editor-border-color: #fff; 4 | $medium-editor-button-size: 60px; 5 | $medium-editor-button-active-text-color: #fff; 6 | $medium-editor-link-color: #fff; 7 | $medium-editor-placeholder-color: #fff; 8 | 9 | // theme rules 10 | .medium-toolbar-arrow-under:after { 11 | top: $medium-editor-button-size; 12 | border-color: $medium-editor-bgcolor transparent transparent transparent; 13 | } 14 | 15 | .medium-toolbar-arrow-over:before { 16 | top: -8px; 17 | border-color: transparent transparent $medium-editor-bgcolor transparent; 18 | } 19 | 20 | .medium-editor-toolbar { 21 | background-color: $medium-editor-bgcolor; 22 | 23 | li { 24 | padding: 0; 25 | 26 | button { 27 | min-width: $medium-editor-button-size; 28 | height: $medium-editor-button-size; 29 | border: none; 30 | border-right: 1px solid lighten($medium-editor-bgcolor, 20); 31 | background-color: transparent; 32 | color: $medium-editor-link-color; 33 | transition: background-color .2s ease-in, color .2s ease-in; 34 | &:hover { 35 | background-color: darken($medium-editor-bgcolor, 20); 36 | color: $medium-editor-button-active-text-color; 37 | } 38 | } 39 | 40 | .medium-editor-button-active { 41 | background-color: darken($medium-editor-bgcolor, 30); 42 | color: $medium-editor-button-active-text-color; 43 | } 44 | 45 | .medium-editor-button-last { 46 | border-right: none; 47 | } 48 | } 49 | } 50 | 51 | .medium-editor-toolbar-form { 52 | .medium-editor-toolbar-input { 53 | height: $medium-editor-button-size; 54 | background: $medium-editor-bgcolor; 55 | color: $medium-editor-link-color; 56 | 57 | &::-webkit-input-placeholder { 58 | color: $medium-editor-placeholder-color; 59 | color: rgba($medium-editor-placeholder-color, .8); 60 | } 61 | 62 | &:-moz-placeholder { /* Firefox 18- */ 63 | color: $medium-editor-placeholder-color; 64 | color: rgba($medium-editor-placeholder-color, .8); 65 | } 66 | 67 | &::-moz-placeholder { /* Firefox 19+ */ 68 | color: $medium-editor-placeholder-color; 69 | color: rgba($medium-editor-placeholder-color, .8); 70 | } 71 | 72 | &:-ms-input-placeholder { 73 | color: $medium-editor-placeholder-color; 74 | color: rgba($medium-editor-placeholder-color, .8); 75 | } 76 | } 77 | 78 | a { 79 | color: $medium-editor-link-color; 80 | } 81 | } 82 | 83 | .medium-editor-toolbar-anchor-preview { 84 | background: $medium-editor-bgcolor; 85 | color: $medium-editor-link-color; 86 | } 87 | 88 | .medium-editor-placeholder:after { 89 | color: lighten($medium-editor-bgcolor, 20); 90 | } 91 | -------------------------------------------------------------------------------- /demo/extension-example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MediumEditor - Extension Example 6 | 7 | 8 | 9 | 10 | 11 |
12 |

Medium Editor

13 |
14 |

Font Awesome

15 |

My father’s family name being Pirrip, and my Christian name Philip, my infant tongue could make of both names nothing longer or more explicit than Pip. So, I called myself Pip, and came to be called Pip.

16 |

I give Pirrip as my father’s family name, on the authority of his tombstone and my sister,—Mrs. Joe Gargery, who married the blacksmith. As I never saw my father or my mother, and never saw any likeness of either of them (for their days were long before the days of photographs), my first fancies regarding what they were like were unreasonably derived from their tombstones...

17 |
18 |
19 |

Source

20 | 21 | 56 | 57 | 58 | -------------------------------------------------------------------------------- /src/sass/themes/roman.scss: -------------------------------------------------------------------------------- 1 | // inspired by http://dribbble.com/shots/848100-Toolbar-Psd 2 | 3 | // theme settings 4 | $medium-editor-bgcolor: #fff; 5 | $medium-editor-border-color: #a8a8a8; 6 | $medium-editor-button-size: 50px; 7 | $medium-editor-button-hover-text-color: #fff; 8 | $medium-editor-button-active-text-color: #000; 9 | $medium-editor-link-color: #889aac; 10 | $medium-editor-border-radius: 5px; 11 | 12 | // theme rules 13 | .medium-toolbar-arrow-under:after, 14 | .medium-toolbar-arrow-over:before { 15 | display: none; 16 | } 17 | 18 | .medium-editor-toolbar { 19 | background-color: $medium-editor-bgcolor; 20 | background-color: rgba($medium-editor-bgcolor, .95); 21 | border-radius: $medium-editor-border-radius; 22 | box-shadow: 0 2px 6px rgba(#000, .45); 23 | 24 | li { 25 | button { 26 | min-width: $medium-editor-button-size; 27 | height: $medium-editor-button-size; 28 | border: none; 29 | border-right: 1px solid $medium-editor-border-color; 30 | background-color: transparent; 31 | color: $medium-editor-link-color; 32 | box-shadow: inset 0 0 3px #f8f8e6; 33 | background: linear-gradient(to bottom, $medium-editor-bgcolor, rgba(#000, .2)); 34 | text-shadow: 1px 4px 6px #def, 0 0 0 #000, 1px 4px 6px #def; 35 | transition: background-color .2s ease-in; 36 | &:hover { 37 | background-color: #fff; 38 | color: $medium-editor-button-hover-text-color; 39 | color: rgba(#000, .8); 40 | } 41 | } 42 | 43 | .medium-editor-button-first { 44 | border-top-left-radius: $medium-editor-border-radius; 45 | border-bottom-left-radius: $medium-editor-border-radius; 46 | } 47 | 48 | .medium-editor-button-last { 49 | border-top-right-radius: $medium-editor-border-radius; 50 | border-bottom-right-radius: $medium-editor-border-radius; 51 | } 52 | 53 | .medium-editor-button-active { 54 | background-color: #ccc; 55 | color: $medium-editor-button-active-text-color; 56 | color: rgba(#000, .8); 57 | background: linear-gradient(to top, $medium-editor-bgcolor, rgba(#000, .1)); 58 | } 59 | } 60 | } 61 | 62 | .medium-editor-toolbar-form { 63 | background: $medium-editor-bgcolor; 64 | color: #999; 65 | border-radius: $medium-editor-border-radius; 66 | 67 | .medium-editor-toolbar-input { 68 | margin: 0; 69 | height: $medium-editor-button-size; 70 | background: $medium-editor-bgcolor; 71 | color: $medium-editor-border-color; 72 | } 73 | 74 | a { 75 | color: $medium-editor-link-color; 76 | } 77 | } 78 | 79 | .medium-editor-toolbar-anchor-preview { 80 | background: $medium-editor-bgcolor; 81 | color: $medium-editor-link-color; 82 | border-radius: $medium-editor-border-radius; 83 | } 84 | 85 | .medium-editor-placeholder:after { 86 | color: $medium-editor-border-color; 87 | } 88 | -------------------------------------------------------------------------------- /src/sass/themes/mani.scss: -------------------------------------------------------------------------------- 1 | // inspired by http://dribbble.com/shots/857472-Toolbar 2 | 3 | // theme settings 4 | $medium-editor-bgcolor: #dee7f0; 5 | $medium-editor-bgcolor-alt: #5c90c7; 6 | $medium-editor-border-color: #cdd6e0; 7 | $medium-editor-button-size: 50px; 8 | $medium-editor-button-hover-text-color: #fff; 9 | $medium-editor-button-active-text-color: #000; 10 | $medium-editor-link-color: #40648a; 11 | $medium-editor-border-radius: 2px; 12 | 13 | // theme rules 14 | .medium-toolbar-arrow-under:after, 15 | .medium-toolbar-arrow-over:before { 16 | display: none; 17 | } 18 | 19 | .medium-editor-toolbar { 20 | border: 1px solid $medium-editor-border-color; 21 | background-color: $medium-editor-bgcolor; 22 | background-color: rgba($medium-editor-bgcolor, .95); 23 | background: linear-gradient(to top, $medium-editor-bgcolor, rgba(#fff, 1)); 24 | border-radius: $medium-editor-border-radius; 25 | box-shadow: 0 2px 6px rgba(#000, .45); 26 | 27 | li { 28 | button { 29 | min-width: $medium-editor-button-size; 30 | height: $medium-editor-button-size; 31 | border: none; 32 | border-right: 1px solid $medium-editor-border-color; 33 | background-color: transparent; 34 | color: $medium-editor-link-color; 35 | transition: background-color .2s ease-in, color .2s ease-in; 36 | &:hover { 37 | background-color: $medium-editor-bgcolor-alt; 38 | background-color: rgba($medium-editor-bgcolor-alt, .45); 39 | color: $medium-editor-button-hover-text-color; 40 | } 41 | } 42 | 43 | .medium-editor-button-first { 44 | border-top-left-radius: $medium-editor-border-radius; 45 | border-bottom-left-radius: $medium-editor-border-radius; 46 | } 47 | 48 | .medium-editor-button-last { 49 | border-top-right-radius: $medium-editor-border-radius; 50 | border-bottom-right-radius: $medium-editor-border-radius; 51 | } 52 | 53 | .medium-editor-button-active { 54 | background-color: $medium-editor-bgcolor-alt; 55 | background-color: rgba($medium-editor-bgcolor-alt, .45); 56 | color: $medium-editor-button-active-text-color; 57 | background: linear-gradient(to bottom, $medium-editor-bgcolor, rgba(#000, .1)); 58 | } 59 | } 60 | } 61 | 62 | .medium-editor-toolbar-form { 63 | background: $medium-editor-bgcolor; 64 | color: #999; 65 | border-radius: $medium-editor-border-radius; 66 | 67 | .medium-editor-toolbar-input { 68 | height: $medium-editor-button-size; 69 | background: $medium-editor-bgcolor; 70 | color: $medium-editor-link-color; 71 | box-sizing: border-box; 72 | } 73 | 74 | a { 75 | color: $medium-editor-link-color; 76 | } 77 | } 78 | 79 | .medium-editor-toolbar-anchor-preview { 80 | background: $medium-editor-bgcolor; 81 | color: $medium-editor-link-color; 82 | border-radius: $medium-editor-border-radius; 83 | } 84 | 85 | .medium-editor-placeholder:after { 86 | color: $medium-editor-border-color; 87 | } 88 | -------------------------------------------------------------------------------- /demo/button-example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | MediumEditor - Button Example 6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 |

Medium Editor

14 |
15 |

Font Awesome

16 |

My father’s family name being Pirrip, and my Christian name Philip, my infant tongue could make of both names nothing longer or more explicit than Pip. So, I called myself Pip, and came to be called Pip.

17 |

I give Pirrip as my father’s family name, on the authority of his tombstone and my sister,—Mrs. Joe Gargery, who married the blacksmith. As I never saw my father or my mother, and never saw any likeness of either of them (for their days were long before the days of photographs), my first fancies regarding what they were like were unreasonably derived from their tombstones...

18 |
19 |
20 |

Source

21 | 22 | 23 | 24 | 25 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "disallowEmptyBlocks": true, 3 | "disallowKeywordsOnNewLine": [ 4 | "else" 5 | ], 6 | "disallowMixedSpacesAndTabs": true, 7 | "disallowMultipleLineBreaks": true, 8 | "disallowMultipleLineStrings": true, 9 | "disallowMultipleSpaces": true, 10 | "disallowNewlineBeforeBlockStatements": true, 11 | "disallowSpaceAfterPrefixUnaryOperators": [ 12 | "++", 13 | "--", 14 | "+", 15 | "-", 16 | "~", 17 | "!" 18 | ], 19 | "disallowSpaceAfterObjectKeys": true, 20 | "disallowSpaceBeforePostfixUnaryOperators": [ 21 | "++", 22 | "--" 23 | ], 24 | "disallowSpacesInCallExpression": true, 25 | "disallowSpacesInFunctionDeclaration": { 26 | "beforeOpeningRoundBrace": true 27 | }, 28 | "disallowSpacesInsideArrayBrackets": true, 29 | "disallowSpacesInsideBrackets": true, 30 | "disallowSpacesInsideParentheses": true, 31 | "disallowTrailingComma": true, 32 | "disallowTrailingWhitespace": true, 33 | "requireBlocksOnNewline": true, 34 | "requireCamelCaseOrUpperCaseIdentifiers": true, 35 | "requireCapitalizedConstructors": true, 36 | "requireCommaBeforeLineBreak": true, 37 | "requireCurlyBraces": [ 38 | "if", 39 | "else", 40 | "for", 41 | "while", 42 | "do", 43 | "try", 44 | "catch" 45 | ], 46 | "requireLineBreakAfterVariableAssignment": true, 47 | "requireMultipleVarDecl": true, 48 | "requireOperatorBeforeLineBreak": [ 49 | "?", 50 | "=", 51 | "+", 52 | "-", 53 | "/", 54 | "*", 55 | "==", 56 | "===", 57 | "!=", 58 | "!==", 59 | ">", 60 | ">=", 61 | "<", 62 | "<=" 63 | ], 64 | "requireSemicolons": true, 65 | "requireSpaceAfterBinaryOperators": [ 66 | "=", 67 | ",", 68 | "+", 69 | "-", 70 | "/", 71 | "*", 72 | "==", 73 | "===", 74 | "!=", 75 | "!==" 76 | ], 77 | "requireSpaceAfterKeywords": [ 78 | "do", 79 | "for", 80 | "if", 81 | "else", 82 | "switch", 83 | "case", 84 | "try", 85 | "catch", 86 | "void", 87 | "while", 88 | "with", 89 | "return", 90 | "typeof", 91 | "function" 92 | ], 93 | "requireSpaceBeforeBinaryOperators": [ 94 | "=", 95 | "+", 96 | "-", 97 | "/", 98 | "*", 99 | "==", 100 | "===", 101 | "!=", 102 | "!==" 103 | ], 104 | "requireSpaceBeforeBlockStatements": true, 105 | "requireSpaceBeforeKeywords": [ 106 | "else", 107 | "while", 108 | "catch" 109 | ], 110 | "requireSpaceBetweenArguments": true, 111 | "requireSpacesInAnonymousFunctionExpression": { 112 | "beforeOpeningRoundBrace": true, 113 | "beforeOpeningCurlyBrace": true 114 | }, 115 | "requireSpacesInConditionalExpression": { 116 | "afterTest": true, 117 | "beforeConsequent": true, 118 | "afterConsequent": true, 119 | "beforeAlternate": true 120 | }, 121 | "requireSpacesInForStatement": true, 122 | "requireSpacesInFunctionDeclaration": { 123 | "beforeOpeningCurlyBrace": true 124 | }, 125 | "requireSpacesInFunction": { 126 | "beforeOpeningCurlyBrace": true 127 | }, 128 | "requireSpacesInsideObjectBrackets": { 129 | "allExcept": [ "}", ")" ] 130 | }, 131 | "validateIndentation": 4, 132 | "validateParameterSeparator": ", ", 133 | "validateQuoteMarks": "'" 134 | } 135 | -------------------------------------------------------------------------------- /spec/header-tags.spec.js: -------------------------------------------------------------------------------- 1 | /*global fireEvent */ 2 | 3 | describe('Protect Header Tags TestCase', function () { 4 | 'use strict'; 5 | 6 | beforeEach(function () { 7 | setupTestHelpers.call(this); 8 | this.el = this.createElement('div', 'editor', '

lorem ipsum

'); 9 | this.el.id = 'editor'; 10 | }); 11 | 12 | afterEach(function () { 13 | this.cleanupTest(); 14 | }); 15 | 16 | describe('ProtectHeaderTags', function () { 17 | it('header intact after leading return', function () { 18 | // place cursor at begining of header 19 | var editor = this.newMediumEditor('.editor'), 20 | el = document.getElementById('header'), 21 | range = document.createRange(), 22 | sel = window.getSelection(); 23 | 24 | range.setStart(el, 0); 25 | range.collapse(true); 26 | sel.removeAllRanges(); 27 | sel.addRange(range); 28 | 29 | // hit return 30 | fireEvent(editor.elements[0], 'keypress', { 31 | keyCode: MediumEditor.util.keyCode.ENTER 32 | }); 33 | 34 | el = document.getElementById('header'); 35 | expect(el).toBeDefined(); 36 | expect(el.nodeName.toLowerCase()).toBe('h2'); 37 | }); 38 | 39 | it('header leading return inserts paragraph, not additional header', function () { 40 | // place cursor at begining of header 41 | var editor = this.newMediumEditor('.editor'), 42 | el = document.getElementById('header'), 43 | range = document.createRange(), 44 | sel = window.getSelection(); 45 | range.setStart(el, 0); 46 | range.collapse(true); 47 | sel.removeAllRanges(); 48 | sel.addRange(range); 49 | 50 | // hit return 51 | fireEvent(editor.elements[0], 'keypress', { 52 | keyCode: MediumEditor.util.keyCode.ENTER 53 | }); 54 | 55 | el = document.getElementById('header'); 56 | expect(el.previousElementSibling.nodeName.toLowerCase()).toBe('p'); 57 | 58 | }); 59 | 60 | it('header leading backspace into empty p preserves header', function () { 61 | // place cursor at begining of header 62 | var editor = this.newMediumEditor('.editor'), 63 | originalHTML = document.getElementById('editor').innerHTML, 64 | el = document.getElementById('header'), 65 | range = document.createRange(), 66 | sel = window.getSelection(); 67 | range.setStart(el, 0); 68 | range.collapse(true); 69 | sel.removeAllRanges(); 70 | sel.addRange(range); 71 | 72 | // hit backspace 73 | fireEvent(editor.elements[0].querySelector(el.nodeName.toLowerCase()), 'keydown', { 74 | keyCode: MediumEditor.util.keyCode.BACKSPACE 75 | }); 76 | 77 | el = document.getElementById('header'); 78 | expect(el).toBeDefined(); 79 | expect(el.nodeName.toLowerCase()).toBe('h2'); 80 | 81 | el = document.getElementById('editor'); 82 | expect(el.innerHTML).not.toBe(originalHTML); 83 | 84 | }); 85 | 86 | }); 87 | 88 | }); 89 | -------------------------------------------------------------------------------- /src/js/extensions/keyboard-commands.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | var KeyboardCommands = MediumEditor.Extension.extend({ 5 | name: 'keyboard-commands', 6 | 7 | /* KeyboardCommands Options */ 8 | 9 | /* commands: [Array] 10 | * Array of objects describing each command and the combination of keys that will trigger it 11 | * Required for each object: 12 | * command [String] (argument passed to editor.execAction()) 13 | * key [String] (keyboard character that triggers this command) 14 | * meta [boolean] (whether the ctrl/meta key has to be active or inactive) 15 | * shift [boolean] (whether the shift key has to be active or inactive) 16 | * alt [boolean] (whether the alt key has to be active or inactive) 17 | */ 18 | commands: [ 19 | { 20 | command: 'bold', 21 | key: 'B', 22 | meta: true, 23 | shift: false, 24 | alt: false 25 | }, 26 | { 27 | command: 'italic', 28 | key: 'I', 29 | meta: true, 30 | shift: false, 31 | alt: false 32 | }, 33 | { 34 | command: 'underline', 35 | key: 'U', 36 | meta: true, 37 | shift: false, 38 | alt: false 39 | } 40 | ], 41 | 42 | init: function () { 43 | MediumEditor.Extension.prototype.init.apply(this, arguments); 44 | 45 | this.subscribe('editableKeydown', this.handleKeydown.bind(this)); 46 | this.keys = {}; 47 | this.commands.forEach(function (command) { 48 | var keyCode = command.key.charCodeAt(0); 49 | if (!this.keys[keyCode]) { 50 | this.keys[keyCode] = []; 51 | } 52 | this.keys[keyCode].push(command); 53 | }, this); 54 | }, 55 | 56 | handleKeydown: function (event) { 57 | var keyCode = MediumEditor.util.getKeyCode(event); 58 | if (!this.keys[keyCode]) { 59 | return; 60 | } 61 | 62 | var isMeta = MediumEditor.util.isMetaCtrlKey(event), 63 | isShift = !!event.shiftKey, 64 | isAlt = !!event.altKey; 65 | 66 | this.keys[keyCode].forEach(function (data) { 67 | if (data.meta === isMeta && 68 | data.shift === isShift && 69 | (data.alt === isAlt || 70 | undefined === data.alt)) { // TODO deprecated: remove check for undefined === data.alt when jumping to 6.0.0 71 | event.preventDefault(); 72 | event.stopPropagation(); 73 | 74 | // command can be a function to execute 75 | if (typeof data.command === 'function') { 76 | data.command.apply(this); 77 | } 78 | // command can be false so the shortcut is just disabled 79 | else if (false !== data.command) { 80 | this.execAction(data.command); 81 | } 82 | } 83 | }, this); 84 | } 85 | }); 86 | 87 | MediumEditor.extensions.keyboardCommands = KeyboardCommands; 88 | }()); 89 | -------------------------------------------------------------------------------- /spec/exploits.spec.js: -------------------------------------------------------------------------------- 1 | describe('Exploits', function () { 2 | 'use strict'; 3 | 4 | beforeEach(function () { 5 | setupTestHelpers.call(this); 6 | this.el = this.createElement('div', 'editor', 'hhh'); 7 | this.el.id = 'paste-editor'; 8 | }); 9 | 10 | afterEach(function () { 11 | this.cleanupTest(); 12 | }); 13 | 14 | it('Should not execute javascript with disableReturn false', function () { 15 | var evt, range, 16 | editorEl = this.el, 17 | sel = window.getSelection(), 18 | editor = this.newMediumEditor('.editor', { 19 | delay: 200, 20 | disableReturn: false 21 | }), 22 | pasteHandler = editor.getExtensionByName('paste'), 23 | test = { 24 | source: 'img onerror handler', 25 | paste: '>', 26 | output: '><img src="x" onerror="alert(\'xss\')">' 27 | }; 28 | 29 | // mock event with clipboardData API 30 | // test requires creating a function, so can't loop or jslint balks 31 | evt = { 32 | preventDefault: function () { 33 | return; 34 | }, 35 | clipboardData: { 36 | getData: function () { 37 | // do we need to return different results for the different types? text/plain, text/html 38 | return test.paste; 39 | } 40 | } 41 | }; 42 | 43 | editorEl.innerHTML = ' '; 44 | 45 | range = document.createRange(); 46 | range.selectNodeContents(editorEl.firstChild); 47 | sel.removeAllRanges(); 48 | sel.addRange(range); 49 | 50 | pasteHandler.handlePaste(evt, editorEl); 51 | jasmine.clock().tick(100); 52 | expect(editorEl.innerHTML).toEqual(test.output); 53 | }); 54 | 55 | it('Should not execute javascript with disableReturn true', function () { 56 | var evt, range, 57 | editorEl = this.el, 58 | sel = window.getSelection(), 59 | editor = this.newMediumEditor('.editor', { 60 | delay: 200, 61 | disableReturn: true 62 | }), 63 | pasteHandler = editor.getExtensionByName('paste'), 64 | test = { 65 | source: 'img onerror handler', 66 | paste: '>', 67 | output: '><img src="x" onerror="alert(\'xss\')">' 68 | }; 69 | 70 | // mock event with clipboardData API 71 | // test requires creating a function, so can't loop or jslint balks 72 | evt = { 73 | preventDefault: function () { 74 | return; 75 | }, 76 | clipboardData: { 77 | getData: function () { 78 | // do we need to return different results for the different types? text/plain, text/html 79 | return test.paste; 80 | } 81 | } 82 | }; 83 | 84 | editorEl.innerHTML = ' '; 85 | 86 | range = document.createRange(); 87 | range.selectNodeContents(document.getElementById('editor-inner')); 88 | sel.removeAllRanges(); 89 | sel.addRange(range); 90 | 91 | pasteHandler.handlePaste(evt, editorEl); 92 | jasmine.clock().tick(100); 93 | expect(editorEl.innerHTML).toEqual(test.output); 94 | }); 95 | }); 96 | -------------------------------------------------------------------------------- /spec/full-content.spec.js: -------------------------------------------------------------------------------- 1 | /*global selectElementContentsAndFire */ 2 | 3 | describe('Full Content Action TestCase', function () { 4 | 'use strict'; 5 | 6 | beforeEach(function () { 7 | setupTestHelpers.call(this); 8 | this.el = this.createElement('div', 'editor', 'lorem ipsum'); 9 | }); 10 | 11 | afterEach(function () { 12 | this.cleanupTest(); 13 | }); 14 | 15 | describe('All editable contents', function () { 16 | it('should be bolded and unbolded when using a full-bold command', function () { 17 | /*jslint regexp: true*/ 18 | var resultRegEx = /^<(b|strong)>lorem ipsum<\/(b|strong)>$/gi; 19 | /*jslint regexp: false*/ 20 | 21 | this.el.innerHTML = 'lorem ipsum'; 22 | var editor = this.newMediumEditor('.editor'); 23 | selectElementContentsAndFire(editor.elements[0]); 24 | 25 | editor.execAction('full-bold'); 26 | expect(this.el.innerHTML).toBe('lorem ipsum'); 27 | 28 | editor.execAction('full-bold'); 29 | expect(resultRegEx.test(this.el.innerHTML)).toBe(true); 30 | }); 31 | }); 32 | 33 | describe('Selection', function () { 34 | it('should preserve selection after multiple full-content commands', function () { 35 | this.el.innerHTML = '

lorem ipsum dolor

'; 36 | 37 | var editor = this.newMediumEditor('.editor'), 38 | // Beacuse not all browsers use or , check for both 39 | sTagO = '<(s|strike)>', 40 | sTagC = '', 41 | regex = new RegExp('^

lorem ' + sTagO + 'ipsum' + sTagC + ' dolor

$'); 42 | 43 | selectElementContentsAndFire(editor.elements[0].querySelector('u')); 44 | 45 | editor.execAction('full-underline'); 46 | expect(this.el.innerHTML).toBe('

lorem ipsum dolor

'); 47 | 48 | editor.execAction('full-underline'); 49 | expect(this.el.innerHTML).toBe('

lorem ipsum dolor

'); 50 | 51 | // Ensure the selection is still maintained 52 | editor.execAction('strikethrough'); 53 | expect(this.el.innerHTML).toMatch(regex); 54 | }); 55 | 56 | it('should justify all contents including multiple block elements', function () { 57 | this.el.innerHTML = '

lorem ipsum dolor

lorem ipsum dolor

'; 58 | var editor = this.newMediumEditor('.editor'); 59 | selectElementContentsAndFire(editor.elements[0].firstChild); 60 | expect(window.getComputedStyle(editor.elements[0].childNodes[0]).getPropertyValue('text-align').indexOf('center')).not.toBe(-1); 61 | expect(window.getComputedStyle(editor.elements[0].childNodes[1]).getPropertyValue('text-align').indexOf('left')).not.toBe(-1); 62 | 63 | editor.execAction('full-justifyRight'); 64 | expect(window.getComputedStyle(editor.elements[0].childNodes[0]).getPropertyValue('text-align').indexOf('right')).not.toBe(-1); 65 | expect(window.getComputedStyle(editor.elements[0].childNodes[1]).getPropertyValue('text-align').indexOf('right')).not.toBe(-1); 66 | 67 | // Ensure only original selected

is affected 68 | editor.execAction('justifyFull'); 69 | expect(window.getComputedStyle(editor.elements[0].childNodes[0]).getPropertyValue('text-align')).toBe('justify'); 70 | expect(window.getComputedStyle(editor.elements[0].childNodes[1]).getPropertyValue('text-align').indexOf('right')).not.toBe(-1); 71 | }); 72 | }); 73 | }); 74 | -------------------------------------------------------------------------------- /demo/clean-paste.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | medium editor | demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | Fork me on GitHub 13 |

14 |

Medium Editor

15 |
16 |

My father’s family name being Pirrip, and my Christian name Philip, my infant tongue could make of both names nothing longer or more explicit than Pip. So, I called myself Pip, and came to be called Pip.

17 |

I give Pirrip as my father’s family name, on the authority of his tombstone and my sister,—Mrs. Joe Gargery, who married the blacksmith. As I never saw my father or my mother, and never saw any likeness of either of them (for their days were long before the days of photographs), my first fancies regarding what they were like were unreasonably derived from their tombstones. The shape of the letters on my father’s, gave me an odd idea that he was a square, stout, dark man, with curly black hair. From the character and turn of the inscription, “Also Georgiana Wife of the Above,” I drew a childish conclusion that my mother was freckled and sickly. To five little stone lozenges, each about a foot and a half long, which were arranged in a neat row beside their grave, and were sacred to the memory of five little brothers of mine,—who gave up trying to get a living, exceedingly early in that universal struggle,—I am indebted for a belief I religiously entertained that they had all been born on their backs with their hands in their trousers-pockets, and had never taken them out in this state of existence.

18 |

Ours was the marsh country, down by the river, within, as the river wound, twenty miles of the sea. My first most vivid and broad impression of the identity of things seems to me to have been gained on a memorable raw afternoon towards evening. At such a time I found out for certain that this bleak place overgrown with nettles was the churchyard; and that Philip Pirrip, late of this parish, and also Georgiana wife of the above, were dead and buried; and that Alexander, Bartholomew, Abraham, Tobias, and Roger, infant children of the aforesaid, were also dead and buried; and that the dark flat wilderness beyond the churchyard, intersected with dikes and mounds and gates, with scattered cattle feeding on it, was the marshes; and that the low leaden line beyond was the river; and that the distant savage lair from which the wind was rushing was the sea; and that the small bundle of shivers growing afraid of it all and beginning to cry, was Pip.

19 |
20 |
21 |

Source

22 | 23 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/js/extensions/file-dragging.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | var CLASS_DRAG_OVER = 'medium-editor-dragover'; 5 | 6 | function clearClassNames(element) { 7 | var editable = MediumEditor.util.getContainerEditorElement(element), 8 | existing = Array.prototype.slice.call(editable.parentElement.querySelectorAll('.' + CLASS_DRAG_OVER)); 9 | 10 | existing.forEach(function (el) { 11 | el.classList.remove(CLASS_DRAG_OVER); 12 | }); 13 | } 14 | 15 | var FileDragging = MediumEditor.Extension.extend({ 16 | name: 'fileDragging', 17 | 18 | allowedTypes: ['image'], 19 | 20 | init: function () { 21 | MediumEditor.Extension.prototype.init.apply(this, arguments); 22 | 23 | this.subscribe('editableDrag', this.handleDrag.bind(this)); 24 | this.subscribe('editableDrop', this.handleDrop.bind(this)); 25 | }, 26 | 27 | handleDrag: function (event) { 28 | event.preventDefault(); 29 | event.dataTransfer.dropEffect = 'copy'; 30 | 31 | var target = event.target.classList ? event.target : event.target.parentElement; 32 | 33 | // Ensure the class gets removed from anything that had it before 34 | clearClassNames(target); 35 | 36 | if (event.type === 'dragover') { 37 | target.classList.add(CLASS_DRAG_OVER); 38 | } 39 | }, 40 | 41 | handleDrop: function (event) { 42 | // Prevent file from opening in the current window 43 | event.preventDefault(); 44 | event.stopPropagation(); 45 | // Select the dropping target, and set the selection to the end of the target 46 | // https://github.com/yabwe/medium-editor/issues/980 47 | this.base.selectElement(event.target); 48 | var selection = this.base.exportSelection(); 49 | selection.start = selection.end; 50 | this.base.importSelection(selection); 51 | // IE9 does not support the File API, so prevent file from opening in the window 52 | // but also don't try to actually get the file 53 | if (event.dataTransfer.files) { 54 | Array.prototype.slice.call(event.dataTransfer.files).forEach(function (file) { 55 | if (this.isAllowedFile(file)) { 56 | if (file.type.match('image')) { 57 | this.insertImageFile(file); 58 | } 59 | } 60 | }, this); 61 | } 62 | 63 | // Make sure we remove our class from everything 64 | clearClassNames(event.target); 65 | }, 66 | 67 | isAllowedFile: function (file) { 68 | return this.allowedTypes.some(function (fileType) { 69 | return !!file.type.match(fileType); 70 | }); 71 | }, 72 | 73 | insertImageFile: function (file) { 74 | if (typeof FileReader !== 'function') { 75 | return; 76 | } 77 | var fileReader = new FileReader(); 78 | fileReader.readAsDataURL(file); 79 | 80 | // attach the onload event handler, makes it easier to listen in with jasmine 81 | fileReader.addEventListener('load', function (e) { 82 | var addImageElement = this.document.createElement('img'); 83 | addImageElement.src = e.target.result; 84 | MediumEditor.util.insertHTMLCommand(this.document, addImageElement.outerHTML); 85 | }.bind(this)); 86 | } 87 | }); 88 | 89 | MediumEditor.extensions.fileDragging = FileDragging; 90 | }()); 91 | -------------------------------------------------------------------------------- /src/sass/themes/bootstrap.scss: -------------------------------------------------------------------------------- 1 | // theme settings 2 | $medium-editor-bgcolor: #428bca; 3 | $medium-editor-border-color: #357ebd; 4 | $medium-editor-button-size: 60px; 5 | $medium-editor-button-active-text-color: #fff; 6 | $medium-editor-hover-color: #3276b1; 7 | $medium-editor-link-color: #fff; 8 | $medium-editor-border-radius: 4px; 9 | $medium-editor-placeholder-color: #fff; 10 | 11 | // theme rules 12 | .medium-toolbar-arrow-under:after { 13 | border-color: $medium-editor-bgcolor transparent transparent transparent; 14 | top: $medium-editor-button-size; 15 | } 16 | 17 | .medium-toolbar-arrow-over:before { 18 | border-color: transparent transparent $medium-editor-bgcolor transparent; 19 | } 20 | 21 | .medium-editor-toolbar { 22 | background-color: $medium-editor-bgcolor; 23 | border: 1px solid $medium-editor-border-color; 24 | border-radius: $medium-editor-border-radius; 25 | 26 | li { 27 | button { 28 | background-color: transparent; 29 | border: none; 30 | border-right: 1px solid $medium-editor-border-color; 31 | box-sizing: border-box; 32 | color: $medium-editor-link-color; 33 | height: $medium-editor-button-size; 34 | min-width: $medium-editor-button-size; 35 | transition: background-color .2s ease-in, color .2s ease-in; 36 | &:hover { 37 | background-color: $medium-editor-hover-color; 38 | color: $medium-editor-button-active-text-color; 39 | } 40 | } 41 | 42 | .medium-editor-button-first { 43 | border-bottom-left-radius: $medium-editor-border-radius; 44 | border-top-left-radius: $medium-editor-border-radius; 45 | } 46 | 47 | .medium-editor-button-last { 48 | border-bottom-right-radius: $medium-editor-border-radius; 49 | border-right: none; 50 | border-top-right-radius: $medium-editor-border-radius; 51 | } 52 | 53 | .medium-editor-button-active { 54 | background-color: $medium-editor-hover-color; 55 | color: $medium-editor-button-active-text-color; 56 | } 57 | } 58 | } 59 | 60 | .medium-editor-toolbar-form { 61 | background: $medium-editor-bgcolor; 62 | border-radius: $medium-editor-border-radius; 63 | color: #fff; 64 | 65 | .medium-editor-toolbar-input { 66 | background: $medium-editor-bgcolor; 67 | color: $medium-editor-link-color; 68 | height: $medium-editor-button-size; 69 | 70 | &::-webkit-input-placeholder { 71 | color: $medium-editor-placeholder-color; 72 | color: rgba($medium-editor-placeholder-color, .8); 73 | } 74 | &:-moz-placeholder { /* Firefox 18- */ 75 | color: $medium-editor-placeholder-color; 76 | color: rgba($medium-editor-placeholder-color, .8); 77 | } 78 | &::-moz-placeholder { /* Firefox 19+ */ 79 | color: $medium-editor-placeholder-color; 80 | color: rgba($medium-editor-placeholder-color, .8); 81 | } 82 | &:-ms-input-placeholder { 83 | color: $medium-editor-placeholder-color; 84 | color: rgba($medium-editor-placeholder-color, .8); 85 | } 86 | } 87 | 88 | a { 89 | color: $medium-editor-link-color; 90 | } 91 | } 92 | 93 | .medium-editor-toolbar-anchor-preview { 94 | background: $medium-editor-bgcolor; 95 | border-radius: $medium-editor-border-radius; 96 | color: $medium-editor-link-color; 97 | } 98 | 99 | .medium-editor-placeholder:after { 100 | color: $medium-editor-border-color; 101 | } 102 | -------------------------------------------------------------------------------- /src/sass/themes/tim.scss: -------------------------------------------------------------------------------- 1 | // theme settings 2 | $medium-editor-bgcolor: #2f1e07; 3 | $medium-editor-border-color: lighten($medium-editor-bgcolor, 10); 4 | $medium-editor-button-size: 60px; 5 | $medium-editor-button-active-text-color: #ffedd5; 6 | $medium-editor-hover-color: darken($medium-editor-bgcolor, 10); 7 | $medium-editor-link-color: #ffedd5; 8 | $medium-editor-border-radius: 6px; 9 | $medium-editor-placeholder-color: #ffedd5; 10 | 11 | // theme rules 12 | .medium-toolbar-arrow-under:after { 13 | border-color: $medium-editor-bgcolor transparent transparent transparent; 14 | top: $medium-editor-button-size; 15 | } 16 | 17 | .medium-toolbar-arrow-over:before { 18 | border-color: transparent transparent $medium-editor-bgcolor transparent; 19 | } 20 | 21 | .medium-editor-toolbar { 22 | background-color: $medium-editor-bgcolor; 23 | border: 1px solid $medium-editor-border-color; 24 | border-radius: $medium-editor-border-radius; 25 | 26 | li { 27 | button { 28 | background-color: transparent; 29 | border: none; 30 | border-right: 1px solid $medium-editor-border-color; 31 | box-sizing: border-box; 32 | color: $medium-editor-link-color; 33 | height: $medium-editor-button-size; 34 | min-width: $medium-editor-button-size; 35 | transition: background-color .2s ease-in, color .2s ease-in; 36 | &:hover { 37 | background-color: $medium-editor-hover-color; 38 | color: $medium-editor-button-active-text-color; 39 | } 40 | } 41 | 42 | .medium-editor-button-first { 43 | border-bottom-left-radius: $medium-editor-border-radius; 44 | border-top-left-radius: $medium-editor-border-radius; 45 | } 46 | 47 | .medium-editor-button-last { 48 | border-bottom-right-radius: $medium-editor-border-radius; 49 | border-right: none; 50 | border-top-right-radius: $medium-editor-border-radius; 51 | } 52 | 53 | .medium-editor-button-active { 54 | background-color: $medium-editor-hover-color; 55 | color: $medium-editor-button-active-text-color; 56 | } 57 | } 58 | } 59 | 60 | .medium-editor-toolbar-form { 61 | background: $medium-editor-bgcolor; 62 | border-radius: $medium-editor-border-radius; 63 | color: #ffedd5; 64 | 65 | .medium-editor-toolbar-input { 66 | background: $medium-editor-bgcolor; 67 | color: $medium-editor-link-color; 68 | height: $medium-editor-button-size; 69 | 70 | &::-webkit-input-placeholder { 71 | color: $medium-editor-placeholder-color; 72 | color: rgba($medium-editor-placeholder-color, .8); 73 | } 74 | &:-moz-placeholder { /* Firefox 18- */ 75 | color: $medium-editor-placeholder-color; 76 | color: rgba($medium-editor-placeholder-color, .8); 77 | } 78 | &::-moz-placeholder { /* Firefox 19+ */ 79 | color: $medium-editor-placeholder-color; 80 | color: rgba($medium-editor-placeholder-color, .8); 81 | } 82 | &:-ms-input-placeholder { 83 | color: $medium-editor-placeholder-color; 84 | color: rgba($medium-editor-placeholder-color, .8); 85 | } 86 | } 87 | 88 | a { 89 | color: $medium-editor-link-color; 90 | } 91 | } 92 | 93 | .medium-editor-toolbar-anchor-preview { 94 | background: $medium-editor-bgcolor; 95 | border-radius: $medium-editor-border-radius; 96 | color: $medium-editor-link-color; 97 | } 98 | 99 | .medium-editor-placeholder:after { 100 | color: $medium-editor-border-color; 101 | } 102 | -------------------------------------------------------------------------------- /demo/nested-editable.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | medium editor | demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | Fork me on GitHub 13 |
14 | Theme: 15 | 22 |
23 |
24 |

Medium Editor

25 |
26 |

My father’s family name being Pirrip, and my Christian name Philip, my infant tongue could make of both names nothing longer or more explicit than Pip. So, I called myself Pip, and came to be called Pip.

27 | 28 |
29 |
30 |
31 |

this portion is not editable

32 |
33 |

this is editable

34 |

that seems really neat

35 |
36 |
37 |
38 |
39 | 40 |

I give Pirrip as my father’s family name, on the authority of his tombstone and my sister,—Mrs. Joe Gargery, who married the blacksmith. As I never saw my father or my mother, and never saw any likeness of either of them (for their days were long before the days of photographs), my first fancies regarding what they were like were unreasonably

derived from their tombstones. The shape of the letters on my father’s, gave me an odd idea that he was a square, stout, dark man, with curly black hair. From the character and turn of the inscription, “Also Georgiana Wife of the Above,” I drew a childish conclusion that my mother was freckled and sickly. To five little stone lozenges, each about a foot and a half long, which were arranged in a neat row beside their grave, and were sacred to the memory of five little brothers of mine,—who gave up trying to get a living, exceedingly early in that universal struggle,—I am indebted for a belief I religiously entertained that they had all been born on their backs with their hands in their trousers-pockets, and had never taken them out in this state of existence.

41 |
42 |
43 |

Source

44 | 45 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /src/sass/themes/beagle.scss: -------------------------------------------------------------------------------- 1 | // theme settings 2 | $medium-editor-bgcolor: #000; 3 | $medium-editor-button-size: 40px; 4 | $medium-editor-button-active-text-color: #a2d7c7; 5 | $medium-editor-hover-color: $medium-editor-bgcolor; 6 | $medium-editor-link-color: #ccc; 7 | $medium-editor-border-radius: 50px; 8 | $medium-editor-placeholder-color: #f8f5f3; 9 | 10 | // theme rules 11 | .medium-toolbar-arrow-under:after { 12 | border-color: $medium-editor-bgcolor transparent transparent transparent; 13 | top: $medium-editor-button-size; 14 | } 15 | 16 | .medium-toolbar-arrow-over:before { 17 | border-color: transparent transparent $medium-editor-bgcolor transparent; 18 | } 19 | 20 | .medium-editor-toolbar { 21 | background-color: $medium-editor-bgcolor; 22 | border: none; 23 | border-radius: $medium-editor-border-radius; 24 | 25 | li { 26 | button { 27 | background-color: transparent; 28 | border: none; 29 | box-sizing: border-box; 30 | color: $medium-editor-link-color; 31 | height: $medium-editor-button-size; 32 | min-width: $medium-editor-button-size; 33 | padding: 5px 12px; 34 | transition: background-color .2s ease-in, color .2s ease-in; 35 | &:hover { 36 | background-color: $medium-editor-hover-color; 37 | color: $medium-editor-button-active-text-color; 38 | } 39 | } 40 | 41 | .medium-editor-button-first { 42 | border-bottom-left-radius: $medium-editor-border-radius; 43 | border-top-left-radius: $medium-editor-border-radius; 44 | padding-left: 24px; 45 | } 46 | 47 | .medium-editor-button-last { 48 | border-bottom-right-radius: $medium-editor-border-radius; 49 | border-right: none; 50 | border-top-right-radius: $medium-editor-border-radius; 51 | padding-right: 24px 52 | } 53 | 54 | .medium-editor-button-active { 55 | background-color: $medium-editor-hover-color; 56 | color: $medium-editor-button-active-text-color; 57 | } 58 | } 59 | } 60 | 61 | .medium-editor-toolbar-form { 62 | background: $medium-editor-bgcolor; 63 | border-radius: $medium-editor-border-radius; 64 | color: $medium-editor-link-color; 65 | overflow: hidden; 66 | 67 | .medium-editor-toolbar-input { 68 | background: $medium-editor-bgcolor; 69 | box-sizing: border-box; 70 | color: $medium-editor-link-color; 71 | height: $medium-editor-button-size; 72 | padding-left: 16px; 73 | width: 220px; 74 | 75 | &::-webkit-input-placeholder { 76 | color: $medium-editor-placeholder-color; 77 | color: rgba($medium-editor-placeholder-color, .8); 78 | } 79 | &:-moz-placeholder { /* Firefox 18- */ 80 | color: $medium-editor-placeholder-color; 81 | color: rgba($medium-editor-placeholder-color, .8); 82 | } 83 | &::-moz-placeholder { /* Firefox 19+ */ 84 | color: $medium-editor-placeholder-color; 85 | color: rgba($medium-editor-placeholder-color, .8); 86 | } 87 | &:-ms-input-placeholder { 88 | color: $medium-editor-placeholder-color; 89 | color: rgba($medium-editor-placeholder-color, .8); 90 | } 91 | } 92 | 93 | a { 94 | color: $medium-editor-link-color; 95 | transform: translateY(2px); 96 | } 97 | 98 | .medium-editor-toolbar-close { 99 | margin-right: 16px; 100 | } 101 | } 102 | 103 | .medium-editor-toolbar-anchor-preview { 104 | background: $medium-editor-bgcolor; 105 | border-radius: $medium-editor-border-radius; 106 | padding: 5px 12px; 107 | } 108 | 109 | .medium-editor-anchor-preview { 110 | a { 111 | color: $medium-editor-link-color; 112 | text-decoration: none; 113 | } 114 | } 115 | 116 | .medium-editor-toolbar-actions { 117 | li, button { 118 | border-radius: $medium-editor-border-radius; 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /demo/multi-paragraph.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | medium editor | demo multi paragraph 6 | 7 | 8 | 9 | 10 | 11 | Fork me on GitHub 12 |
13 |

Medium Editor Demo

14 |
15 |

In this demo the toolbar will not appear if you select several paragraphs or block quotes.

16 |

First Paragraph

Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nullam ultrices ullamcorper nibh, ut imperdiet arcu rutrum et. Vestibulum vitae orci metus. Praesent dapibus interdum purus, vitae mattis urna pharetra a. Praesent sodales volutpat mi et rhoncus. Phasellus quis tortor nulla. Pellentesque dapibus lorem et eros lobortis, et iaculis est accumsan. Nunc et ligula laoreet, egestas est id, placerat dolor. Praesent sed gravida tortor, non elementum enim. Integer et nulla sit amet orci suscipit sagittis pellentesque a est. Maecenas vitae purus odio. Aenean tincidunt varius arcu a vehicula. Quisque vestibulum venenatis vestibulum. Nullam pellentesque purus non dui adipiscing tempus.

17 | 18 |

Second Paragraph

Pellentesque sit amet turpis a felis ornare euismod id vitae ante. Vivamus nec orci interdum, blandit dui id, gravida orci. Aliquam malesuada tristique imperdiet. Mauris lobortis, mi vel dictum feugiat, sem nibh hendrerit mi, eu pellentesque mauris quam et lorem. Integer ante ligula, placerat id pharetra ut, lacinia sit amet ante. Donec luctus, orci eu vestibulum suscipit, nisl lorem dignissim mauris, et auctor dolor odio nec mauris. Maecenas velit justo, lobortis a accumsan et, adipiscing sit amet leo. Vestibulum pharetra nisi erat, vitae dictum eros vestibulum eget. Ut aliquet lorem eu dui auctor aliquam. Nunc mollis elementum justo non ultricies. Nam dictum egestas augue sit amet ullamcorper. Praesent laoreet lectus ut velit porta varius. Vestibulum eget gravida sapien. Mauris viverra, metus vel varius posuere, augue metus aliquam sem, in pellentesque ipsum mauris non enim.

19 | 20 |
This is a block quote. Suspendisse potenti. Vestibulum semper felis vitae sapien ultricies venenatis. Aliquam mollis dui dolor, in auctor urna iaculis dapibus. Nam condimentum mollis sapien, non bibendum lacus feugiat eu. Phasellus feugiat erat ut varius tincidunt. Quisque suscipit ornare lacus, nec dapibus lacus vulputate at. Morbi at ipsum sollicitudin, suscipit elit sed, ultrices ligula. Suspendisse tincidunt libero iaculis velit iaculis, ac congue enim euismod. Nam molestie ligula at mattis tincidunt. Praesent porttitor nisi lectus, nec suscipit lectus porttitor nec.
21 | 22 |

Another paragrph. Suspendisse potenti. Vestibulum semper felis vitae sapien ultricies venenatis. Aliquam mollis dui dolor, in auctor urna iaculis dapibus. Nam condimentum mollis sapien, non bibendum lacus feugiat eu. Phasellus feugiat erat ut varius tincidunt. Quisque suscipit ornare lacus, nec dapibus lacus vulputate at. Morbi at ipsum sollicitudin, suscipit elit sed, ultrices ligula. Suspendisse tincidunt libero iaculis velit iaculis, ac congue enim euismod. Nam molestie ligula at mattis tincidunt. Praesent porttitor nisi lectus, nec suscipit lectus porttitor nec.

23 |
24 | 25 | 26 |
27 | 28 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /demo/custom-toolbar.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | medium editor | demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | Fork me on GitHub 13 |
14 |

Medium Editor

15 |
16 |

Font Awesome

17 |

My father’s family name being Pirrip, and my Christian name Philip, my infant tongue could make of both names nothing longer or more explicit than Pip. So, I called myself Pip, and came to be called Pip.

18 |

I give Pirrip as my father’s family name, on the authority of his tombstone and my sister,—Mrs. Joe Gargery, who married the blacksmith. As I never saw my father or my mother, and never saw any likeness of either of them (for their days were long before the days of photographs), my first fancies regarding what they were like were unreasonably derived from their tombstones...

19 |
20 |
21 |

Custom Labels

22 |

... Ours was the marsh country, down by the river, within, as the river wound, twenty miles of the sea. My first most vivid and broad impression of the identity of things seems to me to have been gained on a memorable raw afternoon towards evening. At such a time I found out for certain that this bleak place overgrown with nettles was the churchyard; and that Philip Pirrip, late of this parish, and also Georgiana wife of the above, were dead and buried; and that Alexander, Bartholomew, Abraham, Tobias, and Roger, infant children of the aforesaid, were also dead and buried; and that the dark flat wilderness beyond the churchyard, intersected with dikes and mounds and gates, with scattered cattle feeding on it, was the marshes; and that the low leaden line beyond was the river; and that the distant savage lair from which the wind was rushing was the sea; and that the small bundle of shivers growing afraid of it all and beginning to cry, was Pip.

23 |
24 |
25 |

Source

26 | 27 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /demo/pass-instance.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | medium editor | demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | Fork me on GitHub 13 |
14 |

Medium Editor

15 |
16 |

My father’s family name being Pirrip, and my Christian name Philip, my infant tongue could make of both names nothing longer or more explicit than Pip. So, I called myself Pip, and came to be called Pip.

17 |

I give Pirrip as my father’s family name, on the authority of his tombstone and my sister,—Mrs. Joe Gargery, who married the blacksmith. As I never saw my father or my mother, and never saw any likeness of either of them (for their days were long before the days of photographs), my first fancies regarding what they were like were unreasonably derived from their tombstones. The shape of the letters on my father’s, gave me an odd idea that he was a square, stout, dark man, with curly black hair. From the character and turn of the inscription, “Also Georgiana Wife of the Above,” I drew a childish conclusion that my mother was freckled and sickly. To five little stone lozenges, each about a foot and a half long, which were arranged in a neat row beside their grave, and were sacred to the memory of five little brothers of mine,—who gave up trying to get a living, exceedingly early in that universal struggle,—I am indebted for a belief I religiously entertained that they had all been born on their backs with their hands in their trousers-pockets, and had never taken them out in this state of existence.

18 |
19 |
20 |

Ours was the marsh country, down by the river, within, as the river wound, twenty miles of the sea. My first most vivid and broad impression of the identity of things seems to me to have been gained on a memorable raw afternoon towards evening. At such a time I found out for certain that this bleak place overgrown with nettles was the churchyard; and that Philip Pirrip, late of this parish, and also Georgiana wife of the above, were dead and buried; and that Alexander, Bartholomew, Abraham, Tobias, and Roger, infant children of the aforesaid, were also dead and buried; and that the dark flat wilderness beyond the churchyard, intersected with dikes and mounds and gates, with scattered cattle feeding on it, was the marshes; and that the low leaden line beyond was the river; and that the distant savage lair from which the wind was rushing was the sea; and that the small bundle of shivers growing afraid of it all and beginning to cry, was Pip.

21 |
22 |
23 |

Source

24 | 25 | 59 | 60 | 61 | -------------------------------------------------------------------------------- /dist/css/medium-editor.min.css: -------------------------------------------------------------------------------- 1 | .medium-editor-anchor-preview,.medium-editor-toolbar{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif;font-size:16px;z-index:2000}@-webkit-keyframes medium-editor-image-loading{0%{-webkit-transform:scale(0);transform:scale(0)}100%{-webkit-transform:scale(1);transform:scale(1)}}@keyframes medium-editor-image-loading{0%{-webkit-transform:scale(0);transform:scale(0)}100%{-webkit-transform:scale(1);transform:scale(1)}}@-webkit-keyframes medium-editor-pop-upwards{0%{opacity:0;-webkit-transform:matrix(.97,0,0,1,0,12);transform:matrix(.97,0,0,1,0,12)}20%{opacity:.7;-webkit-transform:matrix(.99,0,0,1,0,2);transform:matrix(.99,0,0,1,0,2)}40%{opacity:1;-webkit-transform:matrix(1,0,0,1,0,-1);transform:matrix(1,0,0,1,0,-1)}100%{-webkit-transform:matrix(1,0,0,1,0,0);transform:matrix(1,0,0,1,0,0)}}@keyframes medium-editor-pop-upwards{0%{opacity:0;-webkit-transform:matrix(.97,0,0,1,0,12);transform:matrix(.97,0,0,1,0,12)}20%{opacity:.7;-webkit-transform:matrix(.99,0,0,1,0,2);transform:matrix(.99,0,0,1,0,2)}40%{opacity:1;-webkit-transform:matrix(1,0,0,1,0,-1);transform:matrix(1,0,0,1,0,-1)}100%{-webkit-transform:matrix(1,0,0,1,0,0);transform:matrix(1,0,0,1,0,0)}}.medium-editor-anchor-preview{left:0;line-height:1.4;max-width:280px;position:absolute;text-align:center;top:0;word-break:break-all;word-wrap:break-word;visibility:hidden}.medium-editor-anchor-preview a{color:#fff;display:inline-block;margin:5px 5px 10px}.medium-editor-placeholder-relative:after,.medium-editor-placeholder:after{content:attr(data-placeholder)!important;white-space:pre;padding:inherit;margin:inherit;font-style:italic}.medium-editor-anchor-preview-active{visibility:visible}.medium-editor-dragover{background:#ddd}.medium-editor-image-loading{-webkit-animation:medium-editor-image-loading 1s infinite ease-in-out;animation:medium-editor-image-loading 1s infinite ease-in-out;background-color:#333;border-radius:100%;display:inline-block;height:40px;width:40px}.medium-editor-placeholder{position:relative}.medium-editor-placeholder:after{position:absolute;left:0;top:0}.medium-editor-placeholder-relative,.medium-editor-placeholder-relative:after{position:relative}.medium-toolbar-arrow-over:before,.medium-toolbar-arrow-under:after{border-style:solid;content:'';display:block;height:0;left:50%;margin-left:-8px;position:absolute;width:0}.medium-toolbar-arrow-under:after{border-width:8px 8px 0}.medium-toolbar-arrow-over:before{border-width:0 8px 8px;top:-8px}.medium-editor-toolbar{left:0;position:absolute;top:0;visibility:hidden}.medium-editor-toolbar ul{margin:0;padding:0}.medium-editor-toolbar li{float:left;list-style:none;margin:0;padding:0}.medium-editor-toolbar li button{box-sizing:border-box;cursor:pointer;display:block;font-size:14px;line-height:1.33;margin:0;padding:15px;text-decoration:none}.medium-editor-toolbar li button:focus{outline:0}.medium-editor-toolbar li .medium-editor-action-underline{text-decoration:underline}.medium-editor-toolbar li .medium-editor-action-pre{font-family:Consolas,"Liberation Mono",Menlo,Courier,monospace;font-size:12px;font-weight:100;padding:15px 0}.medium-editor-toolbar-active{visibility:visible}.medium-editor-sticky-toolbar{position:fixed;top:1px}.medium-editor-relative-toolbar{position:relative}.medium-editor-toolbar-active.medium-editor-stalker-toolbar{-webkit-animation:medium-editor-pop-upwards 160ms forwards linear;animation:medium-editor-pop-upwards 160ms forwards linear}.medium-editor-action-bold{font-weight:bolder}.medium-editor-action-italic{font-style:italic}.medium-editor-toolbar-form{display:none}.medium-editor-toolbar-form a,.medium-editor-toolbar-form input{font-family:"Helvetica Neue",Helvetica,Arial,sans-serif}.medium-editor-toolbar-form .medium-editor-toolbar-form-row{line-height:14px;margin-left:5px;padding-bottom:5px}.medium-editor-toolbar-form .medium-editor-toolbar-input,.medium-editor-toolbar-form label{border:none;box-sizing:border-box;font-size:14px;margin:0;padding:6px;width:316px;display:inline-block}.medium-editor-toolbar-form .medium-editor-toolbar-input:focus,.medium-editor-toolbar-form label:focus{-webkit-appearance:none;-moz-appearance:none;appearance:none;border:none;box-shadow:none;outline:0}.medium-editor-toolbar-form a{display:inline-block;font-size:24px;font-weight:bolder;margin:0 10px;text-decoration:none}.medium-editor-toolbar-form-active{display:block}.medium-editor-toolbar-actions:after{clear:both;content:"";display:table}.medium-editor-element{word-wrap:break-word;min-height:30px}.medium-editor-element img{max-width:100%}.medium-editor-element sub{vertical-align:sub}.medium-editor-element sup{vertical-align:super}.medium-editor-hidden{display:none} -------------------------------------------------------------------------------- /demo/auto-link.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | medium editor | demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | Fork me on GitHub 13 |
14 | Theme: 15 | 22 |
23 |
24 |

Medium Editor

25 |
26 |

My father’s family name being Pirrip, and my Christian name Philip, my infant tongue could make of both names nothing longer or more explicit than Pip. So, I called myself Pip, and came to be called Pip.

27 |

I give Pirrip as my father’s family name, on the authority of his tombstone and my sister,—Mrs. Joe Gargery, who married the blacksmith. As I never saw my father or my mother, and never saw any likeness of either of them (for their days were long before the days of photographs), my first fancies regarding what they were like were unreasonably derived from their tombstones. The shape of the letters on my father’s, gave me an odd idea that he was a square, stout, dark man, with curly black hair. From the character and turn of the inscription, “Also Georgiana Wife of the Above,” I drew a childish conclusion that my mother was freckled and sickly. To five little stone lozenges, each about a foot and a half long, which were arranged in a neat row beside their grave, and were sacred to the memory of five little brothers of mine,—who gave up trying to get a living, exceedingly early in that universal struggle,—I am indebted for a belief I religiously entertained that they had all been born on their backs with their hands in their trousers-pockets, and had never taken them out in this state of existence.

28 |

Ours was the marsh country, down by the river, within, as the river wound, twenty miles of the sea. My first most vivid and broad impression of the identity of things seems to me to have been gained on a memorable raw afternoon towards evening. At such a time I found out for certain that this bleak place overgrown with nettles was the churchyard; and that Philip Pirrip, late of this parish, and also Georgiana wife of the above, were dead and buried; and that Alexander, Bartholomew, Abraham, Tobias, and Roger, infant children of the aforesaid, were also dead and buried; and that the dark flat wilderness beyond the churchyard, intersected with dikes and mounds and gates, with scattered cattle feeding on it, was the marshes; and that the low leaden line beyond was the river; and that the distant savage lair from which the wind was rushing was the sea; and that the small bundle of shivers growing afraid of it all and beginning to cry, was Pip.

29 |

30 |
31 |
32 |

Source

33 | 34 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/js/extensions/form.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | /* Base functionality for an extension which will display 5 | * a 'form' inside the toolbar 6 | */ 7 | var FormExtension = MediumEditor.extensions.button.extend({ 8 | 9 | init: function () { 10 | MediumEditor.extensions.button.prototype.init.apply(this, arguments); 11 | }, 12 | 13 | // default labels for the form buttons 14 | formSaveLabel: '✓', 15 | formCloseLabel: '×', 16 | 17 | /* activeClass: [string] 18 | * set class which added to shown form 19 | */ 20 | activeClass: 'medium-editor-toolbar-form-active', 21 | 22 | /* hasForm: [boolean] 23 | * 24 | * Setting this to true will cause getForm() to be called 25 | * when the toolbar is created, so the form can be appended 26 | * inside the toolbar container 27 | */ 28 | hasForm: true, 29 | 30 | /* getForm: [function ()] 31 | * 32 | * When hasForm is true, this function must be implemented 33 | * and return a DOM Element which will be appended to 34 | * the toolbar container. The form should start hidden, and 35 | * the extension can choose when to hide/show it 36 | */ 37 | getForm: function () {}, 38 | 39 | /* isDisplayed: [function ()] 40 | * 41 | * This function should return true/false reflecting 42 | * whether the form is currently displayed 43 | */ 44 | isDisplayed: function () { 45 | if (this.hasForm) { 46 | return this.getForm().classList.contains(this.activeClass); 47 | } 48 | return false; 49 | }, 50 | 51 | /* showForm: [function ()] 52 | * 53 | * This function should show the form element inside 54 | * the toolbar container 55 | */ 56 | showForm: function () { 57 | if (this.hasForm) { 58 | this.getForm().classList.add(this.activeClass); 59 | } 60 | }, 61 | 62 | /* hideForm: [function ()] 63 | * 64 | * This function should hide the form element inside 65 | * the toolbar container 66 | */ 67 | hideForm: function () { 68 | if (this.hasForm) { 69 | this.getForm().classList.remove(this.activeClass); 70 | } 71 | }, 72 | 73 | /************************ Helpers ************************ 74 | * The following are helpers that are either set by MediumEditor 75 | * during initialization, or are helper methods which either 76 | * route calls to the MediumEditor instance or provide common 77 | * functionality for all form extensions 78 | *********************************************************/ 79 | 80 | /* showToolbarDefaultActions: [function ()] 81 | * 82 | * Helper method which will turn back the toolbar after canceling 83 | * the customized form 84 | */ 85 | showToolbarDefaultActions: function () { 86 | var toolbar = this.base.getExtensionByName('toolbar'); 87 | if (toolbar) { 88 | toolbar.showToolbarDefaultActions(); 89 | } 90 | }, 91 | 92 | /* hideToolbarDefaultActions: [function ()] 93 | * 94 | * Helper function which will hide the default contents of the 95 | * toolbar, but leave the toolbar container in the same state 96 | * to allow a form to display its custom contents inside the toolbar 97 | */ 98 | hideToolbarDefaultActions: function () { 99 | var toolbar = this.base.getExtensionByName('toolbar'); 100 | if (toolbar) { 101 | toolbar.hideToolbarDefaultActions(); 102 | } 103 | }, 104 | 105 | /* setToolbarPosition: [function ()] 106 | * 107 | * Helper function which will update the size and position 108 | * of the toolbar based on the toolbar content and the current 109 | * position of the user's selection 110 | */ 111 | setToolbarPosition: function () { 112 | var toolbar = this.base.getExtensionByName('toolbar'); 113 | if (toolbar) { 114 | toolbar.setToolbarPosition(); 115 | } 116 | } 117 | }); 118 | 119 | MediumEditor.extensions.form = FormExtension; 120 | })(); -------------------------------------------------------------------------------- /demo/absolute-container.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | medium editor | demo 6 | 7 | 8 | 9 | 10 | 11 | 24 | 25 | 26 | Fork me on GitHub 27 |
28 | Theme: 29 | 38 |
39 |
40 |

Medium Editor

41 |
42 |

My father’s family name being Pirrip, and my Christian name Philip, my infant tongue could make of both names nothing longer or more explicit than Pip. So, I called myself Pip, and came to be called Pip.

43 |

I give Pirrip as my father’s family name, on the authority of his tombstone and my sister,—Mrs. Joe Gargery, who married the blacksmith. As I never saw my father or my mother, and never saw any likeness of either of them (for their days were long before the days of photographs), my first fancies regarding what they were like were unreasonably derived from their tombstones. The shape of the letters on my father’s, gave me an odd idea that he was a square, stout, dark man, with curly black hair. From the character and turn of the inscription, “Also Georgiana Wife of the Above,” I drew a childish conclusion that my mother was freckled and sickly. To five little stone lozenges, each about a foot and a half long, which were arranged in a neat row beside their grave, and were sacred to the memory of five little brothers of mine,—who gave up trying to get a living, exceedingly early in that universal struggle,—I am indebted for a belief I religiously entertained that they had all been born on their backs with their hands in their trousers-pockets, and had never taken them out in this state of existence.

44 |

Ours was the marsh country, down by the river, within, as the river wound, twenty miles of the sea. My first most vivid and broad impression of the identity of things seems to me to have been gained on a memorable raw afternoon towards evening. At such a time I found out for certain that this bleak place overgrown with nettles was the churchyard; and that Philip Pirrip, late of this parish, and also Georgiana wife of the above, were dead and buried; and that Alexander, Bartholomew, Abraham, Tobias, and Roger, infant children of the aforesaid, were also dead and buried; and that the dark flat wilderness beyond the churchyard, intersected with dikes and mounds and gates, with scattered cattle feeding on it, was the marshes; and that the low leaden line beyond was the river; and that the distant savage lair from which the wind was rushing was the sea; and that the small bundle of shivers growing afraid of it all and beginning to cry, was Pip.

45 |
46 |
47 |

Source

48 | 49 | 60 | 61 | 62 | -------------------------------------------------------------------------------- /spec/setup.spec.js: -------------------------------------------------------------------------------- 1 | /*global fireEvent, selectElementContentsAndFire */ 2 | 3 | describe('Setup/Destroy TestCase', function () { 4 | 'use strict'; 5 | 6 | beforeEach(function () { 7 | setupTestHelpers.call(this); 8 | this.el = this.createElement('div', 'editor', 'lore ipsum'); 9 | }); 10 | 11 | afterEach(function () { 12 | this.cleanupTest(); 13 | }); 14 | 15 | it('should toggle the isActive property', function () { 16 | var editor = this.newMediumEditor('.editor'); 17 | editor.destroy(); 18 | expect(editor.isActive).toBe(false); 19 | editor.setup(); 20 | expect(editor.isActive).toBe(true); 21 | editor.destroy(); 22 | expect(editor.isActive).toBe(false); 23 | }); 24 | 25 | describe('Setup', function () { 26 | it('should init the toolbar and editor elements', function () { 27 | var editor = this.newMediumEditor('.editor'); 28 | editor.destroy(); 29 | spyOn(MediumEditor.prototype, 'setup').and.callThrough(); 30 | editor.setup(); 31 | expect(editor.setup).toHaveBeenCalled(); 32 | expect(document.querySelector('[data-medium-editor-element]')).toBeTruthy(); 33 | expect(document.querySelector('[aria-multiline]')).toBeTruthy(); 34 | expect(document.querySelector('[medium-editor-index]')).toBeTruthy(); 35 | expect(document.querySelector('[role]')).toBeTruthy(); 36 | expect(document.querySelector('[spellcheck]')).toBeTruthy(); 37 | expect(document.querySelector('[contenteditable]')).toBeTruthy(); 38 | }); 39 | 40 | it('should know about defaults', function () { 41 | expect(MediumEditor.prototype.defaults).toBeTruthy(); 42 | }); 43 | }); 44 | 45 | describe('Destroy', function () { 46 | it('should remove mediumEditor elements from DOM', function () { 47 | var editor = this.newMediumEditor('.editor'); 48 | expect(document.querySelector('.medium-editor-toolbar')).toBeTruthy(); 49 | editor.destroy(); 50 | expect(document.querySelector('.medium-editor-toolbar')).toBeFalsy(); 51 | 52 | // ensure only initial attributes are here: the editor class 53 | expect(this.el.getAttribute('class')).toBe('editor'); 54 | expect(this.el.attributes.length).toBe(1); 55 | }); 56 | 57 | it('should remove all the added events', function () { 58 | var editor = this.newMediumEditor('.editor'); 59 | expect(editor.events.events.length).toBeGreaterThan(0); 60 | editor.destroy(); 61 | expect(editor.events.events.length).toBe(0); 62 | }); 63 | 64 | it('should abort any pending throttled event handlers', function () { 65 | var editor, triggerEvents, toolbar; 66 | 67 | editor = this.newMediumEditor('.editor', { delay: 5 }); 68 | triggerEvents = function () { 69 | fireEvent(window, 'resize'); 70 | fireEvent(document.body, 'click', { 71 | target: document.body 72 | }); 73 | fireEvent(document.body, 'blur'); 74 | }; 75 | // Store toolbar, since destroy will remove the reference from the editor 76 | toolbar = editor.getExtensionByName('toolbar'); 77 | 78 | // fire event (handler executed immediately) 79 | triggerEvents(); 80 | jasmine.clock().tick(1); 81 | 82 | // fire event again (handler delayed because of throttle) 83 | triggerEvents(); 84 | 85 | spyOn(toolbar, 'positionToolbarIfShown').and.callThrough(); // via: handleResize 86 | spyOn(editor, 'checkSelection').and.callThrough(); // via: handleBlur 87 | editor.destroy(); 88 | jasmine.clock().tick(1000); // arbitrary – must be longer than THROTTLE_INTERVAL 89 | expect(toolbar.positionToolbarIfShown).not.toHaveBeenCalled(); 90 | expect(editor.checkSelection).not.toHaveBeenCalled(); 91 | }); 92 | 93 | // regression test for https://github.com/yabwe/medium-editor/issues/197 94 | it('should not crash when destroy immediately after a mouse click', function () { 95 | var editor = this.newMediumEditor('.editor'); 96 | // selected some content and let the toolbar appear 97 | selectElementContentsAndFire(editor.elements[0], { testDelay: 501 }); 98 | 99 | // fire a mouse up somewhere else (i.e. a button which click handler could have called destroy() ) 100 | fireEvent(document.documentElement, 'mouseup'); 101 | editor.destroy(); 102 | 103 | jasmine.clock().tick(501); 104 | expect(true).toBe(true); 105 | }); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /spec/elements.spec.js: -------------------------------------------------------------------------------- 1 | describe('Elements TestCase', function () { 2 | 'use strict'; 3 | 4 | beforeEach(function () { 5 | setupTestHelpers.call(this); 6 | this.el = this.createElement('div', 'editor', 'lore ipsum'); 7 | }); 8 | 9 | afterEach(function () { 10 | this.cleanupTest(); 11 | }); 12 | 13 | describe('Initialization', function () { 14 | it('should set element contenteditable attribute to true', function () { 15 | var editor = this.newMediumEditor('.editor'); 16 | expect(editor.elements.length).toBe(1); 17 | expect(this.el.getAttribute('contenteditable')).toEqual('true'); 18 | }); 19 | 20 | it('should not set element contenteditable when disableEditing is true', function () { 21 | var editor = this.newMediumEditor('.editor', { 22 | disableEditing: true 23 | }); 24 | expect(editor.elements.length).toBe(1); 25 | expect(this.el.getAttribute('contenteditable')).toBeFalsy(); 26 | }); 27 | 28 | it('should not set element contenteditable when data-disable-editing is true', function () { 29 | this.el.setAttribute('data-disable-editing', true); 30 | var editor = this.newMediumEditor('.editor'); 31 | expect(editor.elements.length).toBe(1); 32 | expect(this.el.getAttribute('contenteditable')).toBeFalsy(); 33 | }); 34 | 35 | it('should set element data attr medium-editor-element to true and add medium-editor-element class', function () { 36 | var editor = this.newMediumEditor('.editor'); 37 | expect(editor.elements.length).toBe(1); 38 | expect(this.el.getAttribute('data-medium-editor-element')).toEqual('true'); 39 | expect(this.el.className).toBe('editor medium-editor-element'); 40 | }); 41 | 42 | it('should set element role attribute to textbox', function () { 43 | var editor = this.newMediumEditor('.editor'); 44 | expect(editor.elements.length).toBe(1); 45 | expect(this.el.getAttribute('role')).toEqual('textbox'); 46 | }); 47 | 48 | it('should set element aria multiline attribute to true', function () { 49 | var editor = this.newMediumEditor('.editor'); 50 | expect(editor.elements.length).toBe(1); 51 | expect(this.el.getAttribute('aria-multiline')).toEqual('true'); 52 | }); 53 | 54 | it('should set the data-medium-editor-editor-index attribute to be the id of the editor instance', function () { 55 | var editor = this.newMediumEditor('.editor'); 56 | expect(editor.elements[0]).toBe(this.el); 57 | expect(parseInt(this.el.getAttribute('data-medium-editor-editor-index'))).toBe(editor.id); 58 | }); 59 | }); 60 | 61 | describe('Destroy', function () { 62 | it('should remove the contenteditable attribute', function () { 63 | var editor = this.newMediumEditor('.editor'); 64 | expect(this.el.getAttribute('contenteditable')).toEqual('true'); 65 | editor.destroy(); 66 | expect(this.el.hasAttribute('contenteditable')).toBe(false); 67 | }); 68 | 69 | it('should remove the medium-editor-element attribute and class name', function () { 70 | this.el.classList.add('temp-class'); 71 | expect(this.el.className).toBe('editor temp-class'); 72 | var editor = this.newMediumEditor('.editor'); 73 | expect(this.el.getAttribute('data-medium-editor-element')).toEqual('true'); 74 | expect(this.el.className).toBe('editor temp-class medium-editor-element'); 75 | editor.destroy(); 76 | expect(this.el.hasAttribute('data-medium-editor-element')).toBe(false); 77 | expect(this.el.className).toBe('editor temp-class'); 78 | }); 79 | 80 | it('should remove the role attribute', function () { 81 | var editor = this.newMediumEditor('.editor'); 82 | expect(this.el.getAttribute('role')).toEqual('textbox'); 83 | editor.destroy(); 84 | expect(this.el.hasAttribute('role')).toBe(false); 85 | }); 86 | 87 | it('should remove the aria-multiline attribute', function () { 88 | var editor = this.newMediumEditor('.editor'); 89 | expect(this.el.getAttribute('aria-multiline')).toEqual('true'); 90 | editor.destroy(); 91 | expect(this.el.hasAttribute('aria-multiline')).toBe(false); 92 | }); 93 | 94 | it('should remove the data-medium-editor-editor-index attribute', function () { 95 | var editor = this.newMediumEditor('.editor'); 96 | expect(parseInt(this.el.getAttribute('data-medium-editor-editor-index'))).toBe(editor.id); 97 | editor.destroy(); 98 | expect(this.el.hasAttribute('data-medium-editor-editor-index')).toBe(false); 99 | }); 100 | }); 101 | }); 102 | -------------------------------------------------------------------------------- /karma.conf.js: -------------------------------------------------------------------------------- 1 | /* global module */ 2 | 3 | module.exports = function (config) { 4 | config.set({ 5 | 6 | browserStack: { 7 | apiClientEndpoint: 'https://api.browserstack.com', 8 | timeout: 600 9 | }, 10 | 11 | customLaunchers: { 12 | WIN81Chrome: { 13 | 'base': 'BrowserStack', 14 | 'os': 'Windows', 15 | 'os_version': '8.1', 16 | 'browser': 'chrome' 17 | }, 18 | WIN81Firefox: { 19 | 'base': 'BrowserStack', 20 | 'os': 'Windows', 21 | 'os_version': '8.1', 22 | 'browser': 'firefox' 23 | }, 24 | WIN81Edge: { 25 | 'base': 'BrowserStack', 26 | 'os': 'Windows', 27 | 'os_version': '8.1', 28 | 'browser': 'edge' 29 | }, 30 | WIN10Chrome: { 31 | 'base': 'BrowserStack', 32 | 'os': 'Windows', 33 | 'os_version': '10', 34 | 'browser': 'chrome' 35 | }, 36 | WIN10Firefox: { 37 | 'base': 'BrowserStack', 38 | 'os': 'Windows', 39 | 'os_version': '10', 40 | 'browser': 'firefox' 41 | }, 42 | WIN10Edge: { 43 | 'base': 'BrowserStack', 44 | 'os': 'Windows', 45 | 'os_version': '10', 46 | 'browser': 'edge' 47 | }, 48 | OSXYosemiteSafari: { 49 | 'base': 'BrowserStack', 50 | 'os': 'OS X', 51 | 'os_version': 'Yosemite', 52 | 'browser': 'safari' 53 | }, 54 | OSXElCapitanChrome: { 55 | 'base': 'BrowserStack', 56 | 'os': 'OS X', 57 | 'os_version': 'El Capitan', 58 | 'browser': 'chrome' 59 | }, 60 | OSXElCapitanSafari: { 61 | 'base': 'BrowserStack', 62 | 'os': 'OS X', 63 | 'os_version': 'El Capitan', 64 | 'browser': 'safari' 65 | }, 66 | OSXElCapitanFirefox: { 67 | 'base': 'BrowserStack', 68 | 'os': 'OS X', 69 | 'os_version': 'El Capitan', 70 | 'browser': 'firefox' 71 | } 72 | }, 73 | 74 | basePath: '', 75 | frameworks: ['jasmine'], 76 | 77 | files: [ 78 | 'dist/css/*.css', 79 | 'node_modules/lodash/lodash.js', 80 | 'src/js/polyfills.js', 81 | 'src/js/globals.js', 82 | 'src/js/util.js', 83 | 'src/js/extension.js', 84 | 'src/js/selection.js', 85 | 'src/js/events.js', 86 | 'src/js/extensions/button.js', 87 | 'src/js/defaults/buttons.js', 88 | 'src/js/extensions/form.js', 89 | 'src/js/extensions/anchor.js', 90 | 'src/js/extensions/anchor-preview.js', 91 | 'src/js/extensions/auto-link.js', 92 | 'src/js/extensions/file-dragging.js', 93 | 'src/js/extensions/keyboard-commands.js', 94 | 'src/js/extensions/fontname.js', 95 | 'src/js/extensions/fontsize.js', 96 | 'src/js/extensions/paste.js', 97 | 'src/js/extensions/placeholder.js', 98 | 'src/js/extensions/toolbar.js', 99 | 'src/js/extensions/deprecated/image-dragging.js', 100 | 'src/js/core.js', 101 | 'src/js/defaults/options.js', 102 | 'src/js/version.js', 103 | 'spec/helpers/util.js', 104 | 'spec/*.spec.js' 105 | ], 106 | 107 | exclude: [ 108 | 'src/js/extensions/deprecated/*' 109 | ], 110 | 111 | preprocessors: { 112 | }, 113 | 114 | plugins: [ 115 | 'karma-jasmine', 116 | 'karma-spec-reporter', 117 | 'karma-jasmine-html-reporter', 118 | 'karma-browserstack-launcher', 119 | 'karma-phantomjs-launcher', 120 | 'karma-firefox-launcher', 121 | 'karma-chrome-launcher', 122 | 'karma-coverage', 123 | 'karma-coveralls' 124 | ], 125 | 126 | reporters: ['coverage', 'coveralls', 'BrowserStack', 'dots', 'spec', 'kjhtml'], 127 | 128 | coverageReporter: { 129 | type: 'lcov', 130 | dir: 'coverage/' 131 | }, 132 | 133 | port: 9876, 134 | 135 | logLevel: config.LOG_ERROR, 136 | colors: true, 137 | 138 | autoWatch: false, 139 | 140 | browsers: ['WIN10Edge', 'WIN10Chrome', 'WIN10Firefox', 'OSXElCapitanChrome', 'OSXElCapitanFirefox', 'OSXYosemiteSafari'], 141 | 142 | client: { 143 | clearContext: false 144 | }, 145 | 146 | singleRun: true, 147 | 148 | concurrency: Infinity 149 | }); 150 | }; -------------------------------------------------------------------------------- /demo/js/extension-table.js: -------------------------------------------------------------------------------- 1 | var TableExtension = MediumEditor.extensions.anchor.extend({ 2 | name: 'table', 3 | action: 'createTable', 4 | aria: 'table', 5 | tagNames: ['table'], 6 | contentDefault: 'T', 7 | contentFA: '', 8 | 9 | doFormSave: function () { 10 | var columnCount = this.getColumnsInput().value, 11 | rowCount = this.getRowsInput().value, 12 | table = this.createTable(columnCount, rowCount); 13 | 14 | // Restore Medium Editor's selection before pasting HTML 15 | this.base.restoreSelection(); 16 | 17 | // Paste newly created table. 18 | this.base.pasteHTML(table.innerHTML); 19 | 20 | // Update toolbar -> hide this form 21 | this.base.checkSelection(); 22 | }, 23 | 24 | createTable: function (cols, rows) { 25 | var doc = this.base.options.ownerDocument, 26 | table = doc.createElement('table'), 27 | header = doc.createElement('thead'), 28 | headerRow = doc.createElement('tr'), 29 | body = doc.createElement('tbody'), 30 | wrap = doc.createElement('div'), 31 | h, r, c, headerCol, bodyRow, bodyCol; 32 | 33 | for (h = 1; h <= cols; h++) { 34 | headerCol = doc.createElement('th'); 35 | headerCol.innerHTML = '...'; 36 | headerRow.appendChild(headerCol); 37 | } 38 | 39 | header.appendChild(headerRow); 40 | 41 | for (r = 1; r <= rows; r++) { 42 | bodyRow = doc.createElement('tr'); 43 | for (c = 1; c <= cols; c++) { 44 | bodyCol = doc.createElement('td'); 45 | bodyCol.innerHTML = '...'; 46 | bodyRow.appendChild(bodyCol); 47 | } 48 | body.appendChild(bodyRow); 49 | } 50 | 51 | table.appendChild(header); 52 | table.appendChild(body); 53 | wrap.appendChild(table); 54 | 55 | return wrap; 56 | }, 57 | 58 | // Called when the button the toolbar is clicked 59 | // Overrides DefaultButton.handleClick 60 | handleClick: function (evt) { 61 | evt.preventDefault(); 62 | evt.stopPropagation(); 63 | 64 | if (!this.isDisplayed()) { 65 | this.showForm(); 66 | } 67 | 68 | return false; 69 | }, 70 | 71 | hideForm: function () { 72 | this.getColumnsInput().value = ''; 73 | this.getRowsInput().value = ''; 74 | this.getForm().style.display = 'none'; 75 | }, 76 | 77 | showForm: function () { 78 | var colsInput = this.getColumnsInput(), 79 | rowsInput = this.getRowsInput(); 80 | 81 | this.base.saveSelection(); 82 | this.hideToolbarDefaultActions(); 83 | this.getForm().style.display = 'block'; 84 | this.setToolbarPosition(); 85 | 86 | colsInput.focus(); 87 | }, 88 | 89 | createForm: function () { 90 | var doc = this.base.options.ownerDocument, 91 | form = doc.createElement('div'), 92 | close = doc.createElement('a'), 93 | save = doc.createElement('a'), 94 | columnInput = doc.createElement('input'), 95 | rowInput = doc.createElement('input'); 96 | 97 | form.className = 'medium-editor-toolbar-form'; 98 | form.id = 'medium-editor-toolbar-form-table-' + this.base.id; 99 | 100 | // Handle clicks on the form itself 101 | this.base.on(form, 'click', this.handleFormClick.bind(this)); 102 | 103 | // Add columns textbox 104 | columnInput.setAttribute('type', 'text'); 105 | columnInput.className = 'medium-editor-toolbar-input medium-editor-toolbar-input-columns'; 106 | columnInput.setAttribute('placeholder', 'Column Count'); 107 | form.appendChild(columnInput); 108 | 109 | // Add rows textbox 110 | rowInput.setAttribute('type', 'text'); 111 | rowInput.className = 'medium-editor-toolbar-input medium-editor-toolbar-input-rows'; 112 | rowInput.setAttribute('placeholder', 'Row Count'); 113 | form.appendChild(rowInput); 114 | 115 | // Handle typing in the textboxes 116 | this.base.on(columnInput, 'keyup', this.handleTextboxKeyup.bind(this)); 117 | this.base.on(rowInput, 'keyup', this.handleTextboxKeyup.bind(this)); 118 | 119 | // Add save buton 120 | save.setAttribute('href', '#'); 121 | save.className = 'medium-editor-toolbar-save'; 122 | save.innerHTML = this.base.options.buttonLabels === 'fontawesome' ? 123 | '' : 124 | '✓'; 125 | form.appendChild(save); 126 | 127 | // Handle save button clicks (capture) 128 | this.base.on(save, 'click', this.handleSaveClick.bind(this), true); 129 | 130 | // Add close button 131 | close.setAttribute('href', '#'); 132 | close.className = 'medium-editor-toolbar-close'; 133 | close.innerHTML = this.base.options.buttonLabels === 'fontawesome' ? 134 | '' : 135 | '×'; 136 | form.appendChild(close); 137 | 138 | // Handle close button clicks 139 | this.base.on(close, 'click', this.handleCloseClick.bind(this)); 140 | 141 | return form; 142 | }, 143 | 144 | getColumnsInput: function () { 145 | return this.getForm().querySelector('input.medium-editor-toolbar-input-columns'); 146 | }, 147 | 148 | getRowsInput: function () { 149 | return this.getForm().querySelector('input.medium-editor-toolbar-input-rows'); 150 | } 151 | }) 152 | -------------------------------------------------------------------------------- /src/js/extensions/placeholder.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | var Placeholder = MediumEditor.Extension.extend({ 5 | name: 'placeholder', 6 | 7 | /* Placeholder Options */ 8 | 9 | /* text: [string] 10 | * Text to display in the placeholder 11 | */ 12 | text: 'Type your text', 13 | 14 | /* hideOnClick: [boolean] 15 | * Should we hide the placeholder on click (true) or when user starts typing (false) 16 | */ 17 | hideOnClick: true, 18 | 19 | init: function () { 20 | MediumEditor.Extension.prototype.init.apply(this, arguments); 21 | 22 | this.initPlaceholders(); 23 | this.attachEventHandlers(); 24 | }, 25 | 26 | initPlaceholders: function () { 27 | this.getEditorElements().forEach(this.initElement, this); 28 | }, 29 | 30 | handleAddElement: function (event, editable) { 31 | this.initElement(editable); 32 | }, 33 | 34 | initElement: function (el) { 35 | if (!el.getAttribute('data-placeholder')) { 36 | el.setAttribute('data-placeholder', this.text); 37 | } 38 | this.updatePlaceholder(el); 39 | }, 40 | 41 | destroy: function () { 42 | this.getEditorElements().forEach(this.cleanupElement, this); 43 | }, 44 | 45 | handleRemoveElement: function (event, editable) { 46 | this.cleanupElement(editable); 47 | }, 48 | 49 | cleanupElement: function (el) { 50 | if (el.getAttribute('data-placeholder') === this.text) { 51 | el.removeAttribute('data-placeholder'); 52 | } 53 | }, 54 | 55 | showPlaceholder: function (el) { 56 | if (el) { 57 | // https://github.com/yabwe/medium-editor/issues/234 58 | // In firefox, styling the placeholder with an absolutely positioned 59 | // pseudo element causes the cursor to appear in a bad location 60 | // when the element is completely empty, so apply a different class to 61 | // style it with a relatively positioned pseudo element 62 | if (MediumEditor.util.isFF && el.childNodes.length === 0) { 63 | el.classList.add('medium-editor-placeholder-relative'); 64 | el.classList.remove('medium-editor-placeholder'); 65 | } else { 66 | el.classList.add('medium-editor-placeholder'); 67 | el.classList.remove('medium-editor-placeholder-relative'); 68 | } 69 | } 70 | }, 71 | 72 | hidePlaceholder: function (el) { 73 | if (el) { 74 | el.classList.remove('medium-editor-placeholder'); 75 | el.classList.remove('medium-editor-placeholder-relative'); 76 | } 77 | }, 78 | 79 | updatePlaceholder: function (el, dontShow) { 80 | // If the element has content, hide the placeholder 81 | if (el.querySelector('img, blockquote, ul, ol, table') || (el.textContent.replace(/^\s+|\s+$/g, '') !== '')) { 82 | return this.hidePlaceholder(el); 83 | } 84 | 85 | if (!dontShow) { 86 | this.showPlaceholder(el); 87 | } 88 | }, 89 | 90 | attachEventHandlers: function () { 91 | if (this.hideOnClick) { 92 | // For the 'hideOnClick' option, the placeholder should always be hidden on focus 93 | this.subscribe('focus', this.handleFocus.bind(this)); 94 | } 95 | 96 | // If the editor has content, it should always hide the placeholder 97 | this.subscribe('editableInput', this.handleInput.bind(this)); 98 | 99 | // When the editor loses focus, check if the placeholder should be visible 100 | this.subscribe('blur', this.handleBlur.bind(this)); 101 | 102 | // Need to know when elements are added/removed from the editor 103 | this.subscribe('addElement', this.handleAddElement.bind(this)); 104 | this.subscribe('removeElement', this.handleRemoveElement.bind(this)); 105 | }, 106 | 107 | handleInput: function (event, element) { 108 | // If the placeholder should be hidden on focus and the 109 | // element has focus, don't show the placeholder 110 | var dontShow = this.hideOnClick && (element === this.base.getFocusedElement()); 111 | 112 | // Editor's content has changed, check if the placeholder should be hidden 113 | this.updatePlaceholder(element, dontShow); 114 | }, 115 | 116 | handleFocus: function (event, element) { 117 | // Editor has focus, hide the placeholder 118 | this.hidePlaceholder(element); 119 | }, 120 | 121 | handleBlur: function (event, element) { 122 | // Editor has lost focus, check if the placeholder should be shown 123 | this.updatePlaceholder(element); 124 | } 125 | }); 126 | 127 | MediumEditor.extensions.placeholder = Placeholder; 128 | }()); 129 | -------------------------------------------------------------------------------- /spec/drag-and-drop.spec.js: -------------------------------------------------------------------------------- 1 | /*global fireEvent */ 2 | 3 | describe('Drag and Drop TestCase', function () { 4 | 'use strict'; 5 | 6 | beforeEach(function () { 7 | setupTestHelpers.call(this); 8 | this.el = this.createElement('div', 'editor', 'lore ipsum'); 9 | }); 10 | 11 | afterEach(function () { 12 | this.cleanupTest(); 13 | }); 14 | 15 | describe('drag', function () { 16 | it('should add medium-editor-dragover class', function () { 17 | var editor = this.newMediumEditor(this.el); 18 | fireEvent(editor.elements[0], 'dragover'); 19 | expect(editor.elements[0].className).toContain('medium-editor-dragover'); 20 | }); 21 | 22 | it('should add medium-editor-dragover class even when data is invalid', function () { 23 | var editor = this.newMediumEditor(this.el, { 24 | imageDragging: false 25 | }); 26 | fireEvent(editor.elements[0], 'dragover'); 27 | expect(editor.elements[0].className).toContain('medium-editor-dragover'); 28 | }); 29 | 30 | it('should remove medium-editor-dragover class on drag leave', function () { 31 | var editor = this.newMediumEditor(this.el); 32 | fireEvent(editor.elements[0], 'dragover'); 33 | expect(editor.elements[0].className).toContain('medium-editor-dragover'); 34 | fireEvent(editor.elements[0], 'dragleave'); 35 | expect(editor.elements[0].className).not.toContain('medium-editor-dragover'); 36 | }); 37 | }); 38 | 39 | describe('drop', function () { 40 | var eventListener; 41 | 42 | beforeEach(function () { 43 | eventListener = jasmine.createSpy(); 44 | 45 | // File API just doesn't work in IE9, so only verify this functionality if it's not IE9 46 | if (typeof FileReader === 'function') { 47 | // Spy on the FileReader and use the spy for any added event listeners 48 | spyOn(window, 'FileReader').and.returnValue({ 49 | addEventListener: eventListener, 50 | readAsDataURL: function () { 51 | } 52 | }); 53 | } 54 | // Spy to ensure that image is inserted 55 | spyOn(MediumEditor.util, 'insertHTMLCommand').and.callThrough(); 56 | }); 57 | 58 | it('should remove medium-editor-dragover class and add the image to the editor content', function () { 59 | var editor = this.newMediumEditor(this.el), 60 | editableInputListener = jasmine.createSpy(); 61 | 62 | editor.subscribe('editableInput', editableInputListener); 63 | expect(editableInputListener).not.toHaveBeenCalled(); 64 | 65 | fireEvent(editor.elements[0], 'dragover'); 66 | expect(editor.elements[0].className).toContain('medium-editor-dragover'); 67 | fireEvent(editor.elements[0], 'drop'); 68 | expect(editor.elements[0].className).not.toContain('medium-editor-dragover'); 69 | 70 | // File API just doesn't work in IE9, so only verify this functionality if it's not IE9 71 | if (typeof FileReader === 'function') { 72 | // Ensure that the load event is bound to the FileReader 73 | expect(eventListener.calls.mostRecent().args[0]).toEqual('load'); 74 | // Pass into the event handler our dummy image source 75 | eventListener.calls.mostRecent().args[1]({ 76 | target: { 77 | result: 'data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7' 78 | } 79 | }); 80 | 81 | // Expect that the image is inserted 82 | expect(MediumEditor.util.insertHTMLCommand).toHaveBeenCalled(); 83 | // Expect that the editableInput event is fired 84 | expect(editableInputListener).toHaveBeenCalled(); 85 | } 86 | }); 87 | 88 | it('should remove medium-editor-dragover class and NOT add the image to the editor content', function () { 89 | var editor = this.newMediumEditor(this.el, { imageDragging: false }), 90 | editableInputListener = jasmine.createSpy(); 91 | 92 | editor.subscribe('editableInput', editableInputListener); 93 | fireEvent(editor.elements[0], 'dragover'); 94 | expect(editor.elements[0].className).toContain('medium-editor-dragover'); 95 | fireEvent(editor.elements[0], 'drop'); 96 | expect(editor.elements[0].className).not.toContain('medium-editor-dragover'); 97 | 98 | //The following ensures that MediumEditor.Extension.insertImageFile is not called: 99 | // 1. Ensure that a load event is not bound to the FileReader 100 | expect(eventListener.calls.mostRecent()).toEqual(undefined); 101 | // 2. Expect that the image is not inserted 102 | expect(MediumEditor.util.insertHTMLCommand).not.toHaveBeenCalled(); 103 | // 3. Expect that the editableInput event is not fired 104 | expect(editableInputListener).not.toHaveBeenCalled(); 105 | }); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | medium editor | demo 6 | 7 | 8 | 9 | 10 | 11 | 12 | Fork me on GitHub 13 |
14 | Theme: 15 | 24 |
25 |
26 |

Medium Editor

27 |
28 | Absolute Container • 29 | Auto Link • 30 | Button Example • 31 | Clean Paste • 32 | Custom Toolbar • 33 | Extension Example • 34 | Multi Editor • 35 | Multi One Instance • 36 | Multi Paragraph • 37 | Nested Editable • 38 | Pass Instance • 39 | Relative Toolbar • 40 | Static Toolbar • 41 | Table Extension • 42 | Textarea 43 |
44 |
45 |

My father’s family name being Pirrip, and my Christian name Philip, my infant tongue could make of both names nothing longer or more explicit than Pip. So, I called myself Pip, and came to be called Pip.

46 |

I give Pirrip as my father’s family name, on the authority of his tombstone and my sister,—Mrs. Joe Gargery, who married the blacksmith. As I never saw my father or my mother, and never saw any likeness of either of them (for their days were long before the days of photographs), my first fancies regarding what they were like were unreasonably derived from their tombstones. The shape of the letters on my father’s, gave me an odd idea that he was a square, stout, dark man, with curly black hair. From the character and turn of the inscription, “Also Georgiana Wife of the Above,” I drew a childish conclusion that my mother was freckled and sickly. To five little stone lozenges, each about a foot and a half long, which were arranged in a neat row beside their grave, and were sacred to the memory of five little brothers of mine,—who gave up trying to get a living, exceedingly early in that universal struggle,—I am indebted for a belief I religiously entertained that they had all been born on their backs with their hands in their trousers-pockets, and had never taken them out in this state of existence.

47 |

Ours was the marsh country, down by the river, within, as the river wound, twenty miles of the sea. My first most vivid and broad impression of the identity of things seems to me to have been gained on a memorable raw afternoon towards evening. At such a time I found out for certain that this bleak place overgrown with nettles was the churchyard; and that Philip Pirrip, late of this parish, and also Georgiana wife of the above, were dead and buried; and that Alexander, Bartholomew, Abraham, Tobias, and Roger, infant children of the aforesaid, were also dead and buried; and that the dark flat wilderness beyond the churchyard, intersected with dikes and mounds and gates, with scattered cattle feeding on it, was the marshes; and that the low leaden line beyond was the river; and that the distant savage lair from which the wind was rushing was the sea; and that the small bundle of shivers growing afraid of it all and beginning to cry, was Pip.

48 |
49 |
50 |

Source

51 | 52 | 62 | 63 | 64 | -------------------------------------------------------------------------------- /src/js/extensions/WALKTHROUGH-EXTENSION.md: -------------------------------------------------------------------------------- 1 | # Walkthrough - Building an Extension 2 | 3 | 4 | 5 | 6 | - [DisableContextMenuExtension](#disablecontextmenuextension) 7 | - [1. Define the Extension](#1-define-the-extension) 8 | - [2. Attaching To Context Menu Event](#2-attaching-to-context-menu-event) 9 | - [3. Adding Functionality](#3-adding-functionality) 10 | - [4. Leveraging Custom Event Listeners](#4-leveraging-custom-event-listeners) 11 | 12 | 13 | 14 | ## DisableContextMenuExtension 15 | 16 | You can find a demo of this example in the source code via [extension-example.html](../../../demo/extension-example.html). 17 | 18 | To interact with the demo, load the page from your fork in a browser via: 19 | 20 | `file://[Medium Editor Source Root]/demo/extension-example.html` 21 | 22 | ### 1. Define the Extension 23 | 24 | As a simple example, let's create an extension that disables the context menu from appearing when the user right-clicks on the editor. 25 | 26 | Defining this extension is as simple as calling `MediumEditor.Extension.extend()` and passing in the methods/properties we want to override. 27 | 28 | ```js 29 | var DisableContextMenuExtension = MediumEditor.Extension.extend({ 30 | name: 'disable-context-menu' 31 | }); 32 | ``` 33 | 34 | We now have an extension named `'disable-context-menu'` which we can pass into MediumEditor like this: 35 | 36 | ```js 37 | var editor = new MediumEditor('.editable', { 38 | extensions: { 39 | 'disable-context-menu': new DisableContextMenuExtension() 40 | } 41 | }); 42 | ``` 43 | 44 | *** 45 | ### 2. Attaching To Context Menu Event 46 | 47 | To make the extension actually do something, we'll want to attach to the `contextmenu` event on all **elements** of the editor. We can set this up by implementing the `init()` method, which is called on every Extension during setup of MediumEditor: 48 | 49 | ```js 50 | var DisableContextMenuExtension = MediumEditor.Extension.extend({ 51 | name: 'disable-context-menu', 52 | 53 | init: function () { 54 | this.getEditorElements().forEach(function (element) { 55 | this.base.on(element, 'contextmenu', this.handleContextmenu.bind(this)); 56 | }, this); 57 | }, 58 | 59 | handleContextmenu: function (event) { } 60 | }); 61 | ``` 62 | 63 | Here, we're leveraging some of the helpers that are available to all Extensions. 64 | 65 | * We're using `this.getEditorElements()`, which is a helper function to give us an array containing all **elements** maintained by this editor. 66 | * We're using `this.base`, which is a reference to the MediumEditor instance. 67 | * We're using `this.base.on()`, which is a [method of MediumEditor](../../../API.md#ontarget-event-listener-usecapture) for attaching to DOM Events. Using this method ensures our event handlers will be detached when MediumEditor is destroyed. 68 | 69 | **NOTE:** 70 | 71 | * There are a few helper methods that allow us to make calls directly into the MediumEditor instance without having to reference `this.base`. One of them is a reference to the `on()` method, so instead of the above code we can just use `this.on(element, 'contextmenu', this.handleContextmenu.bind(this))` which is what we'll use in the rest of the example. 72 | 73 | *** 74 | ### 3. Adding Functionality 75 | 76 | So, the last piece we need is to handle the `contextmenu` event and prevent the default action: 77 | 78 | ```js 79 | var DisableContextMenuExtension = MediumEditor.Extension.extend({ 80 | name: 'disable-context-menu', 81 | 82 | init: function () { 83 | this.getEditorElements().forEach(function (element) { 84 | this.base.on(element, 'contextmenu', this.handleContextmenu.bind(this)); 85 | }, this); 86 | }, 87 | 88 | handleContextmenu: function (event) { 89 | event.preventDefault(); 90 | } 91 | }); 92 | ``` 93 | 94 | Now we have a working extension which prevents the context menu from showing up for any of the **elements**. Let's add some more functionality to allow for toggling this feature on and off. 95 | 96 | *** 97 | ### 4. Leveraging Custom Event Listeners 98 | 99 | Let's say we wanted to support toggling on/off the disable-context-menu extension, for a specific **element**, whenever the user presses ESCAPE. To do this, we'll need to add 2 pieces of functionality: 100 | 101 | 1. Listen to the `keydown` event on each **element**. For this, we can leverage the built-in [`editableKeyDown` custom event](../../../CUSTOM-EVENTS.md#editablekeydown). This allows us to use the 2nd argument of custom event listeners (the active editor **element**) to toggle on/off a `data-allow-context-menu` attribute on the **element**. 102 | 103 | 2. When the `contextmenu` event fires, we only want to prevent the context menu from appearing if the `data-allow-context-menu` attribute is not present. 104 | 105 | ```js 106 | var DisableContextMenuExtension = MediumEditor.Extension.extend({ 107 | name: 'disable-context-menu', 108 | 109 | init: function () { 110 | this.getEditorElements().forEach(function (element) { 111 | this.on(element, 'contextmenu', this.handleContextmenu.bind(this)); 112 | }, this); 113 | this.subscribe('editableKeydown', this.handleKeydown.bind(this)); 114 | }, 115 | 116 | handleContextmenu: function (event) { 117 | if (!event.currentTarget.getAttribute('data-allow-context-menu')) { 118 | event.preventDefault(); 119 | } 120 | }, 121 | 122 | handleKeydown: function (event, editable) { 123 | // If the user hits escape, toggle the data-allow-context-menu attribute 124 | if (MediumEditor.util.isKey(event, MediumEditor.util.keyCode.ESCAPE)) { 125 | if (editable.hasAttribute('data-allow-context-menu')) { 126 | editable.removeAttribute('data-allow-context-menu'); 127 | } else { 128 | editable.setAttribute('data-allow-context-menu', true); 129 | } 130 | } 131 | } 132 | }); 133 | ``` 134 | 135 | **NOTE:** 136 | 137 | For events like `keydown`, we could always use `currentTarget` and not need to use the reference to the editable element (like how we use the `currentTarget` when handling the `contextmenu` event). However, there may be times when we want to trigger one of these events manually, and this allows us to specify exactly which editable element we want to trigger the event for. It's also a handy standardization for events which are more complicated, like the custom `focus` and `blur` events. -------------------------------------------------------------------------------- /src/js/extensions/fontsize.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | var FontSizeForm = MediumEditor.extensions.form.extend({ 5 | 6 | name: 'fontsize', 7 | action: 'fontSize', 8 | aria: 'increase/decrease font size', 9 | contentDefault: '±', // ± 10 | contentFA: '', 11 | 12 | init: function () { 13 | MediumEditor.extensions.form.prototype.init.apply(this, arguments); 14 | }, 15 | 16 | // Called when the button the toolbar is clicked 17 | // Overrides ButtonExtension.handleClick 18 | handleClick: function (event) { 19 | event.preventDefault(); 20 | event.stopPropagation(); 21 | 22 | if (!this.isDisplayed()) { 23 | // Get fontsize of current selection (convert to string since IE returns this as number) 24 | var fontSize = this.document.queryCommandValue('fontSize') + ''; 25 | this.showForm(fontSize); 26 | } 27 | 28 | return false; 29 | }, 30 | 31 | // Called by medium-editor to append form to the toolbar 32 | getForm: function () { 33 | if (!this.form) { 34 | this.form = this.createForm(); 35 | } 36 | return this.form; 37 | }, 38 | 39 | // Used by medium-editor when the default toolbar is to be displayed 40 | isDisplayed: function () { 41 | return this.getForm().style.display === 'block'; 42 | }, 43 | 44 | hideForm: function () { 45 | this.getForm().style.display = 'none'; 46 | this.getInput().value = ''; 47 | }, 48 | 49 | showForm: function (fontSize) { 50 | var input = this.getInput(); 51 | 52 | this.base.saveSelection(); 53 | this.hideToolbarDefaultActions(); 54 | this.getForm().style.display = 'block'; 55 | this.setToolbarPosition(); 56 | 57 | input.value = fontSize || ''; 58 | input.focus(); 59 | }, 60 | 61 | // Called by core when tearing down medium-editor (destroy) 62 | destroy: function () { 63 | if (!this.form) { 64 | return false; 65 | } 66 | 67 | if (this.form.parentNode) { 68 | this.form.parentNode.removeChild(this.form); 69 | } 70 | 71 | delete this.form; 72 | }, 73 | 74 | // core methods 75 | 76 | doFormSave: function () { 77 | this.base.restoreSelection(); 78 | this.base.checkSelection(); 79 | }, 80 | 81 | doFormCancel: function () { 82 | this.base.restoreSelection(); 83 | this.clearFontSize(); 84 | this.base.checkSelection(); 85 | }, 86 | 87 | // form creation and event handling 88 | createForm: function () { 89 | var doc = this.document, 90 | form = doc.createElement('div'), 91 | input = doc.createElement('input'), 92 | close = doc.createElement('a'), 93 | save = doc.createElement('a'); 94 | 95 | // Font Size Form (div) 96 | form.className = 'medium-editor-toolbar-form'; 97 | form.id = 'medium-editor-toolbar-form-fontsize-' + this.getEditorId(); 98 | 99 | // Handle clicks on the form itself 100 | this.on(form, 'click', this.handleFormClick.bind(this)); 101 | 102 | // Add font size slider 103 | input.setAttribute('type', 'range'); 104 | input.setAttribute('min', '1'); 105 | input.setAttribute('max', '7'); 106 | input.className = 'medium-editor-toolbar-input'; 107 | form.appendChild(input); 108 | 109 | // Handle typing in the textbox 110 | this.on(input, 'change', this.handleSliderChange.bind(this)); 111 | 112 | // Add save buton 113 | save.setAttribute('href', '#'); 114 | save.className = 'medium-editor-toobar-save'; 115 | save.innerHTML = this.getEditorOption('buttonLabels') === 'fontawesome' ? 116 | '' : 117 | '✓'; 118 | form.appendChild(save); 119 | 120 | // Handle save button clicks (capture) 121 | this.on(save, 'click', this.handleSaveClick.bind(this), true); 122 | 123 | // Add close button 124 | close.setAttribute('href', '#'); 125 | close.className = 'medium-editor-toobar-close'; 126 | close.innerHTML = this.getEditorOption('buttonLabels') === 'fontawesome' ? 127 | '' : 128 | '×'; 129 | form.appendChild(close); 130 | 131 | // Handle close button clicks 132 | this.on(close, 'click', this.handleCloseClick.bind(this)); 133 | 134 | return form; 135 | }, 136 | 137 | getInput: function () { 138 | return this.getForm().querySelector('input.medium-editor-toolbar-input'); 139 | }, 140 | 141 | clearFontSize: function () { 142 | MediumEditor.selection.getSelectedElements(this.document).forEach(function (el) { 143 | if (el.nodeName.toLowerCase() === 'font' && el.hasAttribute('size')) { 144 | el.removeAttribute('size'); 145 | } 146 | }); 147 | }, 148 | 149 | handleSliderChange: function () { 150 | var size = this.getInput().value; 151 | if (size === '4') { 152 | this.clearFontSize(); 153 | } else { 154 | this.execAction('fontSize', { value: size }); 155 | } 156 | }, 157 | 158 | handleFormClick: function (event) { 159 | // make sure not to hide form when clicking inside the form 160 | event.stopPropagation(); 161 | }, 162 | 163 | handleSaveClick: function (event) { 164 | // Clicking Save -> create the font size 165 | event.preventDefault(); 166 | this.doFormSave(); 167 | }, 168 | 169 | handleCloseClick: function (event) { 170 | // Click Close -> close the form 171 | event.preventDefault(); 172 | this.doFormCancel(); 173 | } 174 | }); 175 | 176 | MediumEditor.extensions.fontSize = FontSizeForm; 177 | }()); -------------------------------------------------------------------------------- /src/js/extensions/fontname.js: -------------------------------------------------------------------------------- 1 | (function () { 2 | 'use strict'; 3 | 4 | var FontNameForm = MediumEditor.extensions.form.extend({ 5 | 6 | name: 'fontname', 7 | action: 'fontName', 8 | aria: 'change font name', 9 | contentDefault: '±', // ± 10 | contentFA: '', 11 | 12 | fonts: ['', 'Arial', 'Verdana', 'Times New Roman'], 13 | 14 | init: function () { 15 | MediumEditor.extensions.form.prototype.init.apply(this, arguments); 16 | }, 17 | 18 | // Called when the button the toolbar is clicked 19 | // Overrides ButtonExtension.handleClick 20 | handleClick: function (event) { 21 | event.preventDefault(); 22 | event.stopPropagation(); 23 | 24 | if (!this.isDisplayed()) { 25 | // Get FontName of current selection (convert to string since IE returns this as number) 26 | var fontName = this.document.queryCommandValue('fontName') + ''; 27 | this.showForm(fontName); 28 | } 29 | 30 | return false; 31 | }, 32 | 33 | // Called by medium-editor to append form to the toolbar 34 | getForm: function () { 35 | if (!this.form) { 36 | this.form = this.createForm(); 37 | } 38 | return this.form; 39 | }, 40 | 41 | // Used by medium-editor when the default toolbar is to be displayed 42 | isDisplayed: function () { 43 | return this.getForm().style.display === 'block'; 44 | }, 45 | 46 | hideForm: function () { 47 | this.getForm().style.display = 'none'; 48 | this.getSelect().value = ''; 49 | }, 50 | 51 | showForm: function (fontName) { 52 | var select = this.getSelect(); 53 | 54 | this.base.saveSelection(); 55 | this.hideToolbarDefaultActions(); 56 | this.getForm().style.display = 'block'; 57 | this.setToolbarPosition(); 58 | 59 | select.value = fontName || ''; 60 | select.focus(); 61 | }, 62 | 63 | // Called by core when tearing down medium-editor (destroy) 64 | destroy: function () { 65 | if (!this.form) { 66 | return false; 67 | } 68 | 69 | if (this.form.parentNode) { 70 | this.form.parentNode.removeChild(this.form); 71 | } 72 | 73 | delete this.form; 74 | }, 75 | 76 | // core methods 77 | 78 | doFormSave: function () { 79 | this.base.restoreSelection(); 80 | this.base.checkSelection(); 81 | }, 82 | 83 | doFormCancel: function () { 84 | this.base.restoreSelection(); 85 | this.clearFontName(); 86 | this.base.checkSelection(); 87 | }, 88 | 89 | // form creation and event handling 90 | createForm: function () { 91 | var doc = this.document, 92 | form = doc.createElement('div'), 93 | select = doc.createElement('select'), 94 | close = doc.createElement('a'), 95 | save = doc.createElement('a'), 96 | option; 97 | 98 | // Font Name Form (div) 99 | form.className = 'medium-editor-toolbar-form'; 100 | form.id = 'medium-editor-toolbar-form-fontname-' + this.getEditorId(); 101 | 102 | // Handle clicks on the form itself 103 | this.on(form, 'click', this.handleFormClick.bind(this)); 104 | 105 | // Add font names 106 | for (var i = 0; i' : 124 | '✓'; 125 | form.appendChild(save); 126 | 127 | // Handle save button clicks (capture) 128 | this.on(save, 'click', this.handleSaveClick.bind(this), true); 129 | 130 | // Add close button 131 | close.setAttribute('href', '#'); 132 | close.className = 'medium-editor-toobar-close'; 133 | close.innerHTML = this.getEditorOption('buttonLabels') === 'fontawesome' ? 134 | '' : 135 | '×'; 136 | form.appendChild(close); 137 | 138 | // Handle close button clicks 139 | this.on(close, 'click', this.handleCloseClick.bind(this)); 140 | 141 | return form; 142 | }, 143 | 144 | getSelect: function () { 145 | return this.getForm().querySelector('select.medium-editor-toolbar-select'); 146 | }, 147 | 148 | clearFontName: function () { 149 | MediumEditor.selection.getSelectedElements(this.document).forEach(function (el) { 150 | if (el.nodeName.toLowerCase() === 'font' && el.hasAttribute('face')) { 151 | el.removeAttribute('face'); 152 | } 153 | }); 154 | }, 155 | 156 | handleFontChange: function () { 157 | var font = this.getSelect().value; 158 | if (font === '') { 159 | this.clearFontName(); 160 | } else { 161 | this.execAction('fontName', { value: font }); 162 | } 163 | }, 164 | 165 | handleFormClick: function (event) { 166 | // make sure not to hide form when clicking inside the form 167 | event.stopPropagation(); 168 | }, 169 | 170 | handleSaveClick: function (event) { 171 | // Clicking Save -> create the font size 172 | event.preventDefault(); 173 | this.doFormSave(); 174 | }, 175 | 176 | handleCloseClick: function (event) { 177 | // Click Close -> close the form 178 | event.preventDefault(); 179 | this.doFormCancel(); 180 | } 181 | }); 182 | 183 | MediumEditor.extensions.fontName = FontNameForm; 184 | }()); 185 | --------------------------------------------------------------------------------