├── .editorconfig ├── .gitignore ├── .jscsrc ├── .jshintrc ├── .npmrc ├── .travis.yml ├── CHANGES.md ├── CONTRIBUTING.md ├── Gruntfile.js ├── LICENSE ├── README.md ├── bower.json ├── demo ├── css │ └── demo.css ├── index.html └── sass │ └── demo.scss ├── dist ├── css │ ├── medium-editor-tables.css │ └── medium-editor-tables.min.css └── js │ ├── medium-editor-tables.js │ └── medium-editor-tables.min.js ├── grunt ├── aliases.yaml ├── autoprefixer.js ├── bump.js ├── concat.js ├── coveralls.js ├── cssmin.js ├── jasmine.js ├── jscs.js ├── jshint.js ├── sass.js ├── uglify.js └── watch.js ├── package.json ├── spec ├── helpers │ └── selection.js └── util.spec.js └── src ├── js ├── builder.js ├── grid.js ├── plugin.js ├── table.js └── util.js ├── sass └── medium-editor-tables.scss └── wrappers ├── end.js └── start.js /.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 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | *.swp 3 | .DS_Store 4 | *.swo 5 | node_modules/ 6 | .sass-cache/ 7 | npm-debug.log 8 | .grunt/ 9 | _SpecRunner.html 10 | reports/ 11 | coverage/ 12 | ._* 13 | bower_components/ 14 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "curly": true, 3 | "eqeqeq": true, 4 | "immed": true, 5 | "latedef": true, 6 | "newcap": false, 7 | "noarg": true, 8 | "sub": true, 9 | "undef": true, 10 | "unused": true, 11 | "boss": true, 12 | "eqnull": true, 13 | "browser": true, 14 | "predef": ["MediumEditor"] 15 | } 16 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | save-exact=true 2 | -------------------------------------------------------------------------------- /.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 | notifications: 12 | email: false 13 | 14 | before_script: 15 | - npm install -g grunt-cli 16 | 17 | script: 18 | - npm run test:ci 19 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | 2 | 0.6.1 / 2016-10-14 3 | ================== 4 | * Table UI get distorted when Row or Column added 5 | 6 | 0.6.0 / 2016-10-13 7 | ================== 8 | * Add restrict table inside table 9 | 10 | 0.5.3 / 2016-09-27 11 | ================== 12 | * Fix detached tbody from table 13 | 14 | 0.5.2 / 2016-06-20 15 | ================== 16 | * Fix electron environment 17 | 18 | 0.5.1 / 2016-04-04 19 | ================== 20 | 21 | * Fix custom number of rows and columns 22 | * Fix grid not showing up 23 | * Fix toolbar when multiple editor's are initialized 24 | * Fix AMD-style definition 25 | 26 | 0.5.0 / 2015-11-05 27 | ================== 28 | 29 | * Add table controls: add row/column, delete row/column, delete table 30 | 31 | 32 | 0.4.1 / 2015-07-27 33 | ================== 34 | 35 | * Fix NPM package 36 | 37 | 38 | 0.4.0 / 2015-07-27 39 | ================== 40 | 41 | * Use MediumEditor new extension format 42 | 43 | 44 | 0.3.0 / 2015-06-18 45 | ================== 46 | 47 | * Upgrade to MediumEditor 5.0 48 | 49 | 50 | 0.2.1 / 2015-04-26 51 | ================== 52 | 53 | * Fix main reference on package.json 54 | 55 | 56 | 0.2.0 / 2015-03-29 57 | ================== 58 | 59 | * Add support for MediumEditor 4 60 | 61 | 62 | 0.1.0 / 2015-02-14 63 | ================== 64 | 65 | * Initial release: 66 | * Tab navigation 67 | * Native resize browsers that support it (Firefox and IE) 68 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | 1. Fork it 4 | 2. Create your feature branch (`git checkout -b my-new-feature`) 5 | 3. Test your changes to the best of your ability 6 | 4. Update the documentation to reflect your changes if they add or changes current functionality 7 | 5. Commit your changes (`git commit -am 'Added some feature'`) 8 | 6. Push to the branch (`git push origin my-new-feature`) 9 | 7. Create new Pull Request 10 | 11 | or 12 | 13 | 1. Just pull request all the things the way you want! 14 | 15 | ## Development 16 | 17 | Clone the repository and: 18 | 19 | ``` 20 | npm install 21 | grunt 22 | ``` 23 | -------------------------------------------------------------------------------- /Gruntfile.js: -------------------------------------------------------------------------------- 1 | global.jsSourceFiles = [ 2 | 'src/js/util.js', 3 | 'src/js/grid.js', 4 | 'src/js/builder.js', 5 | 'src/js/table.js', 6 | 'src/js/plugin.js' 7 | ]; 8 | 9 | module.exports = function (grunt) { 10 | require('load-grunt-config')(grunt, { 11 | loadGruntTasks: { 12 | pattern: [ 13 | 'grunt-*', 14 | '!grunt-template-jasmine-istanbul' 15 | ] 16 | } 17 | }); 18 | require('time-grunt')(grunt); 19 | }; 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright Davi Ferreira, http://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-tables 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MediumEditor Tables 2 | 3 | [![NPM info](https://nodei.co/npm/medium-editor-tables.png?downloads=true)](https://nodei.co/npm/medium-editor-tables.png?downloads=true) 4 | 5 | [![Travis build status](https://travis-ci.org/yabwe/medium-editor-tables.png?branch=master)](https://travis-ci.org/yabwe/medium-editor-tables) 6 | [![dependencies](https://david-dm.org/yabwe/medium-editor-tables.png)](https://david-dm.org/yabwe/medium-editor-tables) 7 | [![devDependency Status](https://david-dm.org/yabwe/medium-editor-tables/dev-status.png)](https://david-dm.org/yabwe/medium-editor-tables#info=devDependencies) 8 | [![Coverage Status](https://coveralls.io/repos/yabwe/medium-editor-tables/badge.svg)](https://coveralls.io/r/yabwe/medium-editor-tables) 9 | 10 | MediumEditor Tables is an extension to add a table button/behavior to [MediumEditor](https://github.com/yabwe/medium-editor). 11 | 12 | Demo: [http://yabwe.github.io/medium-editor-tables/](http://yabwe.github.io/medium-editor-tables/) 13 | 14 | -- 15 | 16 | ![meditor-tables mp4](https://cloud.githubusercontent.com/assets/38787/6430614/8ff048c0-c011-11e4-8e2c-09ff773d2f78.gif) 17 | 18 | -- 19 | 20 | ## Usage 21 | 22 | You can install manually or either by using npm or bower: 23 | 24 | ``` 25 | npm install medium-editor-tables 26 | ``` 27 | 28 | or 29 | 30 | ``` 31 | bower install medium-editor-tables 32 | ``` 33 | 34 | On your app, link the style and the script and initialize MediumEditor with the table extension: 35 | 36 | ```html 37 | 38 | 39 | 40 | ... 41 | 42 | 43 | 44 | ... 45 | 46 | 47 |
48 | 49 | 50 | 51 | 52 | 67 | 68 | 69 | ``` 70 | 71 | ## Initialization options 72 | 73 | * __rows__: maximum number of rows. Default: 10. 74 | * __columns__: maximum number of columns. Default: 10. 75 | 76 | ### Example 77 | 78 | ```javascript 79 | ... 80 | extensions: { 81 | 'table': new MediumEditorTable({ 82 | rows: 40, 83 | columns: 40 84 | }) 85 | } 86 | ... 87 | ``` 88 | 89 | ## Demo 90 | 91 | Clone the repository and: 92 | 93 | ``` 94 | bower install 95 | open demo/index.html 96 | ``` 97 | 98 | ## License 99 | 100 | MIT: https://github.com/yabwe/medium-editor-tables/blob/master/LICENSE 101 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "medium-editor-tables", 3 | "homepage": "http://yabwe.github.io/medium-editor/", 4 | "authors": [ 5 | "Davi Ferreira " 6 | ], 7 | "description": "MediumEditor extension to allow tables.", 8 | "main": [ 9 | "dist/js/medium-editor-tables.js", 10 | "dist/css/medium-editor-tables.css" 11 | ], 12 | "keywords": [ 13 | "wysiwyg", 14 | "medium", 15 | "rich-text", 16 | "editor", 17 | "table", 18 | "tables" 19 | ], 20 | "license": "MIT", 21 | "ignore": [ 22 | "**/.*", 23 | "node_modules", 24 | "bower_components", 25 | "spec", 26 | "coverage", 27 | "reports", 28 | "_SpecRunner.html", 29 | "Gruntfile.js", 30 | "demo", 31 | "package.json", 32 | "src/js", 33 | "README.md", 34 | "CHANGES.md" 35 | ], 36 | "dependencies": { 37 | "medium-editor": "^5.5.1", 38 | "normalize.css": "^3.0.3" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /demo/css/demo.css: -------------------------------------------------------------------------------- 1 | ::-moz-selection { 2 | background: #a0ced9; 3 | color: #fff; } 4 | 5 | ::selection { 6 | background: #a0ced9; 7 | color: #fff; } 8 | 9 | body { 10 | background-color: #fafafa; 11 | color: #333; 12 | font-family: 'Roboto', sans-serif; 13 | font-size: 16px; 14 | line-height: 1.6; } 15 | 16 | p:first-child { 17 | margin-top: 0; } 18 | 19 | p:last-child { 20 | margin-bottom: 0; } 21 | 22 | h1 { 23 | margin-bottom: 60px; 24 | text-align: center; } 25 | 26 | .container { 27 | margin: 60px auto; 28 | width: 600px; } 29 | 30 | .editable { 31 | background-color: #fff; 32 | border: 1px solid #abf4b4; 33 | padding: 30px; } 34 | .editable:focus { 35 | outline: none; } 36 | -------------------------------------------------------------------------------- /demo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Medium Editor Tables 8 | 9 | 10 | 11 | 12 | 14 | 16 | 18 | 20 | 22 | 24 | 25 | 26 | 27 | Fork me on GitHub 28 |
29 |

MediumEditor Tables

30 |
31 |

32 | He always thought of the sea as 'la mar' which is what people call 33 | her in Spanish when they love her. Sometimes those who love her say 34 | bad things of her but they are always said as though she were a woman. 35 | Some of the younger fishermen, those who used buoys as floats for their 36 | lines and had motorboats, bought when the shark livers had brought much 37 | money, spoke of her as 'el mar' which is masculine.They spoke of her as 38 | a contestant or a place or even an enemy. But the old man always thought 39 | of her as feminine and as something that gave or withheld great favours, 40 | and if she did wild or wicked things it was because she could not help 41 | them. The moon affects her as it does a woman, he thought. 42 |

43 |
44 |
45 | 46 | 49 | 51 | 52 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /demo/sass/demo.scss: -------------------------------------------------------------------------------- 1 | ::selection { 2 | background: #a0ced9; 3 | color: #fff; 4 | } 5 | 6 | body { 7 | background-color: #fafafa; 8 | color: #333; 9 | font-family: 'Roboto', sans-serif; 10 | font-size: 16px; 11 | line-height: 1.6; 12 | } 13 | 14 | p { 15 | &:first-child { 16 | margin-top: 0; 17 | } 18 | 19 | &:last-child { 20 | margin-bottom: 0; 21 | } 22 | } 23 | 24 | h1 { 25 | margin-bottom: 60px; 26 | text-align: center; 27 | } 28 | 29 | .container { 30 | margin: 60px auto; 31 | width: 600px; 32 | } 33 | 34 | .editable { 35 | background-color: #fff; 36 | border: 1px solid #abf4b4; 37 | padding: 30px; 38 | 39 | &:focus { 40 | outline: none; 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /dist/css/medium-editor-tables.css: -------------------------------------------------------------------------------- 1 | .medium-editor-table-builder { 2 | display: none; 3 | position: absolute; 4 | left: 0; 5 | top: 101%; } 6 | .medium-editor-table-builder * { 7 | box-sizing: border-box; } 8 | 9 | .medium-editor-table-builder-grid { 10 | border: 1px solid #000; 11 | border-radius: 3px; 12 | overflow: hidden; } 13 | 14 | .medium-editor-table-builder-cell { 15 | background-color: #333; 16 | border: 1px solid #000; 17 | display: block; 18 | float: left; 19 | height: 16px; 20 | margin: 0; 21 | width: 16px; } 22 | .medium-editor-table-builder-cell.active { 23 | background-color: #ccc; } 24 | 25 | .medium-editor-table-builder-cell:hover { 26 | background-color: #ccc; } 27 | 28 | .medium-editor-table { 29 | border-collapse: collapse; 30 | resize: both; 31 | table-layout: fixed; } 32 | 33 | .medium-editor-table, 34 | .medium-editor-table td { 35 | border: 1px dashed #e3e3e3; } 36 | 37 | .medium-editor-table-builder-toolbar { 38 | display: block; 39 | min-width: 162px; 40 | background-color: #333; 41 | font-size: 0.8em; 42 | color: white; } 43 | .medium-editor-table-builder-toolbar span { 44 | width: 45px; 45 | display: block; 46 | float: left; 47 | margin-left: 5px; } 48 | .medium-editor-table-builder-toolbar button { 49 | margin: 0 3px; 50 | background-color: #333; 51 | border: 0; 52 | width: 30px; 53 | cursor: pointer; } 54 | .medium-editor-table-builder-toolbar button i { 55 | color: white; } 56 | -------------------------------------------------------------------------------- /dist/css/medium-editor-tables.min.css: -------------------------------------------------------------------------------- 1 | .medium-editor-table-builder{display:none;position:absolute;left:0;top:101%}.medium-editor-table-builder *{box-sizing:border-box}.medium-editor-table-builder-grid{border:1px solid #000;border-radius:3px;overflow:hidden}.medium-editor-table-builder-cell{background-color:#333;border:1px solid #000;display:block;float:left;height:16px;margin:0;width:16px}.medium-editor-table-builder-cell.active,.medium-editor-table-builder-cell:hover{background-color:#ccc}.medium-editor-table{border-collapse:collapse;resize:both;table-layout:fixed}.medium-editor-table,.medium-editor-table td{border:1px dashed #e3e3e3}.medium-editor-table-builder-toolbar{display:block;min-width:162px;background-color:#333;font-size:.8em;color:#fff}.medium-editor-table-builder-toolbar span{width:45px;display:block;float:left;margin-left:5px}.medium-editor-table-builder-toolbar button{margin:0 3px;background-color:#333;border:0;width:30px;cursor:pointer}.medium-editor-table-builder-toolbar button i{color:#fff} -------------------------------------------------------------------------------- /dist/js/medium-editor-tables.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { 2 | 'use strict'; 3 | var isElectron = typeof module === 'object' && 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.MediumEditorTable = factory; 12 | } 13 | }(this, function (MediumEditor) { 14 | 15 | 'use strict'; 16 | 17 | function extend(dest, source) { 18 | var prop; 19 | dest = dest || {}; 20 | for (prop in source) { 21 | if (source.hasOwnProperty(prop) && !dest.hasOwnProperty(prop)) { 22 | dest[prop] = source[prop]; 23 | } 24 | } 25 | return dest; 26 | } 27 | 28 | function getSelectionText(doc) { 29 | if (doc.getSelection) { 30 | return doc.getSelection().toString(); 31 | } 32 | if (doc.selection && doc.selection.type !== 'Control') { 33 | return doc.selection.createRange().text; 34 | } 35 | return ''; 36 | } 37 | 38 | function getSelectionStart(doc) { 39 | var node = doc.getSelection().anchorNode, 40 | startNode = (node && node.nodeType === 3 ? node.parentNode : node); 41 | 42 | return startNode; 43 | } 44 | 45 | function placeCaretAtNode(doc, node, before) { 46 | if (doc.getSelection !== undefined && node) { 47 | var range = doc.createRange(), 48 | selection = doc.getSelection(); 49 | 50 | if (before) { 51 | range.setStartBefore(node); 52 | } else { 53 | range.setStartAfter(node); 54 | } 55 | 56 | range.collapse(true); 57 | 58 | selection.removeAllRanges(); 59 | selection.addRange(range); 60 | } 61 | } 62 | 63 | function isInsideElementOfTag(node, tag) { 64 | if (!node) { 65 | return false; 66 | } 67 | 68 | var parentNode = node.parentNode, 69 | tagName = parentNode.tagName.toLowerCase(); 70 | 71 | while (tagName !== 'body') { 72 | if (tagName === tag) { 73 | return true; 74 | } 75 | parentNode = parentNode.parentNode; 76 | 77 | if (parentNode && parentNode.tagName) { 78 | tagName = parentNode.tagName.toLowerCase(); 79 | } else { 80 | return false; 81 | } 82 | } 83 | 84 | return false; 85 | } 86 | 87 | function getParentOf(el, tagTarget) { 88 | var tagName = el && el.tagName ? el.tagName.toLowerCase() : false; 89 | 90 | if (!tagName) { 91 | return false; 92 | } 93 | while (tagName && tagName !== 'body') { 94 | if (tagName === tagTarget) { 95 | return el; 96 | } 97 | el = el.parentNode; 98 | tagName = el && el.tagName ? el.tagName.toLowerCase() : false; 99 | } 100 | } 101 | 102 | function Grid(el, callback, rows, columns) { 103 | return this.init(el, callback, rows, columns); 104 | } 105 | 106 | Grid.prototype = { 107 | init: function (el, callback, rows, columns) { 108 | this._root = el; 109 | this._callback = callback; 110 | this.rows = rows; 111 | this.columns = columns; 112 | return this._render(); 113 | }, 114 | 115 | setCurrentCell: function (cell) { 116 | this._currentCell = cell; 117 | }, 118 | 119 | markCells: function () { 120 | [].forEach.call(this._cellsElements, function (el) { 121 | var cell = { 122 | column: parseInt(el.dataset.column, 10), 123 | row: parseInt(el.dataset.row, 10) 124 | }, 125 | active = this._currentCell && 126 | cell.row <= this._currentCell.row && 127 | cell.column <= this._currentCell.column; 128 | 129 | if (active === true) { 130 | el.classList.add('active'); 131 | } else { 132 | el.classList.remove('active'); 133 | } 134 | }.bind(this)); 135 | }, 136 | 137 | _generateCells: function () { 138 | var row = -1; 139 | 140 | this._cells = []; 141 | 142 | for (var i = 0; i < this.rows * this.columns; i++) { 143 | var column = i % this.columns; 144 | 145 | if (column === 0) { 146 | row++; 147 | } 148 | 149 | this._cells.push({ 150 | column: column, 151 | row: row, 152 | active: false 153 | }); 154 | } 155 | }, 156 | 157 | _html: function () { 158 | var width = this.columns * COLUMN_WIDTH + BORDER_WIDTH * 2, 159 | height = this.rows * COLUMN_WIDTH + BORDER_WIDTH * 2, 160 | html = '
'; 161 | html += this._cellsHTML(); 162 | html += '
'; 163 | return html; 164 | }, 165 | 166 | _cellsHTML: function () { 167 | var html = ''; 168 | this._generateCells(); 169 | this._cells.map(function (cell) { 170 | html += ''; 174 | html += ''; 175 | }); 176 | return html; 177 | }, 178 | 179 | _render: function () { 180 | this._root.innerHTML = this._html(); 181 | this._cellsElements = this._root.querySelectorAll('a'); 182 | this._bindEvents(); 183 | }, 184 | 185 | _bindEvents: function () { 186 | [].forEach.call(this._cellsElements, function (el) { 187 | this._onMouseEnter(el); 188 | this._onClick(el); 189 | }.bind(this)); 190 | }, 191 | 192 | _onMouseEnter: function (el) { 193 | var self = this, 194 | timer; 195 | 196 | el.addEventListener('mouseenter', function () { 197 | clearTimeout(timer); 198 | 199 | var dataset = this.dataset; 200 | 201 | timer = setTimeout(function () { 202 | self._currentCell = { 203 | column: parseInt(dataset.column, 10), 204 | row: parseInt(dataset.row, 10) 205 | }; 206 | self.markCells(); 207 | }, 50); 208 | }); 209 | }, 210 | 211 | _onClick: function (el) { 212 | var self = this; 213 | el.addEventListener('click', function (e) { 214 | e.preventDefault(); 215 | self._callback(this.dataset.row, this.dataset.column); 216 | }); 217 | } 218 | }; 219 | 220 | function Builder(options) { 221 | return this.init(options); 222 | } 223 | 224 | Builder.prototype = { 225 | init: function (options) { 226 | this.options = options; 227 | this._doc = options.ownerDocument || document; 228 | this._root = this._doc.createElement('div'); 229 | this._root.className = 'medium-editor-table-builder'; 230 | this.grid = new Grid( 231 | this._root, 232 | this.options.onClick, 233 | this.options.rows, 234 | this.options.columns 235 | ); 236 | 237 | this._range = null; 238 | this._toolbar = this._doc.createElement('div'); 239 | this._toolbar.className = 'medium-editor-table-builder-toolbar'; 240 | 241 | var spanRow = this._doc.createElement('span'); 242 | spanRow.innerHTML = 'Row:'; 243 | this._toolbar.appendChild(spanRow); 244 | var addRowBefore = this._doc.createElement('button'); 245 | addRowBefore.title = 'Add row before'; 246 | addRowBefore.innerHTML = ''; 247 | addRowBefore.onclick = this.addRow.bind(this, true); 248 | this._toolbar.appendChild(addRowBefore); 249 | 250 | var addRowAfter = this._doc.createElement('button'); 251 | addRowAfter.title = 'Add row after'; 252 | addRowAfter.innerHTML = ''; 253 | addRowAfter.onclick = this.addRow.bind(this, false); 254 | this._toolbar.appendChild(addRowAfter); 255 | 256 | var remRow = this._doc.createElement('button'); 257 | remRow.title = 'Remove row'; 258 | remRow.innerHTML = ''; 259 | remRow.onclick = this.removeRow.bind(this); 260 | this._toolbar.appendChild(remRow); 261 | 262 | var spanCol = this._doc.createElement('span'); 263 | spanCol.innerHTML = 'Column:'; 264 | this._toolbar.appendChild(spanCol); 265 | var addColumnBefore = this._doc.createElement('button'); 266 | addColumnBefore.title = 'Add column before'; 267 | addColumnBefore.innerHTML = ''; 268 | addColumnBefore.onclick = this.addColumn.bind(this, true); 269 | this._toolbar.appendChild(addColumnBefore); 270 | 271 | var addColumnAfter = this._doc.createElement('button'); 272 | addColumnAfter.title = 'Add column after'; 273 | addColumnAfter.innerHTML = ''; 274 | addColumnAfter.onclick = this.addColumn.bind(this, false); 275 | this._toolbar.appendChild(addColumnAfter); 276 | 277 | var remColumn = this._doc.createElement('button'); 278 | remColumn.title = 'Remove column'; 279 | remColumn.innerHTML = ''; 280 | remColumn.onclick = this.removeColumn.bind(this); 281 | this._toolbar.appendChild(remColumn); 282 | 283 | var remTable = this._doc.createElement('button'); 284 | remTable.title = 'Remove table'; 285 | remTable.innerHTML = ''; 286 | remTable.onclick = this.removeTable.bind(this); 287 | this._toolbar.appendChild(remTable); 288 | 289 | var grid = this._root.childNodes[0]; 290 | this._root.insertBefore(this._toolbar, grid); 291 | }, 292 | 293 | getElement: function () { 294 | return this._root; 295 | }, 296 | 297 | hide: function () { 298 | this._root.style.display = ''; 299 | this.grid.setCurrentCell({ column: -1, row: -1 }); 300 | this.grid.markCells(); 301 | }, 302 | 303 | show: function (left) { 304 | this._root.style.display = 'block'; 305 | this._root.style.left = left + 'px'; 306 | }, 307 | 308 | setEditor: function (range, restrictNestedTable) { 309 | this._range = range; 310 | this._toolbar.style.display = 'block'; 311 | if (restrictNestedTable) { 312 | var elements = this._doc.getElementsByClassName('medium-editor-table-builder-grid'); 313 | elements[0].style.display = 'none'; 314 | } 315 | }, 316 | 317 | setBuilder: function () { 318 | this._range = null; 319 | this._toolbar.style.display = 'none'; 320 | var elements = this._doc.getElementsByClassName('medium-editor-table-builder-grid'); 321 | elements[0].style.display = 'block'; 322 | for (var i = 0; i < elements.length; i++) { 323 | elements[i].style.height = (COLUMN_WIDTH * this.rows + BORDER_WIDTH * 2) + 'px'; 324 | elements[i].style.width = (COLUMN_WIDTH * this.columns + BORDER_WIDTH * 2) + 'px'; 325 | } 326 | }, 327 | 328 | getParentType: function (el, targetNode) { 329 | var nodeName = el && el.nodeName ? el.nodeName.toLowerCase() : false; 330 | if (!nodeName) { 331 | return false; 332 | } 333 | while (nodeName && nodeName !== 'body') { 334 | if (nodeName === targetNode) { 335 | return el; 336 | } 337 | el = el.parentNode; 338 | nodeName = el && el.nodeName ? el.nodeName.toLowerCase() : false; 339 | } 340 | }, 341 | 342 | addRow: function (before, e) { 343 | e.preventDefault(); 344 | e.stopPropagation(); 345 | var tbody = this.getParentType(this._range, 'tbody'), 346 | selectedTR = this.getParentType(this._range, 'tr'), 347 | tr = this._doc.createElement('tr'), 348 | td; 349 | for (var i = 0; i < selectedTR.childNodes.length; i++) { 350 | td = this._doc.createElement('td'); 351 | td.appendChild(this._doc.createElement('br')); 352 | tr.appendChild(td); 353 | } 354 | if (before !== true && selectedTR.nextSibling) { 355 | tbody.insertBefore(tr, selectedTR.nextSibling); 356 | } else if (before === true) { 357 | tbody.insertBefore(tr, selectedTR); 358 | } else { 359 | tbody.appendChild(tr); 360 | } 361 | this.options.onClick(0, 0); 362 | }, 363 | 364 | removeRow: function (e) { 365 | e.preventDefault(); 366 | e.stopPropagation(); 367 | var tbody = this.getParentType(this._range, 'tbody'), 368 | selectedTR = this.getParentType(this._range, 'tr'); 369 | tbody.removeChild(selectedTR); 370 | this.options.onClick(0, 0); 371 | }, 372 | 373 | addColumn: function (before, e) { 374 | e.preventDefault(); 375 | e.stopPropagation(); 376 | var selectedTR = this.getParentType(this._range, 'tr'), 377 | selectedTD = this.getParentType(this._range, 'td'), 378 | cell = Array.prototype.indexOf.call(selectedTR.childNodes, selectedTD), 379 | tbody = this.getParentType(this._range, 'tbody'), 380 | td; 381 | 382 | for (var i = 0; i < tbody.childNodes.length; i++) { 383 | td = this._doc.createElement('td'); 384 | td.appendChild(this._doc.createElement('br')); 385 | if (before === true) { 386 | tbody.childNodes[i].insertBefore(td, tbody.childNodes[i].childNodes[cell]); 387 | } else if (tbody.childNodes[i].childNodes[cell].nextSibling) { 388 | tbody.childNodes[i].insertBefore(td, tbody.childNodes[i].childNodes[cell].nextSibling); 389 | } else { 390 | tbody.childNodes[i].appendChild(td); 391 | } 392 | } 393 | 394 | this.options.onClick(0, 0); 395 | }, 396 | 397 | removeColumn: function (e) { 398 | e.preventDefault(); 399 | e.stopPropagation(); 400 | var selectedTR = this.getParentType(this._range, 'tr'), 401 | selectedTD = this.getParentType(this._range, 'td'), 402 | cell = Array.prototype.indexOf.call(selectedTR.childNodes, selectedTD), 403 | tbody = this.getParentType(this._range, 'tbody'), 404 | rows = tbody.childNodes.length; 405 | 406 | for (var i = 0; i < rows; i++) { 407 | tbody.childNodes[i].removeChild(tbody.childNodes[i].childNodes[cell]); 408 | } 409 | this.options.onClick(0, 0); 410 | }, 411 | 412 | removeTable: function (e) { 413 | e.preventDefault(); 414 | e.stopPropagation(); 415 | var selectedTR = this.getParentType(this._range, 'tr'), 416 | selectedTD = this.getParentType(this._range, 'td'), 417 | cell = Array.prototype.indexOf.call(selectedTR.childNodes, selectedTD), 418 | table = this.getParentType(this._range, 'table'); 419 | 420 | table.parentNode.removeChild(table); 421 | this.options.onClick(0, 0); 422 | } 423 | }; 424 | 425 | function Table(editor) { 426 | return this.init(editor); 427 | } 428 | 429 | var TAB_KEY_CODE = 9; 430 | 431 | Table.prototype = { 432 | init: function (editor) { 433 | this._editor = editor; 434 | this._doc = this._editor.options.ownerDocument; 435 | this._bindTabBehavior(); 436 | }, 437 | 438 | insert: function (rows, cols) { 439 | var html = this._html(rows, cols); 440 | 441 | this._editor.pasteHTML( 442 | '' + 444 | '' + 445 | html + 446 | '' + 447 | '
', { 448 | cleanAttrs: [], 449 | cleanTags: [] 450 | } 451 | ); 452 | 453 | var table = this._doc.getElementById('medium-editor-table'), 454 | tbody = this._doc.getElementById('medium-editor-table-tbody'); 455 | if (0 === $(table).find('#medium-editor-table-tbody').length) { 456 | //Edge case, where tbody gets appended outside table tag 457 | $(tbody).detach().appendTo(table); 458 | } 459 | tbody.removeAttribute('id'); 460 | table.removeAttribute('id'); 461 | placeCaretAtNode(this._doc, table.querySelector('td'), true); 462 | 463 | this._editor.checkSelection(); 464 | }, 465 | 466 | _html: function (rows, cols) { 467 | var html = '', 468 | x, y, 469 | text = getSelectionText(this._doc); 470 | 471 | for (x = 0; x <= rows; x++) { 472 | html += ''; 473 | for (y = 0; y <= cols; y++) { 474 | html += '' + (x === 0 && y === 0 ? text : '
') + ''; 475 | } 476 | html += ''; 477 | } 478 | return html; 479 | }, 480 | 481 | _bindTabBehavior: function () { 482 | var self = this; 483 | [].forEach.call(this._editor.elements, function (el) { 484 | el.addEventListener('keydown', function (e) { 485 | self._onKeyDown(e); 486 | }); 487 | }); 488 | }, 489 | 490 | _onKeyDown: function (e) { 491 | var el = getSelectionStart(this._doc), 492 | table; 493 | 494 | if (e.which === TAB_KEY_CODE && isInsideElementOfTag(el, 'table')) { 495 | e.preventDefault(); 496 | e.stopPropagation(); 497 | table = this._getTableElements(el); 498 | if (e.shiftKey) { 499 | this._tabBackwards(el.previousSibling, table.row); 500 | } else { 501 | if (this._isLastCell(el, table.row, table.root)) { 502 | this._insertRow(getParentOf(el, 'tbody'), table.row.cells.length); 503 | } 504 | placeCaretAtNode(this._doc, el); 505 | } 506 | } 507 | }, 508 | 509 | _getTableElements: function (el) { 510 | return { 511 | cell: getParentOf(el, 'td'), 512 | row: getParentOf(el, 'tr'), 513 | root: getParentOf(el, 'table') 514 | }; 515 | }, 516 | 517 | _tabBackwards: function (el, row) { 518 | el = el || this._getPreviousRowLastCell(row); 519 | placeCaretAtNode(this._doc, el, true); 520 | }, 521 | 522 | _insertRow: function (tbody, cols) { 523 | var tr = document.createElement('tr'), 524 | html = '', 525 | i; 526 | 527 | for (i = 0; i < cols; i += 1) { 528 | html += '
'; 529 | } 530 | tr.innerHTML = html; 531 | tbody.appendChild(tr); 532 | }, 533 | 534 | _isLastCell: function (el, row, table) { 535 | return ( 536 | (row.cells.length - 1) === el.cellIndex && 537 | (table.rows.length - 1) === row.rowIndex 538 | ); 539 | }, 540 | 541 | _getPreviousRowLastCell: function (row) { 542 | row = row.previousSibling; 543 | if (row) { 544 | return row.cells[row.cells.length - 1]; 545 | } 546 | } 547 | }; 548 | 549 | var COLUMN_WIDTH = 16, 550 | BORDER_WIDTH = 1, 551 | MediumEditorTable; 552 | 553 | MediumEditorTable = MediumEditor.extensions.form.extend({ 554 | name: 'table', 555 | 556 | aria: 'create table', 557 | action: 'table', 558 | contentDefault: 'TBL', 559 | contentFA: '', 560 | 561 | handleClick: function (event) { 562 | event.preventDefault(); 563 | event.stopPropagation(); 564 | 565 | this[this.isActive() === true ? 'hide' : 'show'](); 566 | }, 567 | 568 | hide: function () { 569 | this.setInactive(); 570 | this.builder.hide(); 571 | }, 572 | 573 | show: function () { 574 | this.setActive(); 575 | 576 | var range = MediumEditor.selection.getSelectionRange(this.document); 577 | if (range.startContainer.nodeName.toLowerCase() === 'td' || 578 | range.endContainer.nodeName.toLowerCase() === 'td' || 579 | MediumEditor.util.getClosestTag(MediumEditor.selection.getSelectedParentElement(range), 'td')) { 580 | this.builder.setEditor(MediumEditor.selection.getSelectedParentElement(range), this.restrictNestedTable); 581 | } else { 582 | this.builder.setBuilder(); 583 | } 584 | this.builder.show(this.button.offsetLeft); 585 | }, 586 | 587 | getForm: function () { 588 | if (!this.builder) { 589 | this.builder = new Builder({ 590 | onClick: function (rows, columns) { 591 | if (rows > 0 || columns > 0) { 592 | this.table.insert(rows, columns); 593 | } 594 | this.hide(); 595 | }.bind(this), 596 | ownerDocument: this.document, 597 | rows: this.rows || 10, 598 | columns: this.columns || 10 599 | }); 600 | 601 | this.table = new Table(this.base); 602 | } 603 | 604 | return this.builder.getElement(); 605 | } 606 | }); 607 | 608 | return MediumEditorTable; 609 | }(typeof require === 'function' ? require('medium-editor') : MediumEditor))); 610 | -------------------------------------------------------------------------------- /dist/js/medium-editor-tables.min.js: -------------------------------------------------------------------------------- 1 | !function(a,b){"use strict";var c="object"==typeof module&&process&&process.versions&&process.versions.electron;c||"object"!=typeof module?"function"==typeof define&&define.amd?define(function(){return b}):a.MediumEditorTable=b:module.exports=b}(this,function(){"use strict";function a(a){return a.getSelection?a.getSelection().toString():a.selection&&"Control"!==a.selection.type?a.selection.createRange().text:""}function b(a){var b=a.getSelection().anchorNode,c=b&&3===b.nodeType?b.parentNode:b;return c}function c(a,b,c){if(void 0!==a.getSelection&&b){var d=a.createRange(),e=a.getSelection();c?d.setStartBefore(b):d.setStartAfter(b),d.collapse(!0),e.removeAllRanges(),e.addRange(d)}}function d(a,b){if(!a)return!1;for(var c=a.parentNode,d=c.tagName.toLowerCase();"body"!==d;){if(d===b)return!0;if(c=c.parentNode,!c||!c.tagName)return!1;d=c.tagName.toLowerCase()}return!1}function e(a,b){var c=!(!a||!a.tagName)&&a.tagName.toLowerCase();if(!c)return!1;for(;c&&"body"!==c;){if(c===b)return a;a=a.parentNode,c=!(!a||!a.tagName)&&a.tagName.toLowerCase()}}function f(a,b,c,d){return this.init(a,b,c,d)}function g(a){return this.init(a)}function h(a){return this.init(a)}f.prototype={init:function(a,b,c,d){return this._root=a,this._callback=b,this.rows=c,this.columns=d,this._render()},setCurrentCell:function(a){this._currentCell=a},markCells:function(){[].forEach.call(this._cellsElements,function(a){var b={column:parseInt(a.dataset.column,10),row:parseInt(a.dataset.row,10)},c=this._currentCell&&b.row<=this._currentCell.row&&b.column<=this._currentCell.column;c===!0?a.classList.add("active"):a.classList.remove("active")}.bind(this))},_generateCells:function(){var a=-1;this._cells=[];for(var b=0;b';return c+=this._cellsHTML(),c+=""},_cellsHTML:function(){var a="";return this._generateCells(),this._cells.map(function(b){a+='',a+=""}),a},_render:function(){this._root.innerHTML=this._html(),this._cellsElements=this._root.querySelectorAll("a"),this._bindEvents()},_bindEvents:function(){[].forEach.call(this._cellsElements,function(a){this._onMouseEnter(a),this._onClick(a)}.bind(this))},_onMouseEnter:function(a){var b,c=this;a.addEventListener("mouseenter",function(){clearTimeout(b);var a=this.dataset;b=setTimeout(function(){c._currentCell={column:parseInt(a.column,10),row:parseInt(a.row,10)},c.markCells()},50)})},_onClick:function(a){var b=this;a.addEventListener("click",function(a){a.preventDefault(),b._callback(this.dataset.row,this.dataset.column)})}},g.prototype={init:function(a){this.options=a,this._doc=a.ownerDocument||document,this._root=this._doc.createElement("div"),this._root.className="medium-editor-table-builder",this.grid=new f(this._root,this.options.onClick,this.options.rows,this.options.columns),this._range=null,this._toolbar=this._doc.createElement("div"),this._toolbar.className="medium-editor-table-builder-toolbar";var b=this._doc.createElement("span");b.innerHTML="Row:",this._toolbar.appendChild(b);var c=this._doc.createElement("button");c.title="Add row before",c.innerHTML='',c.onclick=this.addRow.bind(this,!0),this._toolbar.appendChild(c);var d=this._doc.createElement("button");d.title="Add row after",d.innerHTML='',d.onclick=this.addRow.bind(this,!1),this._toolbar.appendChild(d);var e=this._doc.createElement("button");e.title="Remove row",e.innerHTML='',e.onclick=this.removeRow.bind(this),this._toolbar.appendChild(e);var g=this._doc.createElement("span");g.innerHTML="Column:",this._toolbar.appendChild(g);var h=this._doc.createElement("button");h.title="Add column before",h.innerHTML='',h.onclick=this.addColumn.bind(this,!0),this._toolbar.appendChild(h);var i=this._doc.createElement("button");i.title="Add column after",i.innerHTML='',i.onclick=this.addColumn.bind(this,!1),this._toolbar.appendChild(i);var j=this._doc.createElement("button");j.title="Remove column",j.innerHTML='',j.onclick=this.removeColumn.bind(this),this._toolbar.appendChild(j);var k=this._doc.createElement("button");k.title="Remove table",k.innerHTML='',k.onclick=this.removeTable.bind(this),this._toolbar.appendChild(k);var l=this._root.childNodes[0];this._root.insertBefore(this._toolbar,l)},getElement:function(){return this._root},hide:function(){this._root.style.display="",this.grid.setCurrentCell({column:-1,row:-1}),this.grid.markCells()},show:function(a){this._root.style.display="block",this._root.style.left=a+"px"},setEditor:function(a,b){if(this._range=a,this._toolbar.style.display="block",b){var c=this._doc.getElementsByClassName("medium-editor-table-builder-grid");c[0].style.display="none"}},setBuilder:function(){this._range=null,this._toolbar.style.display="none";var a=this._doc.getElementsByClassName("medium-editor-table-builder-grid");a[0].style.display="block";for(var b=0;b'+d+"",{cleanAttrs:[],cleanTags:[]});var e=this._doc.getElementById("medium-editor-table"),f=this._doc.getElementById("medium-editor-table-tbody");0===$(e).find("#medium-editor-table-tbody").length&&$(f).detach().appendTo(e),f.removeAttribute("id"),e.removeAttribute("id"),c(this._doc,e.querySelector("td"),!0),this._editor.checkSelection()},_html:function(b,c){var d,e,f="",g=a(this._doc);for(d=0;d<=b;d++){for(f+="",e=0;e<=c;e++)f+=""+(0===d&&0===e?g:"
")+"";f+=""}return f},_bindTabBehavior:function(){var a=this;[].forEach.call(this._editor.elements,function(b){b.addEventListener("keydown",function(b){a._onKeyDown(b)})})},_onKeyDown:function(a){var f,g=b(this._doc);a.which===i&&d(g,"table")&&(a.preventDefault(),a.stopPropagation(),f=this._getTableElements(g),a.shiftKey?this._tabBackwards(g.previousSibling,f.row):(this._isLastCell(g,f.row,f.root)&&this._insertRow(e(g,"tbody"),f.row.cells.length),c(this._doc,g)))},_getTableElements:function(a){return{cell:e(a,"td"),row:e(a,"tr"),root:e(a,"table")}},_tabBackwards:function(a,b){a=a||this._getPreviousRowLastCell(b),c(this._doc,a,!0)},_insertRow:function(a,b){var c,d=document.createElement("tr"),e="";for(c=0;c
";d.innerHTML=e,a.appendChild(d)},_isLastCell:function(a,b,c){return b.cells.length-1===a.cellIndex&&c.rows.length-1===b.rowIndex},_getPreviousRowLastCell:function(a){if(a=a.previousSibling)return a.cells[a.cells.length-1]}};var j,k=16,l=1;return j=MediumEditor.extensions.form.extend({name:"table",aria:"create table",action:"table",contentDefault:"TBL",contentFA:'',handleClick:function(a){a.preventDefault(),a.stopPropagation(),this[this.isActive()===!0?"hide":"show"]()},hide:function(){this.setInactive(),this.builder.hide()},show:function(){this.setActive();var a=MediumEditor.selection.getSelectionRange(this.document);"td"===a.startContainer.nodeName.toLowerCase()||"td"===a.endContainer.nodeName.toLowerCase()||MediumEditor.util.getClosestTag(MediumEditor.selection.getSelectedParentElement(a),"td")?this.builder.setEditor(MediumEditor.selection.getSelectedParentElement(a),this.restrictNestedTable):this.builder.setBuilder(),this.builder.show(this.button.offsetLeft)},getForm:function(){return this.builder||(this.builder=new g({onClick:function(a,b){(a>0||b>0)&&this.table.insert(a,b),this.hide()}.bind(this),ownerDocument:this.document,rows:this.rows||10,columns:this.columns||10}),this.table=new h(this.base)),this.builder.getElement()}})}()); -------------------------------------------------------------------------------- /grunt/aliases.yaml: -------------------------------------------------------------------------------- 1 | test: 2 | - jscs 3 | - jshint 4 | - jasmine 5 | 6 | travis: 7 | - test 8 | - coveralls 9 | 10 | lint: 11 | - jscs 12 | - jshint 13 | 14 | js: 15 | - concat 16 | - test 17 | - uglify 18 | 19 | css: 20 | - sass 21 | - autoprefixer 22 | - cssmin 23 | 24 | default: 25 | - css 26 | - js 27 | - watch 28 | -------------------------------------------------------------------------------- /grunt/autoprefixer.js: -------------------------------------------------------------------------------- 1 | var autoprefixerBrowsers = ['last 3 versions', 'ie >= 9']; 2 | 3 | module.exports = { 4 | main: { 5 | expand: true, 6 | cwd: 'dist/css/', 7 | src: ['*.css', '!*.min.css'], 8 | dest: 'dist/css/', 9 | browsers: autoprefixerBrowsers 10 | }, 11 | themes: { 12 | expand: true, 13 | cwd: 'demo/css/', 14 | src: ['*.css', '!*.min.css'], 15 | dest: 'demo/css/', 16 | browsers: autoprefixerBrowsers 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /grunt/bump.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | options: { 3 | files: ['package.json'], 4 | updateConfigs: [], 5 | commit: false, 6 | createTag: false, 7 | push: false 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /grunt/concat.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | dist: { 3 | src: ['src/wrappers/start.js'] 4 | .concat(jsSourceFiles) 5 | .concat(['src/wrappers/end.js']), 6 | dest: 'dist/js/<%= package.name %>.js' 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /grunt/coveralls.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | dist: { 3 | src: 'reports/jasmine/lcov/lcov.info' 4 | } 5 | }; 6 | -------------------------------------------------------------------------------- /grunt/cssmin.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | options: { 3 | shorthandCompacting: false, 4 | roundingPrecision: -1 5 | }, 6 | target: { 7 | files: { 8 | 'dist/css/<%= package.name %>.min.css': ['dist/css/<%= package.name %>.css'] 9 | } 10 | } 11 | }; 12 | -------------------------------------------------------------------------------- /grunt/jasmine.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | suite: { 3 | src: 'src/js/**/*.js', 4 | options: { 5 | specs: 'spec/*.spec.js', 6 | helpers: 'spec/helpers/*.js', 7 | styles: 'dist/css/*.css', 8 | vendor: 'node_modules/medium-editor/dist/js/medium-editor.js', 9 | junit: { 10 | path: 'reports/jasmine/', 11 | consolidate: true 12 | }, 13 | keepRunner: true, 14 | template: require('grunt-template-jasmine-istanbul'), 15 | templateOptions: { 16 | coverage: 'reports/jasmine/coverage.json', 17 | report: [ 18 | { 19 | type: 'lcov', 20 | options: { 21 | dir: 'reports/jasmine/lcov' 22 | } 23 | }, { 24 | type: 'html', 25 | options: { 26 | dir: 'coverage' 27 | } 28 | } 29 | ] 30 | }, 31 | summary: true 32 | } 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /grunt/jscs.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | src: jsSourceFiles.concat(['Gruntfile.js', 'grunt/*.js', 'spec/*.js']), 3 | options: { 4 | config: '.jscsrc' 5 | } 6 | }; 7 | -------------------------------------------------------------------------------- /grunt/jshint.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | build: jsSourceFiles.concat(['Gruntfile.js', 'grunt/*.js', 'spec/*.js']) 3 | }; 4 | -------------------------------------------------------------------------------- /grunt/sass.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | dist: { 3 | files: { 4 | 'dist/css/<%= package.name %>.css': 'src/sass/<%= package.name %>.scss', 5 | 'demo/css/demo.css': 'demo/sass/demo.scss' 6 | } 7 | } 8 | }; 9 | -------------------------------------------------------------------------------- /grunt/uglify.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | dist: { 3 | files: { 4 | 'dist/js/<%= package.name %>.min.js': ['dist/js/<%= package.name %>.js'] 5 | } 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /grunt/watch.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | scripts: { 3 | files: ['src/js/**/*.js', 'spec/*.js', 'Gruntfile.js', 'grunt/*.js'], 4 | tasks: ['js'], 5 | options: { 6 | debounceDelay: 250 7 | } 8 | }, 9 | styles: { 10 | files: ['src/sass/**/*.scss', 'demo/sass/**/*.scss'], 11 | tasks: ['css'], 12 | options: { 13 | debounceDelay: 250 14 | } 15 | } 16 | }; 17 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "medium-editor-tables", 3 | "version": "0.6.1", 4 | "author": "Davi Ferreira ", 5 | "description": "MediumEditor extension to allow tables.", 6 | "main": "dist/js/medium-editor-tables.js", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/yabwe/medium-editor-tables" 10 | }, 11 | "bugs": { 12 | "url": "https://github.com/yabwe/medium-editor-tables/issues" 13 | }, 14 | "homepage": "http://yabwe.github.io/medium-editor/", 15 | "keywords": [ 16 | "editor", 17 | "medium", 18 | "wysiwyg", 19 | "rich-text", 20 | "tables", 21 | "table" 22 | ], 23 | "publishConfig": { 24 | "registry": "http://registry.npmjs.org/" 25 | }, 26 | "license": "MIT", 27 | "devDependencies": { 28 | "grunt": "0.4.5", 29 | "grunt-autoprefixer": "3.0.1", 30 | "grunt-bump": "0.3.1", 31 | "grunt-contrib-concat": "0.5.1", 32 | "grunt-contrib-cssmin": "0.12.3", 33 | "grunt-contrib-jasmine": "0.8.2", 34 | "grunt-contrib-jshint": "0.11.2", 35 | "grunt-contrib-uglify": "0.9.1", 36 | "grunt-contrib-watch": "0.6.1", 37 | "grunt-coveralls": "1.0.0", 38 | "grunt-jscs": "1.8.0", 39 | "grunt-sass": "1.0.0", 40 | "grunt-template-jasmine-istanbul": "0.3.3", 41 | "load-grunt-config": "0.17.1", 42 | "time-grunt": "1.2.1" 43 | }, 44 | "scripts": { 45 | "test": "grunt test --verbose", 46 | "test:ci": "grunt travis --verbose", 47 | "start": "open ./demo/index.html" 48 | }, 49 | "peerDependencies": { 50 | "medium-editor": "^5.5.1" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /spec/helpers/selection.js: -------------------------------------------------------------------------------- 1 | function selectElementContents(el, options) { 2 | options = options || {}; 3 | 4 | var range = document.createRange(), 5 | sel = window.getSelection(); 6 | range.selectNodeContents(el); 7 | 8 | if (options.collapse) { 9 | range.collapse(options.collapse === true); 10 | } 11 | 12 | sel.removeAllRanges(); 13 | sel.addRange(range); 14 | } 15 | -------------------------------------------------------------------------------- /spec/util.spec.js: -------------------------------------------------------------------------------- 1 | describe('Util TestCase', function () { 2 | describe('Selection', function () { 3 | beforeEach(function () { 4 | this.div = document.createElement('div'); 5 | this.div.innerHTML = 'Hello, world!'; 6 | document.body.appendChild(this.div); 7 | selectElementContents(this.div); 8 | }); 9 | 10 | afterEach(function () { 11 | document.body.removeChild(this.div); 12 | }); 13 | 14 | describe('#getSelectionText', function () { 15 | it('should return the element selected text', function () { 16 | expect(getSelectionText(document)).toEqual(this.div.innerHTML); 17 | }); 18 | }); 19 | 20 | describe('#getSelectionStart', function () { 21 | it('should return the selection node', function () { 22 | expect(getSelectionStart(document)).toEqual(this.div); 23 | }); 24 | }); 25 | 26 | describe('#isInsideElementOfTag', function () { 27 | it('should return false when node is invalid', function () { 28 | expect(isInsideElementOfTag(undefined, 'p')).toBe(false); 29 | }); 30 | 31 | it('should return true when parent element has specified tag', function () { 32 | var parentEl = document.createElement('div'), 33 | childEl = document.createElement('p'); 34 | 35 | parentEl.appendChild(childEl); 36 | document.body.appendChild(parentEl); 37 | expect(isInsideElementOfTag(childEl, 'div')).toBe(true); 38 | document.body.removeChild(parentEl); 39 | }); 40 | 41 | it('should return true when parent element does not have specified tag', function () { 42 | var parentEl = document.createElement('div'), 43 | childEl = document.createElement('p'); 44 | 45 | parentEl.appendChild(childEl); 46 | document.body.appendChild(parentEl); 47 | expect(isInsideElementOfTag(childEl, 'span')).toBe(false); 48 | document.body.removeChild(parentEl); 49 | }); 50 | }); 51 | }); 52 | }); 53 | -------------------------------------------------------------------------------- /src/js/builder.js: -------------------------------------------------------------------------------- 1 | function Builder(options) { 2 | return this.init(options); 3 | } 4 | 5 | Builder.prototype = { 6 | init: function (options) { 7 | this.options = options; 8 | this._doc = options.ownerDocument || document; 9 | this._root = this._doc.createElement('div'); 10 | this._root.className = 'medium-editor-table-builder'; 11 | this.grid = new Grid( 12 | this._root, 13 | this.options.onClick, 14 | this.options.rows, 15 | this.options.columns 16 | ); 17 | 18 | this._range = null; 19 | this._toolbar = this._doc.createElement('div'); 20 | this._toolbar.className = 'medium-editor-table-builder-toolbar'; 21 | 22 | var spanRow = this._doc.createElement('span'); 23 | spanRow.innerHTML = 'Row:'; 24 | this._toolbar.appendChild(spanRow); 25 | var addRowBefore = this._doc.createElement('button'); 26 | addRowBefore.title = 'Add row before'; 27 | addRowBefore.innerHTML = ''; 28 | addRowBefore.onclick = this.addRow.bind(this, true); 29 | this._toolbar.appendChild(addRowBefore); 30 | 31 | var addRowAfter = this._doc.createElement('button'); 32 | addRowAfter.title = 'Add row after'; 33 | addRowAfter.innerHTML = ''; 34 | addRowAfter.onclick = this.addRow.bind(this, false); 35 | this._toolbar.appendChild(addRowAfter); 36 | 37 | var remRow = this._doc.createElement('button'); 38 | remRow.title = 'Remove row'; 39 | remRow.innerHTML = ''; 40 | remRow.onclick = this.removeRow.bind(this); 41 | this._toolbar.appendChild(remRow); 42 | 43 | var spanCol = this._doc.createElement('span'); 44 | spanCol.innerHTML = 'Column:'; 45 | this._toolbar.appendChild(spanCol); 46 | var addColumnBefore = this._doc.createElement('button'); 47 | addColumnBefore.title = 'Add column before'; 48 | addColumnBefore.innerHTML = ''; 49 | addColumnBefore.onclick = this.addColumn.bind(this, true); 50 | this._toolbar.appendChild(addColumnBefore); 51 | 52 | var addColumnAfter = this._doc.createElement('button'); 53 | addColumnAfter.title = 'Add column after'; 54 | addColumnAfter.innerHTML = ''; 55 | addColumnAfter.onclick = this.addColumn.bind(this, false); 56 | this._toolbar.appendChild(addColumnAfter); 57 | 58 | var remColumn = this._doc.createElement('button'); 59 | remColumn.title = 'Remove column'; 60 | remColumn.innerHTML = ''; 61 | remColumn.onclick = this.removeColumn.bind(this); 62 | this._toolbar.appendChild(remColumn); 63 | 64 | var remTable = this._doc.createElement('button'); 65 | remTable.title = 'Remove table'; 66 | remTable.innerHTML = ''; 67 | remTable.onclick = this.removeTable.bind(this); 68 | this._toolbar.appendChild(remTable); 69 | 70 | var grid = this._root.childNodes[0]; 71 | this._root.insertBefore(this._toolbar, grid); 72 | }, 73 | 74 | getElement: function () { 75 | return this._root; 76 | }, 77 | 78 | hide: function () { 79 | this._root.style.display = ''; 80 | this.grid.setCurrentCell({ column: -1, row: -1 }); 81 | this.grid.markCells(); 82 | }, 83 | 84 | show: function (left) { 85 | this._root.style.display = 'block'; 86 | this._root.style.left = left + 'px'; 87 | }, 88 | 89 | setEditor: function (range, restrictNestedTable) { 90 | this._range = range; 91 | this._toolbar.style.display = 'block'; 92 | if (restrictNestedTable) { 93 | var elements = this._doc.getElementsByClassName('medium-editor-table-builder-grid'); 94 | elements[0].style.display = 'none'; 95 | } 96 | }, 97 | 98 | setBuilder: function () { 99 | this._range = null; 100 | this._toolbar.style.display = 'none'; 101 | var elements = this._doc.getElementsByClassName('medium-editor-table-builder-grid'); 102 | elements[0].style.display = 'block'; 103 | for (var i = 0; i < elements.length; i++) { 104 | elements[i].style.height = (COLUMN_WIDTH * this.rows + BORDER_WIDTH * 2) + 'px'; 105 | elements[i].style.width = (COLUMN_WIDTH * this.columns + BORDER_WIDTH * 2) + 'px'; 106 | } 107 | }, 108 | 109 | getParentType: function (el, targetNode) { 110 | var nodeName = el && el.nodeName ? el.nodeName.toLowerCase() : false; 111 | if (!nodeName) { 112 | return false; 113 | } 114 | while (nodeName && nodeName !== 'body') { 115 | if (nodeName === targetNode) { 116 | return el; 117 | } 118 | el = el.parentNode; 119 | nodeName = el && el.nodeName ? el.nodeName.toLowerCase() : false; 120 | } 121 | }, 122 | 123 | addRow: function (before, e) { 124 | e.preventDefault(); 125 | e.stopPropagation(); 126 | var tbody = this.getParentType(this._range, 'tbody'), 127 | selectedTR = this.getParentType(this._range, 'tr'), 128 | tr = this._doc.createElement('tr'), 129 | td; 130 | for (var i = 0; i < selectedTR.childNodes.length; i++) { 131 | td = this._doc.createElement('td'); 132 | td.appendChild(this._doc.createElement('br')); 133 | tr.appendChild(td); 134 | } 135 | if (before !== true && selectedTR.nextSibling) { 136 | tbody.insertBefore(tr, selectedTR.nextSibling); 137 | } else if (before === true) { 138 | tbody.insertBefore(tr, selectedTR); 139 | } else { 140 | tbody.appendChild(tr); 141 | } 142 | this.options.onClick(0, 0); 143 | }, 144 | 145 | removeRow: function (e) { 146 | e.preventDefault(); 147 | e.stopPropagation(); 148 | var tbody = this.getParentType(this._range, 'tbody'), 149 | selectedTR = this.getParentType(this._range, 'tr'); 150 | tbody.removeChild(selectedTR); 151 | this.options.onClick(0, 0); 152 | }, 153 | 154 | addColumn: function (before, e) { 155 | e.preventDefault(); 156 | e.stopPropagation(); 157 | var selectedTR = this.getParentType(this._range, 'tr'), 158 | selectedTD = this.getParentType(this._range, 'td'), 159 | cell = Array.prototype.indexOf.call(selectedTR.childNodes, selectedTD), 160 | tbody = this.getParentType(this._range, 'tbody'), 161 | td; 162 | 163 | for (var i = 0; i < tbody.childNodes.length; i++) { 164 | td = this._doc.createElement('td'); 165 | td.appendChild(this._doc.createElement('br')); 166 | if (before === true) { 167 | tbody.childNodes[i].insertBefore(td, tbody.childNodes[i].childNodes[cell]); 168 | } else if (tbody.childNodes[i].childNodes[cell].nextSibling) { 169 | tbody.childNodes[i].insertBefore(td, tbody.childNodes[i].childNodes[cell].nextSibling); 170 | } else { 171 | tbody.childNodes[i].appendChild(td); 172 | } 173 | } 174 | 175 | this.options.onClick(0, 0); 176 | }, 177 | 178 | removeColumn: function (e) { 179 | e.preventDefault(); 180 | e.stopPropagation(); 181 | var selectedTR = this.getParentType(this._range, 'tr'), 182 | selectedTD = this.getParentType(this._range, 'td'), 183 | cell = Array.prototype.indexOf.call(selectedTR.childNodes, selectedTD), 184 | tbody = this.getParentType(this._range, 'tbody'), 185 | rows = tbody.childNodes.length; 186 | 187 | for (var i = 0; i < rows; i++) { 188 | tbody.childNodes[i].removeChild(tbody.childNodes[i].childNodes[cell]); 189 | } 190 | this.options.onClick(0, 0); 191 | }, 192 | 193 | removeTable: function (e) { 194 | e.preventDefault(); 195 | e.stopPropagation(); 196 | var selectedTR = this.getParentType(this._range, 'tr'), 197 | selectedTD = this.getParentType(this._range, 'td'), 198 | cell = Array.prototype.indexOf.call(selectedTR.childNodes, selectedTD), 199 | table = this.getParentType(this._range, 'table'); 200 | 201 | table.parentNode.removeChild(table); 202 | this.options.onClick(0, 0); 203 | } 204 | }; 205 | -------------------------------------------------------------------------------- /src/js/grid.js: -------------------------------------------------------------------------------- 1 | function Grid(el, callback, rows, columns) { 2 | return this.init(el, callback, rows, columns); 3 | } 4 | 5 | Grid.prototype = { 6 | init: function (el, callback, rows, columns) { 7 | this._root = el; 8 | this._callback = callback; 9 | this.rows = rows; 10 | this.columns = columns; 11 | return this._render(); 12 | }, 13 | 14 | setCurrentCell: function (cell) { 15 | this._currentCell = cell; 16 | }, 17 | 18 | markCells: function () { 19 | [].forEach.call(this._cellsElements, function (el) { 20 | var cell = { 21 | column: parseInt(el.getAttribute('data-column'), 10), 22 | row: parseInt(el.getAttribute('data-row'), 10) 23 | }, 24 | active = this._currentCell && 25 | cell.row <= this._currentCell.row && 26 | cell.column <= this._currentCell.column; 27 | 28 | if (active === true) { 29 | if ('classList' in el) { 30 | el.classList.add('active'); 31 | } else { 32 | el.className += ' ' + 'active'; 33 | } 34 | } else { 35 | if ('classList' in el) { 36 | el.classList.remove('active'); 37 | } else { 38 | el.className = (' ' + el.className).replace(' ' + 'active' + ' ', ''); 39 | } 40 | } 41 | }.bind(this)); 42 | }, 43 | 44 | _generateCells: function () { 45 | var row = -1; 46 | 47 | this._cells = []; 48 | 49 | for (var i = 0; i < this.rows * this.columns; i++) { 50 | var column = i % this.columns; 51 | 52 | if (column === 0) { 53 | row++; 54 | } 55 | 56 | this._cells.push({ 57 | column: column, 58 | row: row, 59 | active: false 60 | }); 61 | } 62 | }, 63 | 64 | _html: function () { 65 | var width = this.columns * COLUMN_WIDTH + BORDER_WIDTH * 2, 66 | height = this.rows * COLUMN_WIDTH + BORDER_WIDTH * 2, 67 | html = '
'; 68 | html += this._cellsHTML(); 69 | html += '
'; 70 | return html; 71 | }, 72 | 73 | _cellsHTML: function () { 74 | var html = ''; 75 | this._generateCells(); 76 | this._cells.map(function (cell) { 77 | html += ''; 81 | html += ''; 82 | }); 83 | return html; 84 | }, 85 | 86 | _render: function () { 87 | this._root.innerHTML = this._html(); 88 | this._cellsElements = this._root.querySelectorAll('a'); 89 | this._bindEvents(); 90 | }, 91 | 92 | _bindEvents: function () { 93 | [].forEach.call(this._cellsElements, function (el) { 94 | this._onMouseEnter(el); 95 | this._onClick(el); 96 | }.bind(this)); 97 | }, 98 | 99 | _onMouseEnter: function (el) { 100 | var self = this, 101 | timer; 102 | 103 | el.addEventListener('mouseenter', function (e) { 104 | clearTimeout(timer); 105 | 106 | timer = setTimeout(function () { 107 | self._currentCell = { 108 | column: parseInt(e.target.getAttribute('data-column'), 10), 109 | row: parseInt(e.target.getAttribute('data-row'), 10) 110 | }; 111 | self.markCells(); 112 | }, 50); 113 | }); 114 | }, 115 | 116 | _onClick: function (el) { 117 | var self = this; 118 | el.addEventListener('click', function (e) { 119 | e.preventDefault(); 120 | self._callback(e.target.getAttribute('data-row'), e.target.getAttribute('data-column')); 121 | }); 122 | } 123 | }; 124 | -------------------------------------------------------------------------------- /src/js/plugin.js: -------------------------------------------------------------------------------- 1 | var COLUMN_WIDTH = 16, 2 | BORDER_WIDTH = 1, 3 | MediumEditorTable; 4 | 5 | MediumEditorTable = MediumEditor.extensions.form.extend({ 6 | name: 'table', 7 | 8 | aria: 'create table', 9 | action: 'table', 10 | contentDefault: 'TBL', 11 | contentFA: '', 12 | 13 | handleClick: function (event) { 14 | event.preventDefault(); 15 | event.stopPropagation(); 16 | 17 | this[this.isActive() === true ? 'hide' : 'show'](); 18 | }, 19 | 20 | hide: function () { 21 | this.setInactive(); 22 | this.builder.hide(); 23 | }, 24 | 25 | show: function () { 26 | this.setActive(); 27 | 28 | var range = MediumEditor.selection.getSelectionRange(this.document); 29 | if (range.startContainer.nodeName.toLowerCase() === 'td' || 30 | range.endContainer.nodeName.toLowerCase() === 'td' || 31 | MediumEditor.util.getClosestTag(MediumEditor.selection.getSelectedParentElement(range), 'td')) { 32 | this.builder.setEditor(MediumEditor.selection.getSelectedParentElement(range), this.restrictNestedTable); 33 | } else { 34 | this.builder.setBuilder(); 35 | } 36 | this.builder.show(this.button.offsetLeft); 37 | }, 38 | 39 | getForm: function () { 40 | if (!this.builder) { 41 | this.builder = new Builder({ 42 | onClick: function (rows, columns) { 43 | if (rows > 0 || columns > 0) { 44 | this.table.insert(rows, columns); 45 | } 46 | this.hide(); 47 | }.bind(this), 48 | ownerDocument: this.document, 49 | rows: this.rows || 10, 50 | columns: this.columns || 10 51 | }); 52 | 53 | this.table = new Table(this.base); 54 | } 55 | 56 | return this.builder.getElement(); 57 | } 58 | }); 59 | -------------------------------------------------------------------------------- /src/js/table.js: -------------------------------------------------------------------------------- 1 | function Table(editor) { 2 | return this.init(editor); 3 | } 4 | 5 | var TAB_KEY_CODE = 9; 6 | 7 | Table.prototype = { 8 | init: function (editor) { 9 | this._editor = editor; 10 | this._doc = this._editor.options.ownerDocument; 11 | this._bindTabBehavior(); 12 | }, 13 | 14 | insert: function (rows, cols) { 15 | var html = this._html(rows, cols); 16 | 17 | this._editor.pasteHTML( 18 | '' + 20 | '' + 21 | html + 22 | '' + 23 | '
', { 24 | cleanAttrs: [], 25 | cleanTags: [] 26 | } 27 | ); 28 | 29 | var table = this._doc.getElementById('medium-editor-table'), 30 | tbody = this._doc.getElementById('medium-editor-table-tbody'); 31 | if (0 === $(table).find('#medium-editor-table-tbody').length) { 32 | //Edge case, where tbody gets appended outside table tag 33 | $(tbody).detach().appendTo(table); 34 | } 35 | tbody.removeAttribute('id'); 36 | table.removeAttribute('id'); 37 | placeCaretAtNode(this._doc, table.querySelector('td'), true); 38 | 39 | this._editor.checkSelection(); 40 | }, 41 | 42 | _html: function (rows, cols) { 43 | var html = '', 44 | x, y, 45 | text = getSelectionText(this._doc); 46 | 47 | for (x = 0; x <= rows; x++) { 48 | html += ''; 49 | for (y = 0; y <= cols; y++) { 50 | html += '' + (x === 0 && y === 0 ? text : '
') + ''; 51 | } 52 | html += ''; 53 | } 54 | return html; 55 | }, 56 | 57 | _bindTabBehavior: function () { 58 | var self = this; 59 | [].forEach.call(this._editor.elements, function (el) { 60 | el.addEventListener('keydown', function (e) { 61 | self._onKeyDown(e); 62 | }); 63 | }); 64 | }, 65 | 66 | _onKeyDown: function (e) { 67 | var el = getSelectionStart(this._doc), 68 | table; 69 | 70 | if (e.which === TAB_KEY_CODE && isInsideElementOfTag(el, 'table')) { 71 | e.preventDefault(); 72 | e.stopPropagation(); 73 | table = this._getTableElements(el); 74 | if (e.shiftKey) { 75 | this._tabBackwards(el.previousSibling, table.row); 76 | } else { 77 | if (this._isLastCell(el, table.row, table.root)) { 78 | this._insertRow(getParentOf(el, 'tbody'), table.row.cells.length); 79 | } 80 | placeCaretAtNode(this._doc, el); 81 | } 82 | } 83 | }, 84 | 85 | _getTableElements: function (el) { 86 | return { 87 | cell: getParentOf(el, 'td'), 88 | row: getParentOf(el, 'tr'), 89 | root: getParentOf(el, 'table') 90 | }; 91 | }, 92 | 93 | _tabBackwards: function (el, row) { 94 | el = el || this._getPreviousRowLastCell(row); 95 | placeCaretAtNode(this._doc, el, true); 96 | }, 97 | 98 | _insertRow: function (tbody, cols) { 99 | var tr = document.createElement('tr'), 100 | html = '', 101 | i; 102 | 103 | for (i = 0; i < cols; i += 1) { 104 | html += '
'; 105 | } 106 | tr.innerHTML = html; 107 | tbody.appendChild(tr); 108 | }, 109 | 110 | _isLastCell: function (el, row, table) { 111 | return ( 112 | (row.cells.length - 1) === el.cellIndex && 113 | (table.rows.length - 1) === row.rowIndex 114 | ); 115 | }, 116 | 117 | _getPreviousRowLastCell: function (row) { 118 | row = row.previousSibling; 119 | if (row) { 120 | return row.cells[row.cells.length - 1]; 121 | } 122 | } 123 | }; 124 | -------------------------------------------------------------------------------- /src/js/util.js: -------------------------------------------------------------------------------- 1 | function extend(dest, source) { 2 | var prop; 3 | dest = dest || {}; 4 | for (prop in source) { 5 | if (source.hasOwnProperty(prop) && !dest.hasOwnProperty(prop)) { 6 | dest[prop] = source[prop]; 7 | } 8 | } 9 | return dest; 10 | } 11 | 12 | function getSelectionText(doc) { 13 | if (doc.getSelection) { 14 | return doc.getSelection().toString(); 15 | } 16 | if (doc.selection && doc.selection.type !== 'Control') { 17 | return doc.selection.createRange().text; 18 | } 19 | return ''; 20 | } 21 | 22 | function getSelectionStart(doc) { 23 | var node = doc.getSelection().baseNode ? doc.getSelection().baseNode : doc.getSelection().anchorNode, 24 | startNode = (node && node.nodeType === 3 ? node.parentNode : node); 25 | 26 | return startNode; 27 | } 28 | 29 | function placeCaretAtNode(doc, node, before) { 30 | if (doc.getSelection !== undefined && node) { 31 | var range = doc.createRange(), 32 | selection = doc.getSelection(); 33 | 34 | if (before) { 35 | range.setStartBefore(node); 36 | } else { 37 | range.setStartAfter(node); 38 | } 39 | 40 | range.collapse(true); 41 | 42 | selection.removeAllRanges(); 43 | selection.addRange(range); 44 | } 45 | } 46 | 47 | function isInsideElementOfTag(node, tag) { 48 | if (!node) { 49 | return false; 50 | } 51 | 52 | var parentNode = node.parentNode, 53 | tagName = parentNode.tagName.toLowerCase(); 54 | 55 | while (tagName !== 'body') { 56 | if (tagName === tag) { 57 | return true; 58 | } 59 | parentNode = parentNode.parentNode; 60 | 61 | if (parentNode && parentNode.tagName) { 62 | tagName = parentNode.tagName.toLowerCase(); 63 | } else { 64 | return false; 65 | } 66 | } 67 | 68 | return false; 69 | } 70 | 71 | function getParentOf(el, tagTarget) { 72 | var tagName = el && el.tagName ? el.tagName.toLowerCase() : false; 73 | 74 | if (!tagName) { 75 | return false; 76 | } 77 | while (tagName && tagName !== 'body') { 78 | if (tagName === tagTarget) { 79 | return el; 80 | } 81 | el = el.parentNode; 82 | tagName = el && el.tagName ? el.tagName.toLowerCase() : false; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/sass/medium-editor-tables.scss: -------------------------------------------------------------------------------- 1 | .medium-editor-table-builder { 2 | display: none; 3 | position: absolute; 4 | left: 0; 5 | top: 101%; 6 | 7 | * { 8 | box-sizing: border-box; 9 | } 10 | } 11 | 12 | .medium-editor-table-builder-grid { 13 | border: 1px solid #000; 14 | border-radius: 3px; 15 | overflow: hidden; 16 | } 17 | 18 | .medium-editor-table-builder-cell { 19 | background-color: #333; 20 | border: 1px solid #000; 21 | display: block; 22 | float: left; 23 | height: 16px; 24 | margin: 0; 25 | width: 16px; 26 | 27 | &.active { 28 | background-color: #ccc; 29 | } 30 | } 31 | 32 | .medium-editor-table-builder-cell:hover { 33 | background-color: #ccc; 34 | } 35 | 36 | .medium-editor-table { 37 | border-collapse: collapse; 38 | resize: both; 39 | table-layout: fixed; 40 | } 41 | 42 | .medium-editor-table, 43 | .medium-editor-table td { 44 | border: 1px dashed #e3e3e3; 45 | } 46 | 47 | .medium-editor-table-builder-toolbar { 48 | display: block; 49 | min-width: 162px; 50 | background-color: #333; 51 | font-size: 0.8em; 52 | color: white; 53 | 54 | span { 55 | width: 45px; 56 | display: block; 57 | float: left; 58 | margin-left: 5px; 59 | } 60 | 61 | button { 62 | margin: 0 3px; 63 | background-color: #333; 64 | border: 0; 65 | width: 30px; 66 | cursor: pointer; 67 | 68 | i { 69 | color: white; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/wrappers/end.js: -------------------------------------------------------------------------------- 1 | return MediumEditorTable; 2 | }())); 3 | -------------------------------------------------------------------------------- /src/wrappers/start.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { 2 | 'use strict'; 3 | var isElectron = typeof module === 'object' && 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.MediumEditorTable = factory; 12 | } 13 | }(this, function () { 14 | 15 | 'use strict'; 16 | --------------------------------------------------------------------------------