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,
"arraysInArrays": true
}],
"quotes": [2, "single", "avoid-escape"],
"quotes": [2, "single", {
"avoidEscape": true,
"allowTemplateLiterals": true
}],
"eqeqeq": 0,
"brace-style": [2, "1tbs"],
"comma-spacing": [2, {
@ -75,7 +78,9 @@
"RegExp": true,
"Module": true,
"Node": true,
"Element": true,
"Proxy": true,
"Symbol": 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;
line-height: 1.5em;
}
#codex-editor {
margin: 0 auto;
max-width: 800px;
}
</style>
</head>
<body>
@ -22,8 +17,8 @@
</body>
<script src="plugins/paragraph/paragraph.js?v=100"></script>
<link rel="stylesheet" href="plugins/paragraph/paragraph.css">
<script src="plugins/text/text.js?v=100"></script>
<link rel="stylesheet" href="plugins/text/text.css">
<!--<script src="plugins/header/header.js"></script>-->
<!--<link rel="stylesheet" href="plugins/header/header.css">-->
@ -65,9 +60,9 @@
codex.editor = 1;
var editor = new CodexEditor({
holderId : 'codex-editor',
initialBlock : 'paragraph',
initialBlock : 'text',
tools: {
paragraph: Paragraph,
text: Text,
},
toolsConfig: {
quote: {
@ -79,13 +74,13 @@
data: {
items: [
{
type : 'paragraph',
type : 'text',
data : {
text : 'Привет от CodeX'
}
},
{
type : 'paragraph',
type : 'text',
data : {
text : 'Пишите нам на team@ifmo.su'
}
@ -100,17 +95,17 @@
// });
// codex.editor.start({
// holderId : "codex-editor",
// initialBlockPlugin : 'paragraph',
// initialBlockPlugin : 'text',
// // placeholder: 'Прошлой ночью мне приснилось...',
// hideToolbar: false,
// tools : {
// paragraph: {
// type: 'paragraph',
// iconClassname: 'ce-icon-paragraph',
// render: paragraph.render,
// validate: paragraph.validate,
// save: paragraph.save,
// destroy: paragraph.destroy,
// text: {
// type: 'text',
// iconClassname: 'ce-icon-text',
// render: text.render,
// validate: text.validate,
// save: text.save,
// destroy: text.destroy,
// allowedToPaste: true,
// showInlineToolbar: true,
// allowRenderOnPaste: true
@ -272,7 +267,7 @@
// }
// },
// {
// type : 'paragraph',
// type : 'text',
// data : {
// text : 'Пишите нам на team@ifmo.su'
// }

View file

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

View file

@ -1,5 +1,5 @@
/**
* @class Paragraph
* @class Text
* @classdesc Paragraph plugin for CodexEditor
*
* @author CodeX Team (team@ifmo.su)
@ -8,33 +8,44 @@
* @version 2.0.0
*
*
* @typedef {Object} ParagraphData
* @property {String} text HTML content to insert to paragraph element
* @typedef {Object} TextData
* @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
*
* @param {ParagraphData} data initial plugin content
* @param {TextData} data initial plugin content
*/
constructor(data = {}) {
this._CSS = {
wrapper: 'ce-paragraph'
wrapper: 'ce-text'
};
this._data = {};
@ -65,7 +76,7 @@ class Paragraph {
/**
* 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
*/
validate(savedData) {
@ -96,7 +107,7 @@ class Paragraph {
*
* @todo sanitize data while saving
*
* @returns {ParagraphData} Current data
* @returns {TextData} Current data
*/
get data() {
@ -111,7 +122,7 @@ class Paragraph {
/**
* Set new data for plugin
*
* @param {ParagraphData} data data to set
* @param {TextData} data data to set
*/
set data(data) {

195
package-lock.json generated
View file

@ -51,9 +51,9 @@
}
},
"ajv": {
"version": "5.5.0",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.0.tgz",
"integrity": "sha1-6yhAdG6dxIvV4GOjbj/UAMXqtak=",
"version": "5.5.2",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-5.5.2.tgz",
"integrity": "sha1-c7Xuyj+rZT49P5Qis0GtQiBdyWU=",
"dev": true,
"requires": {
"co": "4.6.0",
@ -1225,12 +1225,6 @@
"integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=",
"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": {
"version": "0.0.1",
"resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz",
@ -1543,12 +1537,6 @@
"integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=",
"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": {
"version": "0.1.3",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz",
@ -1669,12 +1657,12 @@
}
},
"errno": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/errno/-/errno-0.1.4.tgz",
"integrity": "sha1-uJbiOp5ei6M4cfyZar02NfyaHH0=",
"version": "0.1.6",
"resolved": "https://registry.npmjs.org/errno/-/errno-0.1.6.tgz",
"integrity": "sha512-IsORQDpaaSwcDP4ZZnHxgE85werpo34VYn1Ud3mq+eUsF593faR8oCZNXrROVkpFu2TsbrNhHin0aUrTsQ9vNw==",
"dev": true,
"requires": {
"prr": "0.0.0"
"prr": "1.0.1"
}
},
"error-ex": {
@ -1783,12 +1771,12 @@
}
},
"eslint": {
"version": "4.12.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-4.12.1.tgz",
"integrity": "sha512-28hOYej+NZ/R5H1yMvyKa1+bPlu+fnsIAQffK6hxXgvmXnImos2bA5XfCn5dYv2k2mrKj+/U/Z4L5ICWxC7TQw==",
"version": "4.13.1",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-4.13.1.tgz",
"integrity": "sha512-UCJVV50RtLHYzBp1DZ8CMPtRSg4iVZvjgO9IJHIKyWU/AnJVjtdRikoUPLB29n5pzMB7TnsLQWf0V6VUJfoPfw==",
"dev": true,
"requires": {
"ajv": "5.5.0",
"ajv": "5.5.2",
"babel-code-frame": "6.26.0",
"chalk": "2.3.0",
"concat-stream": "1.6.0",
@ -1803,11 +1791,11 @@
"file-entry-cache": "2.0.0",
"functional-red-black-tree": "1.0.1",
"glob": "7.1.2",
"globals": "11.0.1",
"globals": "11.1.0",
"ignore": "3.3.7",
"imurmurhash": "0.1.4",
"inquirer": "3.3.0",
"is-resolvable": "1.0.0",
"is-resolvable": "1.0.1",
"js-yaml": "3.10.0",
"json-stable-stringify-without-jsonify": "1.0.1",
"levn": "0.3.0",
@ -1869,9 +1857,9 @@
"dev": true
},
"globals": {
"version": "11.0.1",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.0.1.tgz",
"integrity": "sha1-Eqh7sBDlFUOWrMU14eQ/x1Ow5eg=",
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/globals/-/globals-11.1.0.tgz",
"integrity": "sha512-uEuWt9mqTlPDwSqi+sHjD4nWU/1N+q0fiWI9T1mZpD2UENqX20CFD5T/ziLZvztPaBKl7ZylUi1q6Qfm7E2CiQ==",
"dev": true
},
"has-flag": {
@ -2200,12 +2188,6 @@
"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": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz",
@ -3679,13 +3661,10 @@
"dev": true
},
"is-resolvable": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.0.0.tgz",
"integrity": "sha1-jfV8YeouPFAUCNEA+wE8+NbgzGI=",
"dev": true,
"requires": {
"tryit": "1.0.3"
}
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.0.1.tgz",
"integrity": "sha512-y5CXYbzvB3jTnWAZH1Nl7ykUWb6T3BcTs56HUruwBf8MhF56n1HWqhDWnVFo8GHrUPDgvUUNVhrc2U8W7iqz5g==",
"dev": true
},
"is-stream": {
"version": "1.1.0",
@ -3729,12 +3708,6 @@
"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": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/js-base64/-/js-base64-2.4.0.tgz",
@ -3981,21 +3954,6 @@
"integrity": "sha1-3oGf282E3M2PrlnGrreWFbnSZqw=",
"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": {
"version": "1.3.4",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.4.tgz",
@ -4033,7 +3991,7 @@
"integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=",
"dev": true,
"requires": {
"errno": "0.1.4",
"errno": "0.1.6",
"readable-stream": "2.3.3"
}
},
@ -6959,14 +6917,67 @@
"dev": true
},
"postcss-sass": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/postcss-sass/-/postcss-sass-0.1.0.tgz",
"integrity": "sha1-DSplW10kHsj0Gbs9o43lyhF0bds=",
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/postcss-sass/-/postcss-sass-0.2.0.tgz",
"integrity": "sha512-cUmYzkP747fPCQE6d+CH2l1L4VSyIlAzZsok3HPjb5Gzsq3jE+VjpAdGlPsnQ310WKWI42sw+ar0UNN59/f3hg==",
"dev": true,
"requires": {
"gonzales-pe": "4.2.3",
"mathjs": "3.17.0",
"postcss": "5.2.18"
"postcss": "6.0.14"
},
"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": {
@ -7184,16 +7195,16 @@
}
},
"postcss-smart-import": {
"version": "0.7.5",
"resolved": "https://registry.npmjs.org/postcss-smart-import/-/postcss-smart-import-0.7.5.tgz",
"integrity": "sha512-Bs9wAFxH5irGpenBg9a65jTcydZLh7VBTX6NYwMXvVXO6y9CQ83kRmfpQDs5lHhl6IODeIeQfJep5HBMd9UVCQ==",
"version": "0.7.6",
"resolved": "https://registry.npmjs.org/postcss-smart-import/-/postcss-smart-import-0.7.6.tgz",
"integrity": "sha512-9OpXaQ1uMMHWafUh0RWIpAKa3xxUDC2yyxicUPpGffH33nzbZG4/z+nk5Ocw5gGZ+3qkXV91iDV23Cmxf2Jhew==",
"dev": true,
"requires": {
"babel-runtime": "6.26.0",
"lodash": "4.17.4",
"object-assign": "4.1.1",
"postcss": "6.0.14",
"postcss-sass": "0.1.0",
"postcss-sass": "0.2.0",
"postcss-scss": "1.0.2",
"postcss-value-parser": "3.3.0",
"promise-each": "2.2.0",
@ -7348,9 +7359,9 @@
}
},
"prr": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/prr/-/prr-0.0.0.tgz",
"integrity": "sha1-GoS4WQgyVQFBGFPQCB7j+obikmo=",
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz",
"integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=",
"dev": true
},
"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": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.4.1.tgz",
@ -8147,7 +8152,7 @@
"integrity": "sha512-UUkEAPdSGxtRpiV9ozJ5cMTtYiqz7Ni1OGqLXRCynrvzdtR1p+cfOWe2RJLwvUG8hNanaSRjecIqwOjqeatDsA==",
"dev": true,
"requires": {
"ajv": "5.5.0",
"ajv": "5.5.2",
"ajv-keywords": "2.1.1",
"chalk": "2.3.0",
"lodash": "4.17.4",
@ -8219,12 +8224,6 @@
"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": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
@ -8252,12 +8251,6 @@
"integrity": "sha1-yy4SAwZ+DI3h9hQJS5/kVwTqYAM=",
"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": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz",
@ -8273,12 +8266,6 @@
"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": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
@ -8443,14 +8430,14 @@
}
},
"webpack": {
"version": "3.9.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-3.9.0.tgz",
"integrity": "sha512-SlBO3yUIhSohW7uCA5c0v03V32DsaXU3vDyUtHB8rubgTgfwl1nv+I+BQIScuQ6exu74wWT6brF/GDXxGLStuA==",
"version": "3.10.0",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-3.10.0.tgz",
"integrity": "sha512-fxxKXoicjdXNUMY7LIdY89tkJJJ0m1Oo8PQutZ5rLgWbV5QVKI15Cn7+/IHnRTd3vfKfiwBx6SBqlorAuNA8LA==",
"dev": true,
"requires": {
"acorn": "5.2.1",
"acorn-dynamic-import": "2.0.2",
"ajv": "5.5.0",
"ajv": "5.5.2",
"ajv-keywords": "2.1.1",
"async": "2.6.0",
"enhanced-resolve": "3.4.1",

View file

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

View file

@ -45,15 +45,30 @@
/**
* @typedef {Object} EditorConfig
* @property {String} holderId - Element to append Editor
* @property {String} initialBlock - Tool name which will be initial
* @property {Object} tools - list of tools. The object value must be function (constructor) so that CodexEditor could make an instance
* @property {@link Tools#ToolsConfig} toolsConfig - tools configuration
* @property {Array} data - Blocks list in JSON-format
* @property {String} holderId - Element to append Editor
* @property {Array} data - Blocks list in JSON-format
* @property {Object} tools - Map for used Tools in format { name : Class, ... }
* @property {String} initialBlock - This Tool will be added by default
* @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';
/**
* Apply polyfills
*/
import 'components/polyfills';
/**
* Require Editor modules places in components/modules dir
*/
@ -87,11 +102,19 @@ module.exports = class CodexEditor {
/**
* Configuration object
* @type {EditorConfig}
*/
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 = {};
@ -110,7 +133,7 @@ module.exports = class CodexEditor {
})
.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
* @param {Object} config
* @param {EditorConfig} config
*/
set configuration(config = {}) {
set configuration(config) {
this.config.holderId = config.holderId;
this.config.placeholder = config.placeholder || 'write your story...';
@ -135,11 +158,24 @@ module.exports = class CodexEditor {
this.config.toolsConfig = config.toolsConfig || {};
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 {{}|*}
* @returns {EditorConfig}
*/
get configuration() {
@ -182,7 +218,6 @@ module.exports = class CodexEditor {
* To prevent this, we use 'babel-plugin-class-display-name' plugin
* @see https://www.npmjs.com/package/babel-plugin-class-display-name
*/
this.moduleInstances[Module.displayName] = new Module({
config : this.configuration
});
@ -250,8 +285,8 @@ module.exports = class CodexEditor {
let prepareDecorator = module => module.prepare();
return Promise.resolve()
.then(prepareDecorator(this.moduleInstances.UI))
.then(prepareDecorator(this.moduleInstances.Tools))
.then(prepareDecorator(this.moduleInstances.UI))
.then(() => {
if (this.config.data && this.config.data.items) {
@ -307,11 +342,11 @@ module.exports = class CodexEditor {
// * holds initial 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',
//
// // Type of block showing on empty editor
// initialBlockPlugin: 'paragraph'
// initialBlockPlugin: 'text'
// };
//
// /**

View file

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

View file

@ -20,15 +20,24 @@ export default class Block {
this.tool = tool;
this.CSS = {
wrapper: 'ce-block',
content: 'ce-block__content'
};
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
*
@ -37,8 +46,8 @@ export default class Block {
*/
compose() {
let wrapper = $.make('div', this.CSS.wrapper),
content = $.make('div', this.CSS.content);
let wrapper = $.make('div', Block.CSS.wrapper),
content = $.make('div', Block.CSS.content);
content.appendChild(this.tool.html);
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

View file

@ -92,7 +92,7 @@ export default class Dom {
* @param {Object} node
* @returns {boolean}
*/
static isNode(node) {
static isElement(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) {
if (!$.isNode(node)) {
if (!$.isElement(node)) {
node = node.parentNode;

View file

@ -7,15 +7,20 @@
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 {
/**
* @constructor
* @param {EditorConfig} config
*/
constructor(config) {
constructor({config}) {
super(config);
super({config});
/**
* Proxy for Blocks instance {@link Blocks}
@ -78,13 +83,17 @@ export default class BlockManager extends Module {
* @param {String} toolName plugin name
* @param {Object} data plugin data
*/
insert(toolName, data) {
insert(toolName, data = {}) {
let toolInstance = this.Editor.Tools.construct(toolName, data),
block = new Block(toolInstance);
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 {Object} data plugin data
*/
replace(toolName, data) {
replace(toolName, data = {}) {
let toolInstance = this.Editor.Tools.construct(toolName, data),
block = new Block(toolInstance);
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;
/**
* Update current Block's index
* @type {number}
*/
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(config) {
constructor({config}) {
super(config);
super({config});
this.subscribers = {};
}

View file

@ -12,9 +12,9 @@ export default class Renderer extends Module {
* @constructor
* @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
*/
constructor(config) {
constructor({config}) {
super(config);
super({config});
// default config
this.defaultConfig = null;

View file

@ -54,9 +54,9 @@ export default class Toolbar extends Module {
/**
* @constructor
*/
constructor(config) {
constructor({config}) {
super(config);
super({config});
this.nodes = {
wrapper : null,
@ -65,7 +65,6 @@ export default class Toolbar extends Module {
// Content Zone
plusButton : null,
toolbox : null,
// Actions Zone
settingsToggler : null,
@ -77,14 +76,25 @@ export default class Toolbar extends Module {
defaultSettings: null,
};
this.CSS = {
}
/**
* CSS styles
* @return {Object}
* @constructor
*/
static get CSS() {
return {
toolbar: 'ce-toolbar',
content: 'ce-toolbar__content',
actions: 'ce-toolbar__actions',
toolbarOpened: 'ce-toolbar--opened',
// Content Zone
toolbox: 'ce-toolbar__toolbox',
plusButton: 'ce-toolbar__plus',
plusButtonHidden: 'ce-toolbar__plus--hidden',
// Actions Zone
settingsToggler: 'ce-toolbar__settings-btn',
@ -103,14 +113,14 @@ export default class Toolbar extends Module {
*/
make() {
this.nodes.wrapper = $.make('div', this.CSS.toolbar);
this.nodes.wrapper = $.make('div', Toolbar.CSS.toolbar);
/**
* Make Content Zone and Actions Zone
*/
['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]);
});
@ -121,12 +131,15 @@ export default class Toolbar extends Module {
* - Plus Button
* - 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:
@ -134,7 +147,7 @@ export default class Toolbar extends Module {
* - Remove Block Button
* - Settings Panel
*/
this.nodes.settingsToggler = $.make('span', this.CSS.settingsToggler);
this.nodes.settingsToggler = $.make('span', Toolbar.CSS.settingsToggler);
this.nodes.removeBlockButton = this.makeRemoveBlockButton();
$.append(this.nodes.actions, [this.nodes.settingsToggler, this.nodes.removeBlockButton]);
@ -158,10 +171,10 @@ export default class Toolbar extends Module {
*/
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.defaultSettings = $.make('div', this.CSS.defaultSettings);
this.nodes.pluginSettings = $.make('div', Toolbar.CSS.pluginSettings);
this.nodes.defaultSettings = $.make('div', Toolbar.CSS.defaultSettings);
$.append(this.nodes.settings, [this.nodes.pluginSettings, this.nodes.defaultSettings]);
$.append(this.nodes.actions, this.nodes.settings);
@ -178,7 +191,83 @@ export default class Toolbar extends Module {
* @todo add confirmation panel and handlers
* @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
* Tools must contain the following important objects:
* Each Tool 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 {Boolean} displayInToolbox - will be displayed in toolbox. Default value is TRUE
* @property {Boolean} enableLineBreaks - inserts new block or break lines. Default value is FALSE
@ -19,15 +18,28 @@
*/
/**
* @typedef {Tool} Tool
* @property {String} name - name of this module
* @property {Object[]} toolInstances - list of tool instances
* @property {Tools[]} available - available Tools
* @property {Tools[]} unavailable - unavailable Tools
* @typedef {Function} Tool {@link docs/tools.md}
* @property {Boolean} displayInToolbox - By default, tools won't be added in the Toolbox. Pass true to add.
* @property {String} iconClassName - CSS class name for the Toolbox button
* @property {Boolean} irreplaceable - Toolbox behaviour: replace or add new block below
* @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 {EditorConfig} config - Editor config
*/
export default class Tools extends Module {
/**
@ -51,15 +63,18 @@ export default class Tools extends Module {
}
/**
* If config wasn't passed by user
* @return {ToolsConfig}
* Static getter for default Tool config fields
*
* @usage Tools.defaultConfig.displayInToolbox
* @return {ToolConfig}
*/
get defaultConfig() {
static get defaultConfig() {
return {
iconClassName : 'default-icon',
iconClassName : '',
displayInToolbox : false,
enableLineBreaks : false
enableLineBreaks : false,
irreplaceable : false
};
}
@ -67,21 +82,38 @@ export default class Tools extends Module {
/**
* @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 = {};
/**
* Available tools list
* {name: Class, ...}
* @type {Object}
*/
this.toolsAvailable = {};
/**
* Tools that rejected a prepare method
* {name: Class, ... }
* @type {Object}
*/
this.toolsUnavailable = {};
}
/**
* Creates instances via passed or default configuration
* @return {boolean}
* @return {Promise}
*/
prepare() {
@ -128,7 +160,7 @@ export default class Tools extends Module {
/**
* 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() {
@ -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
*
@ -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'
// };
let CSS = {
editorWrapper : 'codex-editor',
editorZone : 'ce-redactor'
};
import Block from '../block';
/**
* @class
@ -54,7 +51,6 @@ let CSS = {
* @property {Element} nodes.wrapper - <codex-editor>
* @property {Element} nodes.redactor - <ce-redactor>
*/
export default class UI extends Module {
/**
@ -62,9 +58,9 @@ export default class UI extends Module {
*
* @param {EditorConfig} config
*/
constructor(config) {
constructor({config}) {
super(config);
super({config});
this.nodes = {
holder: null,
@ -81,45 +77,19 @@ export default class UI extends Module {
*/
prepare() {
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', CSS.editorWrapper);
this.nodes.redactor = $.make('div', CSS.editorZone);
this.nodes.wrapper.appendChild(this.nodes.redactor);
this.nodes.holder.appendChild(this.nodes.wrapper);
return this.make()
/**
* Make toolbar
*/
this.Editor.Toolbar.make();
.then(() => this.Editor.Toolbar.make())
/**
* Load and append CSS
*/
this.loadStyles();
resolve();
})
/** Add toolbox tools */
// .then(addTools_)
.then(() => this.loadStyles())
/**
* Bind events for the UI elements
*/
.then(() => this.bindEvents())
/** Make container for inline toolbar */
// .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() {
/**
@ -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 () {
//

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 {
/**
* 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
* @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('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
*/
.codex-editor{
.codex-editor {
position: relative;
border: 1px solid #ccc;
padding: 10px;
box-sizing: border-box;
.hide {
display: none;
}
&__redactor {
padding-bottom: 300px;
}
}

View file

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