├── .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 | [](https://nodei.co/npm/medium-editor-tables.png?downloads=true)
4 |
5 | [](https://travis-ci.org/yabwe/medium-editor-tables)
6 | [](https://david-dm.org/yabwe/medium-editor-tables)
7 | [](https://david-dm.org/yabwe/medium-editor-tables#info=devDependencies)
8 | [](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 | 
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 |
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 |
--------------------------------------------------------------------------------