Toolbar, Toolbox, UI (#239)

* Toolbox making

* Add Toolbox buttons click handler

* Toolbar, Toolbox, UI

* Updates

* update css prefix
This commit is contained in:
Peter Savchenko 2017-12-24 15:35:05 +03:00 committed by GitHub
parent c84e4e6191
commit c1afcf0205
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 2680 additions and 1145 deletions

View file

@ -27,7 +27,10 @@
"objectsInArrays": true, "objectsInArrays": true,
"arraysInArrays": true "arraysInArrays": true
}], }],
"quotes": [2, "single", "avoid-escape"], "quotes": [2, "single", {
"avoidEscape": true,
"allowTemplateLiterals": true
}],
"eqeqeq": 0, "eqeqeq": 0,
"brace-style": [2, "1tbs"], "brace-style": [2, "1tbs"],
"comma-spacing": [2, { "comma-spacing": [2, {
@ -75,7 +78,9 @@
"RegExp": true, "RegExp": true,
"Module": true, "Module": true,
"Node": true, "Node": true,
"Element": true,
"Proxy": true, "Proxy": true,
"Symbol": true,
"$": true, "$": true,
"_": true "_": true
} }

File diff suppressed because it is too large Load diff

File diff suppressed because one or more lines are too long

24
docs/tools.md Normal file
View file

@ -0,0 +1,24 @@
# CodeX Editor Tools
CodeX Editor is a block-oriented editor. It means that entry composed with the list of `Blocks` of different types: `Texts`, `Headers`, `Images`, `Quotes` etc.
`Tool` — is a class that provide custom `Block` type. All Tools represented by `Plugins`.
## Tool class structure
### Constructor
### Save
### Render
### Available settings
| Name | Type | Default Value | Description |
| -- | -- | -- | -- |
| `displayInToolbox` | _Boolean_ | `false` | Pass `true` to display this `Tool` in the Editor's `Toolbox` |
| `iconClassName` | _String_ | — | CSS class name for the `Toolbox` icon. Used when `displayInToolbox` is `true` |
| `irreplaceable` | _Boolean_ | `false` | By default, **empty** `Blocks` can be **replaced** by other `Blocks` with the `Toolbox`. Some tools with media-content may prefer another behaviour. Pass `true` and `Toolbox` will add a new block below yours. |
| `contentless` | _Boolean_ | `false` | Pass `true` for Tool which represents decorative empty `Blocks` |
### Sanitize

View file

@ -9,11 +9,6 @@
font-size: 14px; font-size: 14px;
line-height: 1.5em; line-height: 1.5em;
} }
#codex-editor {
margin: 0 auto;
max-width: 800px;
}
</style> </style>
</head> </head>
<body> <body>
@ -22,8 +17,8 @@
</body> </body>
<script src="plugins/paragraph/paragraph.js?v=100"></script> <script src="plugins/text/text.js?v=100"></script>
<link rel="stylesheet" href="plugins/paragraph/paragraph.css"> <link rel="stylesheet" href="plugins/text/text.css">
<!--<script src="plugins/header/header.js"></script>--> <!--<script src="plugins/header/header.js"></script>-->
<!--<link rel="stylesheet" href="plugins/header/header.css">--> <!--<link rel="stylesheet" href="plugins/header/header.css">-->
@ -65,9 +60,9 @@
codex.editor = 1; codex.editor = 1;
var editor = new CodexEditor({ var editor = new CodexEditor({
holderId : 'codex-editor', holderId : 'codex-editor',
initialBlock : 'paragraph', initialBlock : 'text',
tools: { tools: {
paragraph: Paragraph, text: Text,
}, },
toolsConfig: { toolsConfig: {
quote: { quote: {
@ -79,13 +74,13 @@
data: { data: {
items: [ items: [
{ {
type : 'paragraph', type : 'text',
data : { data : {
text : 'Привет от CodeX' text : 'Привет от CodeX'
} }
}, },
{ {
type : 'paragraph', type : 'text',
data : { data : {
text : 'Пишите нам на team@ifmo.su' text : 'Пишите нам на team@ifmo.su'
} }
@ -100,17 +95,17 @@
// }); // });
// codex.editor.start({ // codex.editor.start({
// holderId : "codex-editor", // holderId : "codex-editor",
// initialBlockPlugin : 'paragraph', // initialBlockPlugin : 'text',
// // placeholder: 'Прошлой ночью мне приснилось...', // // placeholder: 'Прошлой ночью мне приснилось...',
// hideToolbar: false, // hideToolbar: false,
// tools : { // tools : {
// paragraph: { // text: {
// type: 'paragraph', // type: 'text',
// iconClassname: 'ce-icon-paragraph', // iconClassname: 'ce-icon-text',
// render: paragraph.render, // render: text.render,
// validate: paragraph.validate, // validate: text.validate,
// save: paragraph.save, // save: text.save,
// destroy: paragraph.destroy, // destroy: text.destroy,
// allowedToPaste: true, // allowedToPaste: true,
// showInlineToolbar: true, // showInlineToolbar: true,
// allowRenderOnPaste: true // allowRenderOnPaste: true
@ -272,7 +267,7 @@
// } // }
// }, // },
// { // {
// type : 'paragraph', // type : 'text',
// data : { // data : {
// text : 'Пишите нам на team@ifmo.su' // text : 'Пишите нам на team@ifmo.su'
// } // }

View file

@ -2,9 +2,10 @@
* Empty paragraph placeholder * Empty paragraph placeholder
*/ */
.ce-paragraph { .ce-text {
padding: 0.7em 0 !important; padding: 15px 0 !important;
line-height: 1.7em; line-height: 1.6em;
outline: none;
} }
.ce-paragraph:empty::before, .ce-paragraph:empty::before,

View file

@ -1,5 +1,5 @@
/** /**
* @class Paragraph * @class Text
* @classdesc Paragraph plugin for CodexEditor * @classdesc Paragraph plugin for CodexEditor
* *
* @author CodeX Team (team@ifmo.su) * @author CodeX Team (team@ifmo.su)
@ -8,33 +8,44 @@
* @version 2.0.0 * @version 2.0.0
* *
* *
* @typedef {Object} ParagraphData * @typedef {Object} TextData
* @property {String} text HTML content to insert to paragraph element * @property {String} text HTML content to insert to text element
* *
*/ */
class Paragraph { class Text {
/** /**
* Get the name of the plugin * Pass true to display this tool in the Editor's Toolbox
* *
* @returns {string} The plugin name * @returns {boolean}
*/ */
static get name() { static get displayInToolbox() {
return 'paragraph'; return true;
}
/**
* Class for the Toolbox icon
*
* @returns {string}
*/
static get iconClassName() {
return 'cdx-text-icon';
} }
/** /**
* Render plugin`s html and set initial content * Render plugin`s html and set initial content
* *
* @param {ParagraphData} data initial plugin content * @param {TextData} data initial plugin content
*/ */
constructor(data = {}) { constructor(data = {}) {
this._CSS = { this._CSS = {
wrapper: 'ce-paragraph' wrapper: 'ce-text'
}; };
this._data = {}; this._data = {};
@ -65,7 +76,7 @@ class Paragraph {
/** /**
* Check if saved text is empty * Check if saved text is empty
* *
* @param {ParagraphData} savedData data received from plugins`s element * @param {TextData} savedData data received from plugins`s element
* @returns {boolean} false if saved text is empty, true otherwise * @returns {boolean} false if saved text is empty, true otherwise
*/ */
validate(savedData) { validate(savedData) {
@ -96,7 +107,7 @@ class Paragraph {
* *
* @todo sanitize data while saving * @todo sanitize data while saving
* *
* @returns {ParagraphData} Current data * @returns {TextData} Current data
*/ */
get data() { get data() {
@ -111,7 +122,7 @@ class Paragraph {
/** /**
* Set new data for plugin * Set new data for plugin
* *
* @param {ParagraphData} data data to set * @param {TextData} data data to set
*/ */
set data(data) { set data(data) {

195
package-lock.json generated
View file

@ -51,9 +51,9 @@
} }
}, },
"ajv": { "ajv": {
"version": "5.5.0", "version": "5.5.2",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.0.tgz", "resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
"integrity": "sha1-6yhAdG6dxIvV4GOjbj/UAMXqtak=", "integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
"dev": true, "dev": true,
"requires": { "requires": {
"co": "4.6.0", "co": "4.6.0",
@ -1225,12 +1225,6 @@
"integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=",
"dev": true "dev": true
}, },
"complex.js": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.0.4.tgz",
"integrity": "sha512-Syl95HpxUTS0QjwNxencZsKukgh1zdS9uXeXX2Us0pHaqBR6kiZZi0AkZ9VpZFwHJyVIUVzI4EumjWdXP3fy6w==",
"dev": true
},
"concat-map": { "concat-map": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -1543,12 +1537,6 @@
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
"dev": true "dev": true
}, },
"decimal.js": {
"version": "7.2.3",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-7.2.3.tgz",
"integrity": "sha512-AoFI37QS0S87Ft0r3Bdz4q9xSpm1Paa9lSeKLXgMPk/u/+QPIM5Gy4DHcZQS1seqPJH4gHLauPGn347z0HbsrA==",
"dev": true
},
"deep-is": { "deep-is": {
"version": "0.1.3", "version": "0.1.3",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
@ -1669,12 +1657,12 @@
} }
}, },
"errno": { "errno": {
"version": "0.1.4", "version": "0.1.6",
"resolved": "https://registry.npmjs.org/errno/-/errno-0.1.4.tgz", "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.6.tgz",
"integrity": "sha1-uJbiOp5ei6M4cfyZar02NfyaHH0=", "integrity": "sha512-IsORQDpaaSwcDP4ZZnHxgE85werpo34VYn1Ud3mq+eUsF593faR8oCZNXrROVkpFu2TsbrNhHin0aUrTsQ9vNw==",
"dev": true, "dev": true,
"requires": { "requires": {
"prr": "0.0.0" "prr": "1.0.1"
} }
}, },
"error-ex": { "error-ex": {
@ -1783,12 +1771,12 @@
} }
}, },
"eslint": { "eslint": {
"version": "4.12.1", "version": "4.13.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-4.12.1.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-4.13.1.tgz",
"integrity": "sha512-28hOYej+NZ/R5H1yMvyKa1+bPlu+fnsIAQffK6hxXgvmXnImos2bA5XfCn5dYv2k2mrKj+/U/Z4L5ICWxC7TQw==", "integrity": "sha512-UCJVV50RtLHYzBp1DZ8CMPtRSg4iVZvjgO9IJHIKyWU/AnJVjtdRikoUPLB29n5pzMB7TnsLQWf0V6VUJfoPfw==",
"dev": true, "dev": true,
"requires": { "requires": {
"ajv": "5.5.0", "ajv": "5.5.2",
"babel-code-frame": "6.26.0", "babel-code-frame": "6.26.0",
"chalk": "2.3.0", "chalk": "2.3.0",
"concat-stream": "1.6.0", "concat-stream": "1.6.0",
@ -1803,11 +1791,11 @@
"file-entry-cache": "2.0.0", "file-entry-cache": "2.0.0",
"functional-red-black-tree": "1.0.1", "functional-red-black-tree": "1.0.1",
"glob": "7.1.2", "glob": "7.1.2",
"globals": "11.0.1", "globals": "11.1.0",
"ignore": "3.3.7", "ignore": "3.3.7",
"imurmurhash": "0.1.4", "imurmurhash": "0.1.4",
"inquirer": "3.3.0", "inquirer": "3.3.0",
"is-resolvable": "1.0.0", "is-resolvable": "1.0.1",
"js-yaml": "3.10.0", "js-yaml": "3.10.0",
"json-stable-stringify-without-jsonify": "1.0.1", "json-stable-stringify-without-jsonify": "1.0.1",
"levn": "0.3.0", "levn": "0.3.0",
@ -1869,9 +1857,9 @@
"dev": true "dev": true
}, },
"globals": { "globals": {
"version": "11.0.1", "version": "11.1.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.0.1.tgz", "resolved": "https://registry.npmjs.org/globals/-/globals-11.1.0.tgz",
"integrity": "sha1-Eqh7sBDlFUOWrMU14eQ/x1Ow5eg=", "integrity": "sha512-uEuWt9mqTlPDwSqi+sHjD4nWU/1N+q0fiWI9T1mZpD2UENqX20CFD5T/ziLZvztPaBKl7ZylUi1q6Qfm7E2CiQ==",
"dev": true "dev": true
}, },
"has-flag": { "has-flag": {
@ -2200,12 +2188,6 @@
"for-in": "1.0.2" "for-in": "1.0.2"
} }
}, },
"fraction.js": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.0.2.tgz",
"integrity": "sha512-OswcigOSil3vYXgrPSx4NCaSyPikXqVNYN/4CyhS0ucVOJ4GVYr6KQQLLcAudvS/4bBOzxqJ3XIsFaaMjl98ZQ==",
"dev": true
},
"fs.realpath": { "fs.realpath": {
"version": "1.0.0", "version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -3679,13 +3661,10 @@
"dev": true "dev": true
}, },
"is-resolvable": { "is-resolvable": {
"version": "1.0.0", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.0.0.tgz", "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.0.1.tgz",
"integrity": "sha1-jfV8YeouPFAUCNEA+wE8+NbgzGI=", "integrity": "sha512-y5CXYbzvB3jTnWAZH1Nl7ykUWb6T3BcTs56HUruwBf8MhF56n1HWqhDWnVFo8GHrUPDgvUUNVhrc2U8W7iqz5g==",
"dev": true, "dev": true
"requires": {
"tryit": "1.0.3"
}
}, },
"is-stream": { "is-stream": {
"version": "1.1.0", "version": "1.1.0",
@ -3729,12 +3708,6 @@
"isarray": "1.0.0" "isarray": "1.0.0"
} }
}, },
"javascript-natural-sort": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
"integrity": "sha1-+eIwPUUH9tdDVac2ZNFED7Wg71k=",
"dev": true
},
"js-base64": { "js-base64": {
"version": "2.4.0", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.0.tgz", "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.0.tgz",
@ -3981,21 +3954,6 @@
"integrity": "sha1-3oGf282E3M2PrlnGrreWFbnSZqw=", "integrity": "sha1-3oGf282E3M2PrlnGrreWFbnSZqw=",
"dev": true "dev": true
}, },
"mathjs": {
"version": "3.17.0",
"resolved": "https://registry.npmjs.org/mathjs/-/mathjs-3.17.0.tgz",
"integrity": "sha512-bFDSjjLV3+csekog1L6z3FjZ0uSkRPFcMlbLef8KXxq68jtQQ48W2f+JKJugM9y6KxJEtt1zWFIGQnYKWR0nxg==",
"dev": true,
"requires": {
"complex.js": "2.0.4",
"decimal.js": "7.2.3",
"fraction.js": "4.0.2",
"javascript-natural-sort": "0.7.1",
"seed-random": "2.2.0",
"tiny-emitter": "2.0.0",
"typed-function": "0.10.6"
}
},
"md5.js": { "md5.js": {
"version": "1.3.4", "version": "1.3.4",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz", "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz",
@ -4033,7 +3991,7 @@
"integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=",
"dev": true, "dev": true,
"requires": { "requires": {
"errno": "0.1.4", "errno": "0.1.6",
"readable-stream": "2.3.3" "readable-stream": "2.3.3"
} }
}, },
@ -6959,14 +6917,67 @@
"dev": true "dev": true
}, },
"postcss-sass": { "postcss-sass": {
"version": "0.1.0", "version": "0.2.0",
"resolved": "https://registry.npmjs.org/postcss-sass/-/postcss-sass-0.1.0.tgz", "resolved": "https://registry.npmjs.org/postcss-sass/-/postcss-sass-0.2.0.tgz",
"integrity": "sha1-DSplW10kHsj0Gbs9o43lyhF0bds=", "integrity": "sha512-cUmYzkP747fPCQE6d+CH2l1L4VSyIlAzZsok3HPjb5Gzsq3jE+VjpAdGlPsnQ310WKWI42sw+ar0UNN59/f3hg==",
"dev": true, "dev": true,
"requires": { "requires": {
"gonzales-pe": "4.2.3", "gonzales-pe": "4.2.3",
"mathjs": "3.17.0", "postcss": "6.0.14"
"postcss": "5.2.18" },
"dependencies": {
"ansi-styles": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.0.tgz",
"integrity": "sha512-NnSOmMEYtVR2JVMIGTzynRkkaxtiq1xnFBcdQD/DnNCYPoEPsVJhM98BDyaoNOQIi7p4okdi3E27eN7GQbsUug==",
"dev": true,
"requires": {
"color-convert": "1.9.1"
}
},
"chalk": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-2.3.0.tgz",
"integrity": "sha512-Az5zJR2CBujap2rqXGaJKaPHyJ0IrUimvYNX+ncCy8PJP4ltOGTrHUIo097ZaL2zMeKYpiCdqDvS6zdrTFok3Q==",
"dev": true,
"requires": {
"ansi-styles": "3.2.0",
"escape-string-regexp": "1.0.5",
"supports-color": "4.5.0"
}
},
"has-flag": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-2.0.0.tgz",
"integrity": "sha1-6CB68cx7MNRGzHC3NLXovhj4jVE=",
"dev": true
},
"postcss": {
"version": "6.0.14",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.14.tgz",
"integrity": "sha512-NJ1z0f+1offCgadPhz+DvGm5Mkci+mmV5BqD13S992o0Xk9eElxUfPPF+t2ksH5R/17gz4xVK8KWocUQ5o3Rog==",
"dev": true,
"requires": {
"chalk": "2.3.0",
"source-map": "0.6.1",
"supports-color": "4.5.0"
}
},
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true
},
"supports-color": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-4.5.0.tgz",
"integrity": "sha1-vnoN5ITexcXN34s9WRJQRJEvY1s=",
"dev": true,
"requires": {
"has-flag": "2.0.0"
}
}
} }
}, },
"postcss-scss": { "postcss-scss": {
@ -7184,16 +7195,16 @@
} }
}, },
"postcss-smart-import": { "postcss-smart-import": {
"version": "0.7.5", "version": "0.7.6",
"resolved": "https://registry.npmjs.org/postcss-smart-import/-/postcss-smart-import-0.7.5.tgz", "resolved": "https://registry.npmjs.org/postcss-smart-import/-/postcss-smart-import-0.7.6.tgz",
"integrity": "sha512-Bs9wAFxH5irGpenBg9a65jTcydZLh7VBTX6NYwMXvVXO6y9CQ83kRmfpQDs5lHhl6IODeIeQfJep5HBMd9UVCQ==", "integrity": "sha512-9OpXaQ1uMMHWafUh0RWIpAKa3xxUDC2yyxicUPpGffH33nzbZG4/z+nk5Ocw5gGZ+3qkXV91iDV23Cmxf2Jhew==",
"dev": true, "dev": true,
"requires": { "requires": {
"babel-runtime": "6.26.0", "babel-runtime": "6.26.0",
"lodash": "4.17.4", "lodash": "4.17.4",
"object-assign": "4.1.1", "object-assign": "4.1.1",
"postcss": "6.0.14", "postcss": "6.0.14",
"postcss-sass": "0.1.0", "postcss-sass": "0.2.0",
"postcss-scss": "1.0.2", "postcss-scss": "1.0.2",
"postcss-value-parser": "3.3.0", "postcss-value-parser": "3.3.0",
"promise-each": "2.2.0", "promise-each": "2.2.0",
@ -7348,9 +7359,9 @@
} }
}, },
"prr": { "prr": {
"version": "0.0.0", "version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-0.0.0.tgz", "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
"integrity": "sha1-GoS4WQgyVQFBGFPQCB7j+obikmo=", "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
"dev": true "dev": true
}, },
"pseudomap": { "pseudomap": {
@ -7823,12 +7834,6 @@
} }
} }
}, },
"seed-random": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/seed-random/-/seed-random-2.2.0.tgz",
"integrity": "sha1-KpsZ4lCoFwmSMaW5mk2vgLf77VQ=",
"dev": true
},
"semver": { "semver": {
"version": "5.4.1", "version": "5.4.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz", "resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz",
@ -8147,7 +8152,7 @@
"integrity": "sha512-UUkEAPdSGxtRpiV9ozJ5cMTtYiqz7Ni1OGqLXRCynrvzdtR1p+cfOWe2RJLwvUG8hNanaSRjecIqwOjqeatDsA==", "integrity": "sha512-UUkEAPdSGxtRpiV9ozJ5cMTtYiqz7Ni1OGqLXRCynrvzdtR1p+cfOWe2RJLwvUG8hNanaSRjecIqwOjqeatDsA==",
"dev": true, "dev": true,
"requires": { "requires": {
"ajv": "5.5.0", "ajv": "5.5.2",
"ajv-keywords": "2.1.1", "ajv-keywords": "2.1.1",
"chalk": "2.3.0", "chalk": "2.3.0",
"lodash": "4.17.4", "lodash": "4.17.4",
@ -8219,12 +8224,6 @@
"setimmediate": "1.0.5" "setimmediate": "1.0.5"
} }
}, },
"tiny-emitter": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.0.0.tgz",
"integrity": "sha1-utMnrbGAS0KiMa+nQVMr2ITNCa0=",
"dev": true
},
"tmp": { "tmp": {
"version": "0.0.33", "version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
@ -8252,12 +8251,6 @@
"integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=", "integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=",
"dev": true "dev": true
}, },
"tryit": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/tryit/-/tryit-1.0.3.tgz",
"integrity": "sha1-OTvnMKlEb9Hq1tpZoBQwjzbCics=",
"dev": true
},
"tty-browserify": { "tty-browserify": {
"version": "0.0.0", "version": "0.0.0",
"resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
@ -8273,12 +8266,6 @@
"prelude-ls": "1.1.2" "prelude-ls": "1.1.2"
} }
}, },
"typed-function": {
"version": "0.10.6",
"resolved": "https://registry.npmjs.org/typed-function/-/typed-function-0.10.6.tgz",
"integrity": "sha512-PYtsDjxyW3vq7Itn2RMz0cn6CrbybIY6XC2i9c1q1o/H94QW8B1Pf3wSsbBDOCMpN1i5jDRrlDsLXFaqXBpfHQ==",
"dev": true
},
"typedarray": { "typedarray": {
"version": "0.0.6", "version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
@ -8443,14 +8430,14 @@
} }
}, },
"webpack": { "webpack": {
"version": "3.9.0", "version": "3.10.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-3.9.0.tgz", "resolved": "https://registry.npmjs.org/webpack/-/webpack-3.10.0.tgz",
"integrity": "sha512-SlBO3yUIhSohW7uCA5c0v03V32DsaXU3vDyUtHB8rubgTgfwl1nv+I+BQIScuQ6exu74wWT6brF/GDXxGLStuA==", "integrity": "sha512-fxxKXoicjdXNUMY7LIdY89tkJJJ0m1Oo8PQutZ5rLgWbV5QVKI15Cn7+/IHnRTd3vfKfiwBx6SBqlorAuNA8LA==",
"dev": true, "dev": true,
"requires": { "requires": {
"acorn": "5.2.1", "acorn": "5.2.1",
"acorn-dynamic-import": "2.0.2", "acorn-dynamic-import": "2.0.2",
"ajv": "5.5.0", "ajv": "5.5.2",
"ajv-keywords": "2.1.1", "ajv-keywords": "2.1.1",
"async": "2.6.0", "async": "2.6.0",
"enhanced-resolve": "3.4.1", "enhanced-resolve": "3.4.1",

View file

@ -17,7 +17,7 @@
"babel-preset-es2015": "^6.24.1", "babel-preset-es2015": "^6.24.1",
"babel-runtime": "^6.26.0", "babel-runtime": "^6.26.0",
"css-loader": "^0.28.7", "css-loader": "^0.28.7",
"eslint": "^4.11.0", "eslint": "^4.13.1",
"eslint-loader": "^1.9.0", "eslint-loader": "^1.9.0",
"extract-text-webpack-plugin": "^3.0.2", "extract-text-webpack-plugin": "^3.0.2",
"html-janitor": "^2.0.2", "html-janitor": "^2.0.2",
@ -36,8 +36,8 @@
"postcss-nested": "^2.1.2", "postcss-nested": "^2.1.2",
"postcss-nested-ancestors": "^1.0.0", "postcss-nested-ancestors": "^1.0.0",
"postcss-nesting": "^4.2.1", "postcss-nesting": "^4.2.1",
"postcss-smart-import": "^0.7.5", "postcss-smart-import": "^0.7.6",
"webpack": "^3.8.1" "webpack": "^3.10.0"
}, },
"dependencies": {} "dependencies": {}
} }

View file

@ -45,15 +45,30 @@
/** /**
* @typedef {Object} EditorConfig * @typedef {Object} EditorConfig
* @property {String} holderId - Element to append Editor * @property {String} holderId - Element to append Editor
* @property {String} initialBlock - Tool name which will be initial * @property {Array} data - Blocks list in JSON-format
* @property {Object} tools - list of tools. The object value must be function (constructor) so that CodexEditor could make an instance * @property {Object} tools - Map for used Tools in format { name : Class, ... }
* @property {@link Tools#ToolsConfig} toolsConfig - tools configuration * @property {String} initialBlock - This Tool will be added by default
* @property {Array} data - Blocks list in JSON-format * @property {String} placeholder - First Block placeholder
* @property {Object} sanitizer - @todo fill desc
* @property {Boolean} hideToolbar - @todo fill desc
* @property {Object} toolsConfig - tools configuration {@link Tools#ToolsConfig}
*/
/**
* Dynamically imported utils
*
* @typedef {Dom} $ - {@link components/dom.js}
* @typedef {Util} _ - {@link components/utils.js}
*/ */
'use strict'; 'use strict';
/**
* Apply polyfills
*/
import 'components/polyfills';
/** /**
* Require Editor modules places in components/modules dir * Require Editor modules places in components/modules dir
*/ */
@ -87,11 +102,19 @@ module.exports = class CodexEditor {
/** /**
* Configuration object * Configuration object
* @type {EditorConfig}
*/ */
this.config = {}; this.config = {};
/** /**
* Editor Components * @typedef {Object} EditorComponents
* @property {BlockManager} BlockManager
* @property {Tools} Tools
* @property {Events} Events
* @property {UI} UI
* @property {Toolbar} Toolbar
* @property {Toolbox} Toolbox
* @property {Renderer} Renderer
*/ */
this.moduleInstances = {}; this.moduleInstances = {};
@ -110,7 +133,7 @@ module.exports = class CodexEditor {
}) })
.catch(error => { .catch(error => {
console.log('CodeX Editor does not ready beecause of %o', error); console.log('CodeX Editor does not ready because of %o', error);
}); });
@ -118,9 +141,9 @@ module.exports = class CodexEditor {
/** /**
* Setting for configuration * Setting for configuration
* @param {Object} config * @param {EditorConfig} config
*/ */
set configuration(config = {}) { set configuration(config) {
this.config.holderId = config.holderId; this.config.holderId = config.holderId;
this.config.placeholder = config.placeholder || 'write your story...'; this.config.placeholder = config.placeholder || 'write your story...';
@ -135,11 +158,24 @@ module.exports = class CodexEditor {
this.config.toolsConfig = config.toolsConfig || {}; this.config.toolsConfig = config.toolsConfig || {};
this.config.data = config.data || []; this.config.data = config.data || [];
/**
* If initial Block's Tool was not passed, use the first Tool in config.tools
*/
if (!config.initialBlock) {
for (this.config.initialBlock in this.config.tools) break;
} else {
this.config.initialBlock = config.initialBlock;
}
} }
/** /**
* Returns private property * Returns private property
* @returns {{}|*} * @returns {EditorConfig}
*/ */
get configuration() { get configuration() {
@ -182,7 +218,6 @@ module.exports = class CodexEditor {
* To prevent this, we use 'babel-plugin-class-display-name' plugin * To prevent this, we use 'babel-plugin-class-display-name' plugin
* @see https://www.npmjs.com/package/babel-plugin-class-display-name * @see https://www.npmjs.com/package/babel-plugin-class-display-name
*/ */
this.moduleInstances[Module.displayName] = new Module({ this.moduleInstances[Module.displayName] = new Module({
config : this.configuration config : this.configuration
}); });
@ -250,8 +285,8 @@ module.exports = class CodexEditor {
let prepareDecorator = module => module.prepare(); let prepareDecorator = module => module.prepare();
return Promise.resolve() return Promise.resolve()
.then(prepareDecorator(this.moduleInstances.UI))
.then(prepareDecorator(this.moduleInstances.Tools)) .then(prepareDecorator(this.moduleInstances.Tools))
.then(prepareDecorator(this.moduleInstances.UI))
.then(() => { .then(() => {
if (this.config.data && this.config.data.items) { if (this.config.data && this.config.data.items) {
@ -307,11 +342,11 @@ module.exports = class CodexEditor {
// * holds initial settings // * holds initial settings
// */ // */
// editor.settings = { // editor.settings = {
// tools : ['paragraph', 'header', 'picture', 'list', 'quote', 'code', 'twitter', 'instagram', 'smile'], // tools : ['text', 'header', 'picture', 'list', 'quote', 'code', 'twitter', 'instagram', 'smile'],
// holderId : 'codex-editor', // holderId : 'codex-editor',
// //
// // Type of block showing on empty editor // // Type of block showing on empty editor
// initialBlockPlugin: 'paragraph' // initialBlockPlugin: 'text'
// }; // };
// //
// /** // /**

View file

@ -1,7 +1,7 @@
/** /**
* @abstract * @abstract
* @class Module * @class Module
* @classdesc All modules inherites from this class. * @classdesc All modules inherits from this class.
* *
* @typedef {Module} Module * @typedef {Module} Module
* @property {Object} config - Editor user settings * @property {Object} config - Editor user settings
@ -22,7 +22,14 @@ export default class Module {
} }
/**
* @type {EditorConfig}
*/
this.config = config; this.config = config;
/**
* @type {EditorComponents}
*/
this.Editor = null; this.Editor = null;
} }

View file

@ -20,15 +20,24 @@ export default class Block {
this.tool = tool; this.tool = tool;
this.CSS = {
wrapper: 'ce-block',
content: 'ce-block__content'
};
this._html = this.compose(); this._html = this.compose();
} }
/**
* CSS classes for the Block
* @return {{wrapper: string, content: string}}
*/
static get CSS() {
return {
wrapper: 'ce-block',
content: 'ce-block__content',
selected: 'ce-block--selected'
};
}
/** /**
* Make default block wrappers and put tool`s content there * Make default block wrappers and put tool`s content there
* *
@ -37,8 +46,8 @@ export default class Block {
*/ */
compose() { compose() {
let wrapper = $.make('div', this.CSS.wrapper), let wrapper = $.make('div', Block.CSS.wrapper),
content = $.make('div', this.CSS.content); content = $.make('div', Block.CSS.content);
content.appendChild(this.tool.html); content.appendChild(this.tool.html);
wrapper.appendChild(content); wrapper.appendChild(content);
@ -58,4 +67,74 @@ export default class Block {
} }
/**
* Check block for emptiness
*
* @return {Boolean}
*/
get isEmpty() {
/**
* Allow Tool to represent decorative contentless blocks: for example "* * *"-tool
* That Tools are not empty
*/
if (this.tool.contentless) {
return false;
}
let emptyText = this._html.textContent.trim().length === 0,
emptyMedia = !this.hasMedia;
return emptyText && emptyMedia;
}
/**
* Check if block has a media content such as images, iframes and other
* @return {Boolean}
*/
get hasMedia() {
/**
* This tags represents media-content
* @type {string[]}
*/
const mediaTags = [
'img',
'iframe',
'video',
'audio',
'source',
'input',
'textarea',
'twitterwidget'
];
return !!this._html.querySelector(mediaTags.join(','));
}
/**
* Set selected state
* @param {Boolean} state - 'true' to select, 'false' to remove selection
*/
set selected(state) {
/**
* We don't need to mark Block as Selected when it is not empty
*/
if (state === true && !this.isEmpty) {
this._html.classList.add(Block.CSS.selected);
} else {
this._html.classList.remove(Block.CSS.selected);
}
}
} }

View file

@ -99,42 +99,6 @@ export default class Core {
} }
/**
* Core custom logger
*
* @param msg
* @param type
* @param args
*/
log(msg, type, args) {
type = type || 'log';
if (!args) {
args = msg || 'undefined';
msg = '[codex-editor]: %o';
} else {
msg = '[codex-editor]: ' + msg;
}
try{
if ( 'console' in window && window.console[ type ] ) {
if ( args ) window.console[ type ]( msg, args );
else window.console[ type ]( msg );
}
} catch(e) {
// do nothing
}
}
/** /**
* Native Ajax * Native Ajax

View file

@ -92,7 +92,7 @@ export default class Dom {
* @param {Object} node * @param {Object} node
* @returns {boolean} * @returns {boolean}
*/ */
static isNode(node) { static isElement(node) {
return node && typeof node === 'object' && node.nodeType && node.nodeType === Node.ELEMENT_NODE; return node && typeof node === 'object' && node.nodeType && node.nodeType === Node.ELEMENT_NODE;

View file

@ -118,7 +118,7 @@ module.exports = class Content {
*/ */
getFirstLevelBlock(node) { getFirstLevelBlock(node) {
if (!$.isNode(node)) { if (!$.isElement(node)) {
node = node.parentNode; node = node.parentNode;

View file

@ -7,15 +7,20 @@
import Block from '../block'; import Block from '../block';
/**
* @typedef {BlockManager} BlockManager
* @property {Number} currentBlockIndex - Index of current working block
* @property {Proxy} _blocks - Proxy for Blocks instance {@link Blocks}
*/
export default class BlockManager extends Module { export default class BlockManager extends Module {
/** /**
* @constructor * @constructor
* @param {EditorConfig} config * @param {EditorConfig} config
*/ */
constructor(config) { constructor({config}) {
super(config); super({config});
/** /**
* Proxy for Blocks instance {@link Blocks} * Proxy for Blocks instance {@link Blocks}
@ -78,13 +83,17 @@ export default class BlockManager extends Module {
* @param {String} toolName plugin name * @param {String} toolName plugin name
* @param {Object} data plugin data * @param {Object} data plugin data
*/ */
insert(toolName, data) { insert(toolName, data = {}) {
let toolInstance = this.Editor.Tools.construct(toolName, data), let toolInstance = this.Editor.Tools.construct(toolName, data),
block = new Block(toolInstance); block = new Block(toolInstance);
this._blocks[++this.currentBlockIndex] = block; this._blocks[++this.currentBlockIndex] = block;
/**
* @todo fire Tool's appendCallback
*/
} }
/** /**
@ -93,13 +102,17 @@ export default class BlockManager extends Module {
* @param {String} toolName plugin name * @param {String} toolName plugin name
* @param {Object} data plugin data * @param {Object} data plugin data
*/ */
replace(toolName, data) { replace(toolName, data = {}) {
let toolInstance = this.Editor.Tools.construct(toolName, data), let toolInstance = this.Editor.Tools.construct(toolName, data),
block = new Block(toolInstance); block = new Block(toolInstance);
this._blocks.insert(this.currentBlockIndex, block, true); this._blocks.insert(this.currentBlockIndex, block, true);
/**
* @todo fire Tool's appendCallback
*/
} }
/** /**
@ -156,8 +169,23 @@ export default class BlockManager extends Module {
let nodes = this._blocks.nodes; let nodes = this._blocks.nodes;
/**
* Update current Block's index
* @type {number}
*/
this.currentBlockIndex = nodes.indexOf(element); this.currentBlockIndex = nodes.indexOf(element);
/**
* Remove previous selected Block's state
*/
this._blocks.array.forEach( block => block.selected = false);
/**
* Mark current Block as selected
* @type {boolean}
*/
this.currentBlock.selected = true;
} }
/** /**
@ -171,6 +199,38 @@ export default class BlockManager extends Module {
} }
/**
* 1) Find first-level Block from passed child Node
* 2) Mark it as current
*
* @param {Element|Text} childNode - look ahead from this node.
* @throws Error - when passed Node is not included at the Block
*/
setCurrentBlockByChildNode(childNode) {
/**
* If node is Text TextNode
*/
if (!$.isElement(childNode)) {
childNode = childNode.parentNode;
}
let parentFirstLevelBlock = childNode.closest(`.${Block.CSS.wrapper}`);
if (parentFirstLevelBlock) {
this.currentNode = parentFirstLevelBlock;
} else {
throw new Error('Can not find a Block from this child Node');
}
}
} }
/** /**

View file

@ -0,0 +1,109 @@
/**
* @class Caret
* @classdesc Contains methods for working Caret
*
* @typedef {Caret} Caret
*/
export default class Caret extends Module {
/**
* @constructor
*/
constructor({config}) {
super({config});
}
/**
* Set Caret to the last Block
*
* If last block is not empty, append another empty block
*/
setToTheLastBlock() {
let blocks = this.Editor.BlockManager.blocks,
lastBlock;
if (blocks.length) {
lastBlock = blocks[blocks.length - 1];
}
/**
* If last block is empty and it is an initialBlock, set to that.
* Otherwise, append new empty block and set to that
*/
if (lastBlock.isEmpty) {
this.set(lastBlock.html);
} else {
this.Editor.BlockManager.insert(this.config.initialBlock);
}
/**
// * If inputs in redactor does not exits, then we put input index 0 not -1
// */
// var indexOfLastInput = editor.state.inputs.length > 0 ? editor.state.inputs.length - 1 : 0;
//
// /** If we have any inputs */
// if (editor.state.inputs.length) {
//
// /** getting firstlevel parent of input */
// firstLevelBlock = editor.content.getFirstLevelBlock(editor.state.inputs[indexOfLastInput]);
//
// }
//
// /** If input is empty, then we set caret to the last input */
// if (editor.state.inputs.length && editor.state.inputs[indexOfLastInput].textContent === '' && firstLevelBlock.dataset.tool == editor.settings.initialBlockPlugin) {
//
// editor.caret.setToBlock(indexOfLastInput);
//
// } else {
//
// /** Create new input when caret clicked in redactors area */
// var NEW_BLOCK_TYPE = editor.settings.initialBlockPlugin;
//
// editor.content.insertBlock({
// type : NEW_BLOCK_TYPE,
// block : editor.tools[NEW_BLOCK_TYPE].render()
// });
//
// /** If there is no inputs except inserted */
// if (editor.state.inputs.length === 1) {
//
// editor.caret.setToBlock(indexOfLastInput);
//
// } else {
//
// /** Set caret to this appended input */
// editor.caret.setToNextBlock(indexOfLastInput);
//
// }
//
// }
}
/**
* Set caret to the passed Node
* @param {Element} node - content-editable Element
*/
set(node) {
/**
* @todo add working with Selection
* tmp: work with textContent
*/
node.textContent += '|';
}
}

View file

@ -15,9 +15,9 @@ export default class Events extends Module {
/** /**
* @constructor * @constructor
*/ */
constructor(config) { constructor({config}) {
super(config); super({config});
this.subscribers = {}; this.subscribers = {};
} }

View file

@ -12,9 +12,9 @@ export default class Renderer extends Module {
* @constructor * @constructor
* @param {EditorConfig} config * @param {EditorConfig} config
*/ */
constructor(config) { constructor({config}) {
super(config); super({config});
} }

View file

@ -43,9 +43,9 @@ export default class Sanitizer extends Module {
* *
* @param {SanitizerConfig} config * @param {SanitizerConfig} config
*/ */
constructor(config) { constructor({config}) {
super(config); super({config});
// default config // default config
this.defaultConfig = null; this.defaultConfig = null;

View file

@ -54,9 +54,9 @@ export default class Toolbar extends Module {
/** /**
* @constructor * @constructor
*/ */
constructor(config) { constructor({config}) {
super(config); super({config});
this.nodes = { this.nodes = {
wrapper : null, wrapper : null,
@ -65,7 +65,6 @@ export default class Toolbar extends Module {
// Content Zone // Content Zone
plusButton : null, plusButton : null,
toolbox : null,
// Actions Zone // Actions Zone
settingsToggler : null, settingsToggler : null,
@ -77,14 +76,25 @@ export default class Toolbar extends Module {
defaultSettings: null, defaultSettings: null,
}; };
this.CSS = { }
/**
* CSS styles
* @return {Object}
* @constructor
*/
static get CSS() {
return {
toolbar: 'ce-toolbar', toolbar: 'ce-toolbar',
content: 'ce-toolbar__content', content: 'ce-toolbar__content',
actions: 'ce-toolbar__actions', actions: 'ce-toolbar__actions',
toolbarOpened: 'ce-toolbar--opened',
// Content Zone // Content Zone
toolbox: 'ce-toolbar__toolbox',
plusButton: 'ce-toolbar__plus', plusButton: 'ce-toolbar__plus',
plusButtonHidden: 'ce-toolbar__plus--hidden',
// Actions Zone // Actions Zone
settingsToggler: 'ce-toolbar__settings-btn', settingsToggler: 'ce-toolbar__settings-btn',
@ -103,14 +113,14 @@ export default class Toolbar extends Module {
*/ */
make() { make() {
this.nodes.wrapper = $.make('div', this.CSS.toolbar); this.nodes.wrapper = $.make('div', Toolbar.CSS.toolbar);
/** /**
* Make Content Zone and Actions Zone * Make Content Zone and Actions Zone
*/ */
['content', 'actions'].forEach( el => { ['content', 'actions'].forEach( el => {
this.nodes[el] = $.make('div', this.CSS[el]); this.nodes[el] = $.make('div', Toolbar.CSS[el]);
$.append(this.nodes.wrapper, this.nodes[el]); $.append(this.nodes.wrapper, this.nodes[el]);
}); });
@ -121,12 +131,15 @@ export default class Toolbar extends Module {
* - Plus Button * - Plus Button
* - Toolbox * - Toolbox
*/ */
['plusButton', 'toolbox'].forEach( el => { this.nodes.plusButton = $.make('div', Toolbar.CSS.plusButton);
$.append(this.nodes.content, this.nodes.plusButton);
this.nodes.plusButton.addEventListener('click', event => this.plusButtonClicked(event), false);
this.nodes[el] = $.make('div', this.CSS[el]);
$.append(this.nodes.content, this.nodes[el]);
}); /**
* Make a Toolbox
*/
this.Editor.Toolbox.make();
/** /**
* Fill Actions Zone: * Fill Actions Zone:
@ -134,7 +147,7 @@ export default class Toolbar extends Module {
* - Remove Block Button * - Remove Block Button
* - Settings Panel * - Settings Panel
*/ */
this.nodes.settingsToggler = $.make('span', this.CSS.settingsToggler); this.nodes.settingsToggler = $.make('span', Toolbar.CSS.settingsToggler);
this.nodes.removeBlockButton = this.makeRemoveBlockButton(); this.nodes.removeBlockButton = this.makeRemoveBlockButton();
$.append(this.nodes.actions, [this.nodes.settingsToggler, this.nodes.removeBlockButton]); $.append(this.nodes.actions, [this.nodes.settingsToggler, this.nodes.removeBlockButton]);
@ -158,10 +171,10 @@ export default class Toolbar extends Module {
*/ */
makeBlockSettingsPanel() { makeBlockSettingsPanel() {
this.nodes.settings = $.make('div', this.CSS.settings); this.nodes.settings = $.make('div', Toolbar.CSS.settings);
this.nodes.pluginSettings = $.make('div', this.CSS.pluginSettings); this.nodes.pluginSettings = $.make('div', Toolbar.CSS.pluginSettings);
this.nodes.defaultSettings = $.make('div', this.CSS.defaultSettings); this.nodes.defaultSettings = $.make('div', Toolbar.CSS.defaultSettings);
$.append(this.nodes.settings, [this.nodes.pluginSettings, this.nodes.defaultSettings]); $.append(this.nodes.settings, [this.nodes.pluginSettings, this.nodes.defaultSettings]);
$.append(this.nodes.actions, this.nodes.settings); $.append(this.nodes.actions, this.nodes.settings);
@ -178,7 +191,83 @@ export default class Toolbar extends Module {
* @todo add confirmation panel and handlers * @todo add confirmation panel and handlers
* @see {@link settings#makeRemoveBlockButton} * @see {@link settings#makeRemoveBlockButton}
*/ */
return $.make('span', this.CSS.removeBlockButton); return $.make('span', Toolbar.CSS.removeBlockButton);
}
/**
* Move Toolbar to the Current Block
*/
move() {
/** Close Toolbox when we move toolbar */
this.Editor.Toolbox.close();
let currentNode = this.Editor.BlockManager.currentNode;
/**
* If no one Block selected as a Current
*/
if (!currentNode) {
return;
}
/**
* @todo Compute dynamically on prepare
* @type {number}
*/
const defaultToolbarHeight = 49;
const defaultOffset = 34;
var newYCoordinate = currentNode.offsetTop - (defaultToolbarHeight / 2) + defaultOffset;
this.nodes.wrapper.style.transform = `translate3D(0, ${Math.floor(newYCoordinate)}px, 0)`;
/** Close trash actions */
// editor.toolbar.settings.hideRemoveActions();
}
/**
* Open Toolbar with Plus Button
*/
open() {
this.nodes.wrapper.classList.add(Toolbar.CSS.toolbarOpened);
}
/**
* Close the Toolbar
*/
close() {
this.nodes.wrapper.classList.remove(Toolbar.CSS.toolbarOpened);
}
/**
* Plus Button public methods
* @return {{hide: function(): void, show: function(): void}}
*/
get plusButton() {
return {
hide: () => this.nodes.plusButton.classList.add(Toolbar.CSS.plusButtonHidden),
show: () => this.nodes.plusButton.classList.remove(Toolbar.CSS.plusButtonHidden)
};
}
/**
* Handler for Plus Button
* @param {MouseEvent} event
*/
plusButtonClicked(event) {
this.Editor.Toolbox.toggle();
} }

View file

@ -0,0 +1,223 @@
/**
* @class Toolbox
* @classdesc Holder for Tools
*
* @typedef {Toolbox} Toolbox
* @property {Boolean} opened - opening state
* @property {Object} nodes - Toolbox nodes
* @property {Object} CSS - CSS class names
*
*/
export default class Toolbox extends Module {
/**
* @constructor
*/
constructor({config}) {
super({config});
this.nodes = {
toolbox: null,
buttons: []
};
/**
* Opening state
* @type {boolean}
*/
this.opened = false;
}
/**
* CSS styles
* @return {{toolbox: string, toolboxButton: string, toolboxOpened: string}}
*/
static get CSS() {
return {
toolbox: 'ce-toolbox',
toolboxButton: 'ce-toolbox__button',
toolboxOpened: 'ce-toolbox--opened',
};
}
/**
* Makes the Toolbox
*/
make() {
this.nodes.toolbox = $.make('div', Toolbox.CSS.toolbox);
$.append(this.Editor.Toolbar.nodes.content, this.nodes.toolbox);
this.addTools();
}
/**
* Iterates available tools and appends them to the Toolbox
*/
addTools() {
let tools = this.Editor.Tools.toolsAvailable;
for (let toolName in tools) {
this.addTool(toolName, tools[toolName]);
}
}
/**
* Append Tool to the Toolbox
*
* @param {string} toolName - tool name
* @param {Tool} tool - tool class
*/
addTool(toolName, tool) {
if (tool.displayInToolbox && !tool.iconClassName) {
_.log('Toolbar icon class name is missed. Tool %o skipped', 'warn', toolName);
return;
}
/**
* @todo Add checkup for the render method
*/
// if (typeof tool.render !== 'function') {
//
// _.log('render method missed. Tool %o skipped', 'warn', tool);
// return;
//
// }
/**
* Skip tools that pass 'displayInToolbox=false'
*/
if (!tool.displayInToolbox) {
return;
}
let button = $.make('li', [Toolbox.CSS.toolboxButton, tool.iconClassName], {
title: toolName
});
/**
* Save tool's name in the button data-name
*/
button.dataset.name = toolName;
$.append(this.nodes.toolbox, button);
this.nodes.toolbox.appendChild(button);
this.nodes.buttons.push(button);
/**
* @todo add event with module Listeners
*/
// this.Editor.Listeners.add();
button.addEventListener('click', event => {
this.buttonClicked(event);
}, false);
}
/**
* Toolbox button click listener
* 1) if block is empty -> replace
* 2) if block is not empty -> add new block below
*
* @param {MouseEvent} event
*/
buttonClicked(event) {
let toolButton = event.target,
toolName = toolButton.dataset.name,
tool = this.Editor.Tools.toolClasses[toolName];
/**
* @type {Block}
*/
let currentBlock = this.Editor.BlockManager.currentBlock;
/**
* We do replace if:
* - block is empty
* - block is not irreplaceable
* @type {Array}
*/
if (!tool.irreplaceable && currentBlock.isEmpty) {
this.Editor.BlockManager.replace(toolName);
} else {
this.Editor.BlockManager.insert(toolName);
}
/**
* @todo set caret to the new block
*/
// window.setTimeout(function () {
/** Set caret to current block */
// editor.caret.setToBlock(currentInputIndex);
// }, 10);
/**
* Move toolbar when node is changed
*/
this.Editor.Toolbar.move();
}
/**
* Open Toolbox with Tools
*/
open() {
this.nodes.toolbox.classList.add(Toolbox.CSS.toolboxOpened);
this.opened = true;
}
/**
* Close Toolbox
*/
close() {
this.nodes.toolbox.classList.remove(Toolbox.CSS.toolboxOpened);
this.opened = false;
}
/**
* Close Toolbox
*/
toggle() {
if (!this.opened) {
this.open();
} else {
this.close();
}
}
}

View file

@ -5,10 +5,9 @@
*/ */
/** /**
* Load user defined tools * Each Tool must contain the following important objects:
* Tools must contain the following important objects:
* *
* @typedef {Object} ToolsConfig * @typedef {Object} ToolConfig {@link docs/tools.md}
* @property {String} iconClassname - this a icon in toolbar * @property {String} iconClassname - this a icon in toolbar
* @property {Boolean} displayInToolbox - will be displayed in toolbox. Default value is TRUE * @property {Boolean} displayInToolbox - will be displayed in toolbox. Default value is TRUE
* @property {Boolean} enableLineBreaks - inserts new block or break lines. Default value is FALSE * @property {Boolean} enableLineBreaks - inserts new block or break lines. Default value is FALSE
@ -19,15 +18,28 @@
*/ */
/** /**
* @typedef {Tool} Tool * @typedef {Function} Tool {@link docs/tools.md}
* @property {String} name - name of this module * @property {Boolean} displayInToolbox - By default, tools won't be added in the Toolbox. Pass true to add.
* @property {Object[]} toolInstances - list of tool instances * @property {String} iconClassName - CSS class name for the Toolbox button
* @property {Tools[]} available - available Tools * @property {Boolean} irreplaceable - Toolbox behaviour: replace or add new block below
* @property {Tools[]} unavailable - unavailable Tools * @property render
* @property save
* @property settings
* @property validate
*
* @todo update according to current API
* @todo describe Tool in the {@link docs/tools.md}
*/
/**
* Class properties:
*
* @typedef {Tools} Tools
* @property {Tools[]} toolsAvailable - available Tools
* @property {Tools[]} toolsUnavailable - unavailable Tools
* @property {Object} toolsClasses - all classes * @property {Object} toolsClasses - all classes
* @property {EditorConfig} config - Editor config * @property {EditorConfig} config - Editor config
*/ */
export default class Tools extends Module { export default class Tools extends Module {
/** /**
@ -51,15 +63,18 @@ export default class Tools extends Module {
} }
/** /**
* If config wasn't passed by user * Static getter for default Tool config fields
* @return {ToolsConfig} *
* @usage Tools.defaultConfig.displayInToolbox
* @return {ToolConfig}
*/ */
get defaultConfig() { static get defaultConfig() {
return { return {
iconClassName : 'default-icon', iconClassName : '',
displayInToolbox : false, displayInToolbox : false,
enableLineBreaks : false enableLineBreaks : false,
irreplaceable : false
}; };
} }
@ -67,21 +82,38 @@ export default class Tools extends Module {
/** /**
* @constructor * @constructor
* *
* @param {ToolsConfig} config * @param {EditorConfig} config
*/ */
constructor(config) { constructor({config}) {
super(config); super({config});
/**
* Map {name: Class, ...} where:
* name block type name in JSON. Got from EditorConfig.tools keys
* @type {Object}
*/
this.toolClasses = {}; this.toolClasses = {};
/**
* Available tools list
* {name: Class, ...}
* @type {Object}
*/
this.toolsAvailable = {}; this.toolsAvailable = {};
/**
* Tools that rejected a prepare method
* {name: Class, ... }
* @type {Object}
*/
this.toolsUnavailable = {}; this.toolsUnavailable = {};
} }
/** /**
* Creates instances via passed or default configuration * Creates instances via passed or default configuration
* @return {boolean} * @return {Promise}
*/ */
prepare() { prepare() {
@ -128,7 +160,7 @@ export default class Tools extends Module {
/** /**
* Binds prepare function of plugins with user or default config * Binds prepare function of plugins with user or default config
* @return {Array} list of functions that needs to be fired sequently * @return {Array} list of functions that needs to be fired sequentially
*/ */
getListOfPrepareFunctions() { getListOfPrepareFunctions() {
@ -147,6 +179,13 @@ export default class Tools extends Module {
} }
}); });
} else {
/**
* If Tool hasn't a prepare method, mark it as available
*/
this.toolsAvailable[toolName] = toolClass;
} }
} }
@ -173,16 +212,6 @@ export default class Tools extends Module {
} }
/**
* Returns all tools
* @return {Array}
*/
getTools() {
return this.toolInstances;
}
/** /**
* Return tool`a instance * Return tool`a instance
* *
@ -209,4 +238,15 @@ export default class Tools extends Module {
} }
/**
* Check if passed Tool is an instance of Initial Block Tool
* @param {Tool} tool - Tool to check
* @return {Boolean}
*/
isInitial(tool) {
return tool instanceof this.available[this.config.initialBlock];
}
} }

View file

@ -31,10 +31,7 @@
// SETTINGS_ITEM : 'ce-settings__item' // SETTINGS_ITEM : 'ce-settings__item'
// }; // };
let CSS = { import Block from '../block';
editorWrapper : 'codex-editor',
editorZone : 'ce-redactor'
};
/** /**
* @class * @class
@ -54,7 +51,6 @@ let CSS = {
* @property {Element} nodes.wrapper - <codex-editor> * @property {Element} nodes.wrapper - <codex-editor>
* @property {Element} nodes.redactor - <ce-redactor> * @property {Element} nodes.redactor - <ce-redactor>
*/ */
export default class UI extends Module { export default class UI extends Module {
/** /**
@ -62,9 +58,9 @@ export default class UI extends Module {
* *
* @param {EditorConfig} config * @param {EditorConfig} config
*/ */
constructor(config) { constructor({config}) {
super(config); super({config});
this.nodes = { this.nodes = {
holder: null, holder: null,
@ -81,45 +77,19 @@ export default class UI extends Module {
*/ */
prepare() { prepare() {
return new Promise( (resolve, reject) => { return this.make()
/**
* Element where we need to append CodeX Editor
* @type {Element}
*/
this.nodes.holder = document.getElementById(this.config.holderId);
if (!this.nodes.holder) {
reject(Error("Holder wasn't found by ID: #" + this.config.holderId));
return;
}
/**
* Create and save main UI elements
*/
this.nodes.wrapper = $.make('div', CSS.editorWrapper);
this.nodes.redactor = $.make('div', CSS.editorZone);
this.nodes.wrapper.appendChild(this.nodes.redactor);
this.nodes.holder.appendChild(this.nodes.wrapper);
/** /**
* Make toolbar * Make toolbar
*/ */
this.Editor.Toolbar.make(); .then(() => this.Editor.Toolbar.make())
/** /**
* Load and append CSS * Load and append CSS
*/ */
this.loadStyles(); .then(() => this.loadStyles())
/**
resolve(); * Bind events for the UI elements
*/
}) .then(() => this.bindEvents())
/** Add toolbox tools */
// .then(addTools_)
/** Make container for inline toolbar */ /** Make container for inline toolbar */
// .then(makeInlineToolbar_) // .then(makeInlineToolbar_)
@ -143,6 +113,58 @@ export default class UI extends Module {
} }
/**
* CodeX Editor UI CSS class names
* @return {{editorWrapper: string, editorZone: string, block: string}}
*/
get CSS() {
return {
editorWrapper : 'codex-editor',
editorZone : 'codex-editor__redactor',
};
}
/**
* Makes CodeX Editor interface
* @return {Promise<any>}
*/
make() {
return new Promise( (resolve, reject) => {
/**
* Element where we need to append CodeX Editor
* @type {Element}
*/
this.nodes.holder = document.getElementById(this.config.holderId);
if (!this.nodes.holder) {
reject(Error("Holder wasn't found by ID: #" + this.config.holderId));
return;
}
/**
* Create and save main UI elements
*/
this.nodes.wrapper = $.make('div', this.CSS.editorWrapper);
this.nodes.redactor = $.make('div', this.CSS.editorZone);
this.nodes.wrapper.appendChild(this.nodes.redactor);
this.nodes.holder.appendChild(this.nodes.wrapper);
resolve();
});
}
/**
* Appends CSS
*/
loadStyles() { loadStyles() {
/** /**
@ -164,6 +186,174 @@ export default class UI extends Module {
} }
/**
* Bind events on the CodeX Editor interface
*/
bindEvents() {
/**
* @todo bind events with the Listeners module
*/
this.nodes.redactor.addEventListener('click', event => this.redactorClicked(event), false );
}
/**
* All clicks on the redactor zone
*
* @param {MouseEvent} event
*
* @description
* 1. Save clicked Block as a current {@link BlockManager#currentNode}
* it uses for the following:
* - add CSS modifier for the selected Block
* - on Enter press, we make a new Block under that
*
* 2. Move and show the Toolbar
*
* 3. Set a Caret
*
* 4. By clicks on the Editor's bottom zone:
* - if last Block is empty, set a Caret to this
* - otherwise, add a new empty Block and set a Caret to that
*
* 5. Hide the Inline Toolbar
*
* @see selectClickedBlock
*
*/
redactorClicked(event) {
let clickedNode = event.target;
/**
* Select clicked Block as Current
*/
try {
this.Editor.BlockManager.setCurrentBlockByChildNode(clickedNode);
/**
* If clicked outside first-level Blocks, set Caret to the last empty Block
*/
} catch (e) {
this.Editor.Caret.setToTheLastBlock();
}
/**
* @todo hide the Inline Toolbar
*/
// var selectedText = editor.toolbar.inline.getSelectionText(),
// firstLevelBlock;
/** If selection range took off, then we hide inline toolbar */
// if (selectedText.length === 0) {
// editor.toolbar.inline.close();
// }
/**
*
/** Update current input index in memory when caret focused into existed input */
// if (event.target.contentEditable == 'true') {
//
// editor.caret.saveCurrentInputIndex();
//
// }
// if (editor.content.currentNode === null) {
//
// /**
// * If inputs in redactor does not exits, then we put input index 0 not -1
// */
// var indexOfLastInput = editor.state.inputs.length > 0 ? editor.state.inputs.length - 1 : 0;
//
// /** If we have any inputs */
// if (editor.state.inputs.length) {
//
// /** getting firstlevel parent of input */
// firstLevelBlock = editor.content.getFirstLevelBlock(editor.state.inputs[indexOfLastInput]);
//
// }
//
// /** If input is empty, then we set caret to the last input */
// if (editor.state.inputs.length && editor.state.inputs[indexOfLastInput].textContent === '' && firstLevelBlock.dataset.tool == editor.settings.initialBlockPlugin) {
//
// editor.caret.setToBlock(indexOfLastInput);
//
// } else {
//
// /** Create new input when caret clicked in redactors area */
// var NEW_BLOCK_TYPE = editor.settings.initialBlockPlugin;
//
// editor.content.insertBlock({
// type : NEW_BLOCK_TYPE,
// block : editor.tools[NEW_BLOCK_TYPE].render()
// });
//
// /** If there is no inputs except inserted */
// if (editor.state.inputs.length === 1) {
//
// editor.caret.setToBlock(indexOfLastInput);
//
// } else {
//
// /** Set caret to this appended input */
// editor.caret.setToNextBlock(indexOfLastInput);
//
// }
//
// }
//
// } else {
//
// /** Close all panels */
// editor.toolbar.settings.close();
// editor.toolbar.toolbox.close();
//
// }
//
/**
* Move toolbar and open
*/
this.Editor.Toolbar.move();
this.Editor.Toolbar.open();
//
// var inputIsEmpty = !editor.content.currentNode.textContent.trim(),
// currentNodeType = editor.content.currentNode.dataset.tool,
// isInitialType = currentNodeType == editor.settings.initialBlockPlugin;
//
//
/**
* Hide the Plus Button
* */
this.Editor.Toolbar.plusButton.hide();
/**
* Show the Plus Button if:
* - Block is an initial-block (Text)
* - Block is empty
*/
let isInitialBlock = this.Editor.Tools.isInitial(this.Editor.BlockManager.currentBlock.tool),
isEmptyBlock = this.Editor.BlockManager.currentBlock.isEmpty;
if (isInitialBlock && isEmptyBlock) {
this.Editor.Toolbar.plusButton.show();
}
}
} }
// /** // /**
@ -194,54 +384,6 @@ export default class UI extends Module {
// //
// }; // };
// //
// /**
// * @private
// * Append tools passed in editor.tools
// */
// var addTools_ = function () {
//
// var tool,
// toolName,
// toolButton;
//
// for ( toolName in editor.settings.tools ) {
//
// tool = editor.settings.tools[toolName];
//
// editor.tools[toolName] = tool;
//
// if (!tool.iconClassname && tool.displayInToolbox) {
//
// editor.core.log('Toolbar icon classname missed. Tool %o skipped', 'warn', toolName);
// continue;
//
// }
//
// if (typeof tool.render != 'function') {
//
// editor.core.log('render method missed. Tool %o skipped', 'warn', toolName);
// continue;
//
// }
//
// if (!tool.displayInToolbox) {
//
// continue;
//
// } else {
//
// /** if tools is for toolbox */
// toolButton = editor.draw.toolbarButton(toolName, tool.iconClassname);
//
// editor.nodes.toolbox.appendChild(toolButton);
//
// editor.nodes.toolbarButtons[toolName] = toolButton;
//
// }
//
// }
//
// };
// //
// var addInlineToolbarTools_ = function () { // var addInlineToolbarTools_ = function () {
// //

View file

@ -0,0 +1,24 @@
/**
* Element.closest()
*
* https://developer.mozilla.org/en-US/docs/Web/API/Element/closest
*/
if (!Element.prototype.matches)
Element.prototype.matches = Element.prototype.msMatchesSelector ||
Element.prototype.webkitMatchesSelector;
if (!Element.prototype.closest)
Element.prototype.closest = function (s) {
var el = this;
if (!document.documentElement.contains(el)) return null;
do {
if (el.matches(s)) return el;
el = el.parentElement || el.parentNode;
} while (el !== null);
return null;
};

View file

@ -3,6 +3,43 @@
*/ */
export default class Util { export default class Util {
/**
* Custom logger
*
* @param {string} msg - message
* @param {string} type - logging type 'log'|'warn'|'error'|'info'
* @param {*} args - argument to log with a message
*/
static log(msg, type, args) {
type = type || 'log';
if (!args) {
args = msg || 'undefined';
msg = '[codex-editor]: %o';
} else {
msg = '[codex-editor]: ' + msg;
}
try{
if ( 'console' in window && window.console[ type ] ) {
if ( args ) window.console[ type ]( msg, args );
else window.console[ type ]( msg );
}
} catch(e) {
// do nothing
}
}
/** /**
* @typedef {Object} ChainData * @typedef {Object} ChainData
* @property {Object} data - data that will be passed to the success or fallback * @property {Object} data - data that will be passed to the success or fallback

12
src/styles/block.css Normal file
View file

@ -0,0 +1,12 @@
.ce-block {
border: 1px dotted #ccc;
margin: 2px 0;
&--selected {
background-color: var(--bg-light);
}
&__content {
max-width: var(--content-width);
margin: 0 auto;
}
}

View file

@ -1,2 +1,5 @@
@import url('variables.css'); @import url('variables.css');
@import url('ui.css'); @import url('ui.css');
@import url('toolbar.css');
@import url('toolbox.css');
@import url('block.css');

44
src/styles/toolbar.css Normal file
View file

@ -0,0 +1,44 @@
.ce-toolbar {
position: absolute;
left: 0;
right: 0;
top: 0;
opacity: 0;
visibility: hidden;
transition: opacity 100ms ease;
will-change: opacity, transform;
&--opened {
opacity: 1;
visibility: visible;
}
&__content {
max-width: var(--content-width);
margin: 0 auto;
position: relative;
}
&__plus {
position: absolute;
left: calc(-var(--toolbar-buttons-size) - 10px);
display: inline-block;
background-color: var(--bg-light);
width: var(--toolbar-buttons-size);
height: var(--toolbar-buttons-size);
line-height: 34px;
text-align: center;
border-radius: 50%;
&::after {
content: '+';
font-size: 26px;
display: block;
margin-top: -2px;
margin-right: -2px;
}
&--hidden {
display: none;
}
}
}

34
src/styles/toolbox.css Normal file
View file

@ -0,0 +1,34 @@
.ce-toolbox {
visibility: hidden;
transition: opacity 100ms ease;
will-change: opacity;
&--opened {
opacity: 1;
visibility: visible;
}
&__button {
display: inline-block;
list-style: none;
margin: 0;
background: var(--bg-light);
width: var(--toolbar-buttons-size);
height: var(--toolbar-buttons-size);
border-radius: 30px;
overflow: hidden;
text-align: center;
line-height: var(--toolbar-buttons-size);
&::before {
content: attr(title);
font-size: 22px;
font-weight: 500;
letter-spacing: 1em;
font-variant-caps: all-small-caps;
padding-left: 11.5px;
margin-top: -1px;
display: inline-block;
}
}
}

View file

@ -1,12 +1,17 @@
/** /**
* Editor wrapper * Editor wrapper
*/ */
.codex-editor{ .codex-editor {
position: relative; position: relative;
border: 1px solid #ccc; border: 1px solid #ccc;
padding: 10px; padding: 10px;
box-sizing: border-box;
.hide { .hide {
display: none; display: none;
} }
&__redactor {
padding-bottom: 300px;
}
} }

View file

@ -5,4 +5,14 @@
*/ */
--bg-light: #eff2f5; --bg-light: #eff2f5;
/**
* Block content width
*/
--content-width: 650px;
/**
* Toolbar Plus Button and Toolbox buttons height and width
*/
--toolbar-buttons-size: 34px;
} }