CodeX Editor 2.0

Co-authored-by: Murod Khaydarov <murod.haydarov@gmail.com>
Co-authored-by: Petr Savchenko <specc.dev@gmail.com>
Co-authored-by: George Berezhnoy <gohabereg@gmail.com>
Co-authored-by: Taly Guryn <vitalik7tv@yandex.ru>
This commit is contained in:
Murod Khaydarov 2018-07-23 10:38:46 +03:00 committed by Taly
parent b9776c9c97
commit 48ac2271f9
186 changed files with 48405 additions and 11915 deletions

9
.editorconfig Normal file
View file

@ -0,0 +1,9 @@
root = false
[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true

View file

@ -1,7 +1,8 @@
{
/** Enable ES6 features */
"parserOptions": {
"ecmaVersion": 6,
"ecmaVersion": 2017,
"sourceType": "module"
},
"rules": {
@ -18,7 +19,7 @@
"no-shadow": 2,
"no-undef-init": 2,
"no-undef": 2,
"no-unused-vars": 1,
"no-unused-vars": 0,
/** Style */
"array-bracket-spacing": [2, "never", {
@ -26,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, {
@ -38,7 +42,7 @@
"no-nested-ternary": 1,
"no-trailing-spaces": 2,
"no-mixed-spaces-and-tabs": 2,
"padded-blocks": [2, "always"],
"padded-blocks": [2, "never"],
"space-before-blocks": 1,
"space-before-function-paren": [1, {
"anonymous": "always",
@ -49,7 +53,7 @@
"markers": ["=", "!"]
}],
"semi": [2, "always"],
"indent": [2, 4, {
"indent": [2, 2, {
"SwitchCase": 1
}],
"camelcase": [2, {
@ -71,7 +75,14 @@
"FormData": true,
"XMLHttpRequest": true,
"ActiveXObject": true,
"RegExp": true
"RegExp": true,
"Module": true,
"Node": true,
"Element": true,
"Proxy": true,
"Symbol": true,
"$": true,
"_": true,
"setTimeout": true
}
}
}

2
.gitignore vendored
View file

@ -10,3 +10,5 @@ node_modules/*
/uploads/
plugins/personality/
npm-debug.log

View file

@ -12,11 +12,15 @@
// Define globals exposed by CodeX Team
"predef": [
"codex"
"codex",
"_",
"$",
"editorModules",
"Module"
],
// Allow ES6.
"esversion": 6,
"esversion": 2017,
/*
* ENFORCING OPTIONS

16
.postcssrc Normal file
View file

@ -0,0 +1,16 @@
plugins:
postcss-smart-import: {}
postcss-custom-properties: {}
postcss-apply: {}
postcss-custom-media: {}
postcss-media-minmax: {}
postcss-custom-selectors: {}
postcss-nested-ancestors: {}
postcss-nesting: {}
postcss-nested: {}
postcss-color-mod-function: {}
postcss-color-hex-alpha: {}
postcss-font-variant: {}
postcss-font-family-system-ui: {}
autoprefixer:
browsers: ['last 2 versions', '> 1%']

View file

@ -1,12 +1,17 @@
{
"rules": {
"at-rule-empty-line-before": [ "always", {
except: [
"blockless-after-same-name-blockless",
"first-nested",
],
ignore: ["after-comment"],
} ],
"at-rule-empty-line-before": [
"always",
{
except: [
"blockless-after-same-name-blockless",
"first-nested",
],
ignore: [
"after-comment"
],
}
],
"at-rule-name-case": "lower",
"at-rule-name-space-after": "always-single-line",
"at-rule-semicolon-newline-after": "always",
@ -21,27 +26,42 @@
"color-hex-case": "lower",
"color-hex-length": "short",
"color-no-invalid-hex": true,
"comment-empty-line-before": [ "always", {
except: ["first-nested"],
ignore: ["stylelint-commands"],
} ],
"comment-empty-line-before": [
"always",
{
except: [
"first-nested"
],
ignore: [
"stylelint-commands"
],
}
],
"comment-no-empty": true,
"comment-whitespace-inside": "always",
"custom-property-empty-line-before": [ "always", {
except: [
"after-custom-property",
"first-nested",
],
ignore: [
"after-comment",
"inside-single-line-block",
],
} ],
"custom-property-empty-line-before": [
"always",
{
except: [
"after-custom-property",
"first-nested",
],
ignore: [
"after-comment",
"inside-single-line-block",
],
}
],
"declaration-bang-space-after": "never",
"declaration-bang-space-before": "always",
"declaration-block-no-duplicate-properties": [ true, {
ignore: ["consecutive-duplicates-with-different-values"],
} ],
"declaration-block-no-duplicate-properties": [
true,
{
ignore: [
"consecutive-duplicates-with-different-values"
],
}
],
"declaration-block-no-redundant-longhand-properties": true,
"declaration-block-no-shorthand-property-overrides": true,
"declaration-block-semicolon-newline-after": "always-multi-line",
@ -52,16 +72,19 @@
"declaration-colon-newline-after": "always-multi-line",
"declaration-colon-space-after": "always-single-line",
"declaration-colon-space-before": "never",
"declaration-empty-line-before": [ "always", {
except: [
"after-declaration",
"first-nested",
],
ignore: [
"after-comment",
"inside-single-line-block",
],
} ],
"declaration-empty-line-before": [
"always",
{
except: [
"after-declaration",
"first-nested",
],
ignore: [
"after-comment",
"inside-single-line-block",
],
}
],
"font-family-no-duplicate-names": true,
"function-calc-no-unspaced-operator": true,
"function-comma-newline-after": "always-multi-line",
@ -96,13 +119,17 @@
"number-no-trailing-zeros": true,
"property-case": "lower",
"property-no-unknown": true,
"rule-nested-empty-line-before": [ "always-multi-line", {
except: ["first-nested"],
ignore: ["after-comment"],
} ],
"rule-non-nested-empty-line-before": [ "always-multi-line", {
ignore: ["after-comment"],
} ],
"rule-empty-line-before": [
"always-multi-line",
{
except: [
"first-nested"
],
ignore: [
"after-comment"
],
}
],
"selector-attribute-brackets-space-inside": "never",
"selector-attribute-operator-space-after": "never",
"selector-attribute-operator-space-before": "never",
@ -129,4 +156,4 @@
"value-list-comma-space-before": "never",
"value-list-max-empty-lines": 0,
},
}
}

23770
build/codex-editor.js Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

42
build/sprite.svg Normal file
View file

@ -0,0 +1,42 @@
<?xml version="1.0" encoding="utf-8"?>
<svg xmlns="http://www.w3.org/2000/svg">
<symbol id="arrow-down" viewBox="0 0 14 14">
<path transform="matrix(1 0 0 -1 0 14)" d="M8.024 4.1v8.6a1.125 1.125 0 0 1-2.25 0V4.1L2.18 7.695A1.125 1.125 0 1 1 .59 6.104L6.103.588c.44-.439 1.151-.439 1.59 0l5.516 5.516a1.125 1.125 0 0 1-1.59 1.59L8.023 4.1z"/>
</symbol>
<symbol id="arrow-up" viewBox="0 0 14 14">
<path d="M8.024 4.1v8.6a1.125 1.125 0 0 1-2.25 0V4.1L2.18 7.695A1.125 1.125 0 1 1 .59 6.104L6.103.588c.44-.439 1.151-.439 1.59 0l5.516 5.516a1.125 1.125 0 0 1-1.59 1.59L8.023 4.1z"/>
</symbol>
<symbol id="bold" viewBox="0 0 13 15">
<path d="M5.996 13.9H1.752c-.613 0-1.05-.137-1.312-.412-.262-.275-.393-.712-.393-1.312V1.737C.047 1.125.18.684.449.416.718.147 1.152.013 1.752.013h4.5a10.5 10.5 0 0 1 1.723.123c.487.082.922.24 1.308.474a3.43 3.43 0 0 1 1.449 1.738c.132.363.199.747.199 1.151 0 1.39-.695 2.406-2.084 3.05 1.825.581 2.737 1.712 2.737 3.391 0 .777-.199 1.477-.596 2.099a3.581 3.581 0 0 1-1.61 1.378c-.424.177-.91.301-1.46.374-.549.073-1.19.109-1.922.109zm-.209-6.167H2.86v4.055h3.022c1.9 0 2.851-.686 2.851-2.056 0-.7-.246-1.21-.739-1.525-.492-.316-1.228-.474-2.207-.474zM2.86 2.125v3.59h2.577c.7 0 1.242-.066 1.624-.198a1.55 1.55 0 0 0 .876-.758c.158-.265.237-.562.237-.89 0-.702-.25-1.167-.748-1.398-.499-.23-1.26-.346-2.283-.346H2.86z"/>
</symbol>
<symbol id="cross" viewBox="0 0 237 237">
<path transform="rotate(45 280.675 51.325)" d="M191 191V73c0-5.523 4.477-10 10-10h25c5.523 0 10 4.477 10 10v118h118c5.523 0 10 4.477 10 10v25c0 5.523-4.477 10-10 10H236v118c0 5.523-4.477 10-10 10h-25c-5.523 0-10-4.477-10-10V236H73c-5.523 0-10-4.477-10-10v-25c0-5.523 4.477-10 10-10h118z"/>
</symbol>
<symbol id="dots" viewBox="0 0 18 4">
<g fill-rule="evenodd">
<circle cx="9" cy="2" r="2"/>
<circle cx="2" cy="2" r="2"/>
<circle cx="16" cy="2" r="2"/>
</g>
</symbol>
<symbol id="italic" viewBox="0 0 6 15">
<path d="M4 5.2l-1.368 7.474c-.095.518-.29.91-.585 1.175a1.468 1.468 0 0 1-1.01.398c-.379 0-.662-.136-.85-.407-.186-.272-.234-.66-.141-1.166L1.4 5.276c.093-.511.282-.896.567-1.155a1.43 1.43 0 0 1 .994-.389c.38 0 .668.13.867.389.199.259.256.618.172 1.08zm-.79-2.67c-.36 0-.648-.111-.863-.332-.215-.221-.286-.534-.212-.938.067-.366.253-.668.559-.905A1.57 1.57 0 0 1 3.673 0c.334 0 .612.107.831.322.22.215.292.527.217.938-.073.398-.256.709-.55.933a1.55 1.55 0 0 1-.961.336z"/>
</symbol>
<symbol id="link" viewBox="0 0 15 14">
<path transform="rotate(-45 11.83 6.678)" d="M11.332 4.013a51.07 51.07 0 0 1-2.28.001A1.402 1.402 0 0 0 7.7 2.25H3.65a1.4 1.4 0 1 0 0 2.8h.848c.206.86.693 1.61 1.463 2.25H3.65a3.65 3.65 0 1 1 0-7.3H7.7a3.65 3.65 0 0 1 3.632 4.013zM10.9 0h2a3.65 3.65 0 0 1 0 7.3H8.85a3.65 3.65 0 0 1-3.632-4.011A62.68 62.68 0 0 1 7.5 3.273 1.401 1.401 0 0 0 8.85 5.05h4.05a1.4 1.4 0 0 0 0-2.8h-.48C12.274 1.664 11.694.785 10.9 0z"/>
</symbol>
<symbol id="plus" viewBox="0 0 14 14">
<path d="M8.05 5.8h4.625a1.125 1.125 0 0 1 0 2.25H8.05v4.625a1.125 1.125 0 0 1-2.25 0V8.05H1.125a1.125 1.125 0 0 1 0-2.25H5.8V1.125a1.125 1.125 0 0 1 2.25 0V5.8z"/>
</symbol>
<symbol id="unlink" viewBox="0 0 16 18">
<path transform="rotate(-45 8.358 11.636)" d="M9.14 9.433c.008-.12-.087-.686-.112-.81a1.4 1.4 0 0 0-1.64-1.106l-3.977.772a1.4 1.4 0 0 0 .535 2.749l.935-.162s.019 1.093.592 2.223l-1.098.148A3.65 3.65 0 1 1 2.982 6.08l3.976-.773c1.979-.385 3.838.919 4.28 2.886.51 2.276-1.084 2.816-1.073 2.935.011.12-.394-1.59-1.026-1.696zm3.563-.875l2.105 3.439a3.65 3.65 0 0 1-6.19 3.868L6.47 12.431c-1.068-1.71-.964-2.295-.49-3.07.067-.107 1.16-1.466 1.48-.936-.12.036.9 1.33.789 1.398-.656.41-.28.76.13 1.415l2.145 3.435a1.4 1.4 0 0 0 2.375-1.484l-1.132-1.941c.42-.435 1.237-1.054.935-2.69zm1.88-2.256h3.4a1.125 1.125 0 0 1 0 2.25h-3.4a1.125 1.125 0 0 1 0-2.25zM11.849.038c.62 0 1.125.503 1.125 1.125v3.4a1.125 1.125 0 0 1-2.25 0v-3.4c0-.622.503-1.125 1.125-1.125z"/>
</symbol></svg>

After

Width:  |  Height:  |  Size: 3.8 KiB

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

146
codex.js
View file

@ -1,146 +0,0 @@
/**
*
* Codex Editor
*
* @author Codex Team
*/
module.exports = (function (editor) {
'use strict';
editor.version = VERSION;
editor.scriptPrefix = 'cdx-script-';
var init = function () {
editor.core = require('./modules/core');
editor.tools = require('./modules/tools');
editor.ui = require('./modules/ui');
editor.transport = require('./modules/transport');
editor.renderer = require('./modules/renderer');
editor.saver = require('./modules/saver');
editor.content = require('./modules/content');
editor.toolbar = require('./modules/toolbar/toolbar');
editor.callback = require('./modules/callbacks');
editor.draw = require('./modules/draw');
editor.caret = require('./modules/caret');
editor.notifications = require('./modules/notifications');
editor.parser = require('./modules/parser');
editor.sanitizer = require('./modules/sanitizer');
editor.listeners = require('./modules/listeners');
editor.destroyer = require('./modules/destroyer');
editor.paste = require('./modules/paste');
};
/**
* @public
* holds initial settings
*/
editor.settings = {
tools : ['paragraph', 'header', 'picture', 'list', 'quote', 'code', 'twitter', 'instagram', 'smile'],
holderId : 'codex-editor',
// Type of block showing on empty editor
initialBlockPlugin: 'paragraph'
};
/**
* public
*
* Static nodes
*/
editor.nodes = {
holder : null,
wrapper : null,
toolbar : null,
inlineToolbar : {
wrapper : null,
buttons : null,
actions : null
},
toolbox : null,
notifications : null,
plusButton : null,
showSettingsButton: null,
showTrashButton : null,
blockSettings : null,
pluginSettings : null,
defaultSettings : null,
toolbarButtons : {}, // { type : DomEl, ... }
redactor : null
};
/**
* @public
*
* Output state
*/
editor.state = {
jsonOutput : [],
blocks : [],
inputs : []
};
/**
* @public
* Editor plugins
*/
editor.tools = {};
/**
* Initialization
* @uses Promise cEditor.core.prepare
* @param {Object} userSettings
* @param {Array} userSettings.tools list of plugins
* @param {String} userSettings.holderId Element's id to append editor
*
* Load user defined tools
* Tools must contain this important objects :
* @param {String} type - this is a type of plugin. It can be used as plugin name
* @param {String} iconClassname - this a icon in toolbar
* @param {Object} make - what should plugin do, when it is clicked
* @param {Object} appendCallback - callback after clicking
* @param {Element} settings - what settings does it have
* @param {Object} render - plugin get JSON, and should return HTML
* @param {Object} save - plugin gets HTML content, returns JSON
* @param {Boolean} displayInToolbox - will be displayed in toolbox. Default value is TRUE
* @param {Boolean} enableLineBreaks - inserts new block or break lines. Default value is FALSE
*
* @example
* - type : 'header',
* - iconClassname : 'ce-icon-header',
* - make : headerTool.make,
* - appendCallback : headerTool.appendCallback,
* - settings : headerTool.makeSettings(),
* - render : headerTool.render,
* - save : headerTool.save,
* - displayInToolbox : true,
* - enableLineBreaks : false
*/
editor.start = function (userSettings) {
init();
editor.core.prepare(userSettings)
// If all ok, make UI, bind events and parse initial-content
.then(editor.ui.prepare)
.then(editor.tools.prepare)
.then(editor.sanitizer.prepare)
.then(editor.paste.prepare)
.then(editor.transport.prepare)
.then(editor.renderer.makeBlocksFromData)
.then(editor.ui.saveInputs)
.catch(function (error) {
editor.core.log('Initialization failed with error: %o', 'warn', error);
});
};
return editor;
})({});

76
docs/api.md Normal file
View file

@ -0,0 +1,76 @@
# CodeX Editor API
Blocks have access to the public methods provided by CodeX Editor API Module. Plugin and Tune Developers
can use Editor API as they want.
## Api object description
Common API interface.
```js
export interface IAPI {
blocks: IBlocksAPI;
caret: ICaretAPI;
sanitizer: ISanitizerAPI;
toolbar: IToolbarAPI;
}
```
#### IBlocksAPI
Methods that working with Blocks
```swap(fromIndex, toIndex)``` - swaps two Blocks by their positions
```delete(blockIndex?: Number)``` - deletes Block with passed index
```getCurrentBlockIndex()``` - current Block index
```getBlockByIndex(index: Number)``` - returns Block with passed index
```getBlocksCount()``` - returns Blocks count
#### ISanitizerAPI
```clean(taintString, config)``` - method uses HTMLJanitor to clean taint string.
CodeX Editor provides basic config without attributes, but you can inherit by passing your own config.
Usage:
```js
let taintString = '<div><p style="font-size: 5em;"><b></b>BlockWithText<a onclick="void(0)"></div>'
let customConfig = {
b: true,
p: {
style: true,
},
}
this.api.sanitizer.clean(taintString, customConfig);
```
### IToolbarAPI
Methods that working with Toolbar
```open()``` - Opens toolbar
```close()``` - Closes toolbar, toolbox and blockSettings if they are opened
### IEventsAPI
Methods that allows to subscribe on CodeX Editor events
```on(eventName: string, callback: Function)``` - subscribe callback on event
```off(eventName: string, callback: Function)``` - unsubscribe callback from event
```emit(eventName: string, data: object)``` - fires all subscribed callbacks with passed data
### IListenerAPI
Methods that allows to work with DOM listener. Useful when you forgot to remove listener.
Module collects all listeners and destroys automatically
```on(element: HTMLElement, eventType: string, handler: Function, useCapture?: boolean)``` - add event listener to HTML element
```off(element: HTMLElement, eventType: string, handler: Function)``` - remove event handler from HTML element

37
docs/caret.md Normal file
View file

@ -0,0 +1,37 @@
# CodeX Editor Caret Module
The `Caret` module contains methods working with caret. Uses [Range](https://developer.mozilla.org/en-US/docs/Web/API/Range) methods to navigate caret
between blocks.
Caret class implements basic Module class that holds User configuration
and default CodeX Editor instances
## Properties
## Methods
### setToBlock
```javascript
Caret.setToBlock(block, offset, atEnd)
```
> Method gets Block instance and puts caret to the text node with offset
#### params
| Param | Type | Description|
| -------------|------ |:-------------:|
| block | Object | Block instance that BlockManager created|
| offset | Number | caret offset regarding to the text node (Default: 0)|
| atEnd | Boolean | puts caret at the end of last text node|
### setToTheLastBlock
```javascript
Caret.setToTheLastBlock()
```
> sets Caret at the end of last Block
If last block is not empty, inserts another empty Block which is passed as initial

50
docs/events.md Normal file
View file

@ -0,0 +1,50 @@
# CodeX Editor Events Module
Module allows Developers to subscribe on events or trigger own events
## Methods
### On
```javascript
Events.on(eventName, callback)
```
> Method subscribes callback on event. It will be called when CodeX Editor emits this event
#### params
| Param | Type | Description|
| -------------|------ |:-------------:|
| eventName | String | event name|
| callback | Function | event callback|
### Off
```javascript
Events.off(eventName, callback)
```
> Method unsubscribes callback on event
#### params
| Param | Type | Description|
| -------------|------ |:-------------:|
| eventName | String | event name|
| callback | Function | event callback|
### Emit
```javascript
Events.emit(eventName, data)
```
> Method emits data to all subscribed callbacks
#### params
| Param | Type | Description|
| -------------|------ |:-------------:|
| eventName | String | event name|
| data | Object | any data|

7
docs/renderer.md Normal file
View file

@ -0,0 +1,7 @@
# CodeX Editor Renderer
`Renderer` is a class that is responsible for rendering JSON data to HTML.
Inside it uses BlockManager to compose block's list bounded to the Tool instance.
## Methods

45
docs/sanitizer.md Normal file
View file

@ -0,0 +1,45 @@
# CodeX Editor Sanitizer Module
The `Sanitizer` module represents a set of methods that clears taint strings.
Uses lightweight npm package with simple API [html-janitor](https://www.npmjs.com/package/html-janitor)
Sanitizer class implements basic Module class that holds User configuration
and default CodeX Editor instances
You can read more about Module class [here]()
## Properties
Default Editor Sanitizer configuration according to the html-janitor API
```javascript
defaultConfig
```
Custom User configuration which passed on Editor initialization. Data type must be according to the html-janitor API
```javascript
sanitizerConfig
```
Property that holds an instance used in Module
```javascript
sanitizerInstance
```
## Methods
### clean
```javascript
clean(taintString, customConfig)
```
> Cleans up the passed taint string
#### params
| Param | Type | Description|
| -------------|------ |:-------------:|
| taintString | String | string that needs to be cleaned|
| customConfig | Object | Can be passed new config per usage (Default: uses default configuration)|

2
docs/saver.md Normal file
View file

@ -0,0 +1,2 @@
# CodeX Editor Saver Module

91
docs/toolbar-settings.md Normal file
View file

@ -0,0 +1,91 @@
# CodeX Editor Toolbar Block Settings Module
Toolbar Module has space for Block settings. Settings divided into:
- space for plugin's settings, that is described by «Plugin»'s Developer
- space for default settings. This option is also can be implemented and expanded
They difference between zones is that the first option is specified by plugin
and each Block can have different options, when second option is for every Block
regardless to the plugin's option.
### Let's look the examples:
«Plugin»'s Developers need to expand «renderSettings» method that returns HTML.
Every user action will be handled by itself. So, you can easily write
callbacks that switches your content or makes better. For more information
read [Tools](tools.md).
---
«Tune»'s Developers need to implement core-provided interface to develop
tunes that will be appeared in Toolbar default settings zone.
Tunes must expand two important methods:
- `render()` - returns HTML and it is appended to the default settings zone
- `save()` - extracts important information to be saved
No restrictions. Handle user action by yourself
Create Class that implements block-tune.ts
Your Tune's constructor gets argument as object and it includes:
- {Object} api - object contains public methods from modules. @see [API](api.md)
- {Object} settings - settings contains block default state.
This object could have information about cover, anchor and so on.
Example on TypeScript:
```js
import IBlockTune from './block-tune';
export default class YourCustomTune implements IBlockTune {
public constructor({api, settings}) {
this.api = api;
this.settings = settings;
}
render() {
let someHTML = '...';
return someHTML;
}
save() {
// Return the important data that needs to be saved
return object
}
someMethod() {
// moves current block down
this.api.blocks.moveDown();
}
}
```
Example on ES6
```js
export default class YourCustomTune {
constructor({api, settings}) {
this.api = api;
this.settings = settings;
}
render() {
let someHTML = '...';
return someHTML;
}
save() {
// Return the important data that needs to be saved
return object
}
someMethod() {
// moves current block down
this.api.blocks.moveDown();
}
}
```

105
docs/tools-inline.md Normal file
View file

@ -0,0 +1,105 @@
# Tools for the Inline Toolbar
Similar with [Tools](tools.md) represented Blocks, you can create Tools for the Inline Toolbar. It will work with
selected fragment of text. The simplest example is `bold` or `italic` Tools.
## Base structure
First of all, Tool's class should have a `isInline` property (static getter) set as `true`.
After that Inline Tool should implement next methods.
- `render()` — create a button
- `surround()` — works with selected range
- `checkState()` — get Tool's activated state by selected range
Also, you can provide optional methods
- `renderActions()` — create additional element below the buttons
- `clear()` — clear Tool's stuff on opening/closing of Inline Toolbar
- `shortcut()` — shortcut that handles Tool
At the constructor of Tool's class exemplar you will accept an object with the [API](api.md) as a parameter.
---
### render()
Method that returns button to append at the Inline Toolbar
#### Parameters
Method does not accept any parameters
#### Return value
type | description |
-- | -- |
`HTMLElement` | element that will be added to the Inline Toolbar |
---
### surround(range: Range)
Method that accepts selected range and wrap it somehow
#### Parameters
name | type | description |
-- |-- | -- |
range | Range | first range of current Selection |
#### Return value
There is no return value
---
### checkState(selection: Selection)
Get Selection and detect if Tool was applied. For example, after that Tool can highlight button or show some details.
#### Parameters
name | type | description |
-- |-- | -- |
selection | Selection | current Selection |
#### Return value
type | description |
-- | -- |
`Boolean` | `true` if Tool is active, otherwise `false` |
---
### renderActions()
Optional method that returns additional Element with actions.
For example, input for the 'link' tool or textarea for the 'comment' tool.
It will be places below the buttons list at Inline Toolbar.
#### Parameters
Method does not accept any parameters
#### Return value
type | description |
-- | -- |
`HTMLElement` | element that will be added to the Inline Toolbar |
---
### clear()
Optional method that will be called on opening/closing of Inline Toolbar.
Can contain logic for clearing Tool's stuff, such as inputs, states and other.
#### Parameters
Method does not accept any parameters
#### Return value
Method should not return a value.

144
docs/tools.md Normal file
View file

@ -0,0 +1,144 @@
# 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
### Render
### Save
### Validate
### Merge (optional)
Method that specifies how to merge two `Blocks` of the same type, for example on `Backspace` keypress.
Method does accept data object in same format as the `Render` and it should provide logic how to combine new
data with the currently stored value.
### Internal Tool Settings
Options that Tool can specify. All settings should be passed as static properties of Tool's class.
| 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` |
| `toolboxIcon` | _String_ | — | Tool's SVG icon for Toolbox |
| `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` |
| `isInline` | _Boolean_ | `false` | Describes Tool as a [Tool for the Inline Toolbar](tools-inline.md) |
### User configuration
All Tools can be configured by users. For this reason, we provide `toolConfig` option at the Editor Initial Settings.
Unlike Internal Tool Settings, this options can be specified outside the Tool class,
so users can set up different configurations for the same Tool.
```javascript
var editor = new CodexEditor({
holderId : 'codex-editor',
initialBlock : 'text',
tools: {
text: Text // 'Text' Tool class for Blocks with type 'text'
},
toolsConfig: {
text: { // user configuration for Blocks with type 'text'
inlineToolbar : true,
}
}
});
```
There are few options available by CodeX Editor.
| Name | Type | Default Value | Description |
| -- | -- | -- | -- |
| `enableLineBreaks` | _Boolean_ | `false` | With this option, CodeX Editor won't handle Enter keydowns. Can be helpful for Tools like `<code>` where line breaks should be handled by default behaviour. |
| `inlineToolbar` | _Boolean/Array_ | `false` | Pass `true` to enable the Inline Toolbar with all Tools, or pass an array with specified Tools list |
| `disallowPaste` | _Boolean_ | `false` | Pass `true` if you want to prevent any paste into your Tool
### Paste handling
CodeX Editor handles paste on Blocks and provides API for Tools to process the pasted data.
When user pastes content into Editor, pasted content is splitted into blocks.
1. If plain text has been pasted, it is split by new line characters
2. If HTML string has been pasted, it is split by block tags
Also Editor API allows you to define RegExp patterns to substitute them by your data.
To provide paste handling for your Tool you need to define static getter `onPaste` in Tool class.
`onPaste` getter should return object with fields described below.
##### HTML tags handling
To handle pasted HTML elements object returned from `onPaste` getter should contain following fields:
| Name | Type | Description |
| -- | -- | -- |
| `handler(content: HTMLElement)` | `Function` | _Optional_. Pasted HTML elements handler. Gets one argument `content`. `content` is HTML element extracted from pasted data. Handler should return the same object as Tool's `save` method |
| `tags` | `String[]` | _Optional_. Should contain all tag names you want to be extracted from pasted data and be passed to your `handler` method |
For correct work you MUST provide `onPaste.handler` at least for `initialBlock` Tool.
> Example
Header tool can handle `H1`-`H6` tags using paste handling API
```javascript
static get onPaste() {
return {
tags: ['H1', 'H2', 'H3', 'H4', 'H5', 'H6'],
handler: (element) => ({
type: element.tagName,
text: element.innerHTML
})
}
}
```
> One tag can be handled by one Tool only.
##### Patterns handling
Your Tool can analyze text by RegExp patterns to substitute pasted string with data you want. Object returned from `onPaste` getter should contain following fields to use patterns:
| Name | Type | Description |
| -- | -- | -- |
| `patterns` | `Object` | _Optional_. `patterns` object contains RegExp patterns with their names as object's keys |
| `patternHandler(text: string, key: string)` | `Function` | _Optional_. Gets pasted string and pattern name. Should return the same object as Tool `save` method |
Pattern will be processed only if paste was on `initialBlock` Tool and pasted string length is less than 450 characters.
> Example
You can handle youtube links and insert embeded video instead:
```javascript
static get onPaste() {
return {
patterns: {
youtube: /http(?:s?):\/\/(?:www\.)?youtu(?:be\.com\/watch\?v=|\.be\/)([\w\-\_]*)(&(amp;)?[\w\?=]*)?/
},
patternHandler: (text, key) => {
const urlData = Youtube.onPaste.patterns[key].exec(text);
return {
iframe: Youtube.makeEmbededFromURL(urlData)
};
}
}
}
```
> Both `onPaste.handler` and `onPaste.patternHandler` can be `async` or return a `Promise`.
### Sanitize

View file

@ -1,244 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CodeX Editor example</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
font-size: 14px;
line-height: 1.5em;
}
</style>
</head>
<body>
<div id="codex-editor"></div>
</body>
<script src="codex-editor.js?v=108"></script>
<link rel="stylesheet" href="codex-editor.css?v=11000">
<script src="plugins/paragraph/paragraph.js?v=100"></script>
<link rel="stylesheet" href="plugins/paragraph/paragraph.css">
<script src="plugins/header/header.js"></script>
<link rel="stylesheet" href="plugins/header/header.css">
<script src="plugins/code/code.js"></script>
<link rel="stylesheet" href="plugins/code/code.css">
<script src="plugins/link/link.js"></script>
<link rel="stylesheet" href="plugins/link/link.css">
<script src="plugins/quote/quote.js"></script>
<link rel="stylesheet" href="plugins/quote/quote.css">
<script src="plugins/list/list.js"></script>
<link rel="stylesheet" href="plugins/list/list.css">
<script src="plugins/image/image.js"></script>
<link rel="stylesheet" href="plugins/image/image.css">
<script src="plugins/instagram/instagram.js"></script>
<link rel="stylesheet" href="plugins/instagram/instagram.css">
<script src="plugins/twitter/twitter.js"></script>
<link rel="stylesheet" href="plugins/twitter/twitter.css">
<script src="plugins/embed/embed.js"></script>
<link rel="stylesheet" href="plugins/embed/embed.css">
<script src="plugins/raw/raw.js"></script>
<link rel="stylesheet" href="plugins/raw/raw.css">
<script src="plugins/attaches/attaches.js"></script>
<link rel="stylesheet" href="plugins/attaches/attaches.css">
<script>
codex.editor.start({
holderId : "codex-editor",
initialBlockPlugin : 'paragraph',
// placeholder: 'Прошлой ночью мне приснилось...',
hideToolbar: false,
tools : {
paragraph: {
type: 'paragraph',
iconClassname: 'ce-icon-paragraph',
render: paragraph.render,
validate: paragraph.validate,
save: paragraph.save,
destroy: paragraph.destroy,
allowedToPaste: true,
showInlineToolbar: true,
allowRenderOnPaste: true
},
header: {
type: 'header',
iconClassname: 'ce-icon-header',
appendCallback: header.appendCallback,
makeSettings: header.makeSettings,
render: header.render,
validate: header.validate,
save: header.save,
destroy: header.destroy,
displayInToolbox: true
},
code: {
type: 'code',
iconClassname: 'ce-icon-code',
make: code.make,
appendCallback: null,
settings: null,
render: code.render,
validate: code.validate,
save: code.save,
destroy: code.destroy,
displayInToolbox: true,
enableLineBreaks: true
},
link: {
type: 'link',
iconClassname: 'ce-icon-link',
prepare: link.prepare,
make: link.makeNewBlock,
appendCallback: link.appendCallback,
render: link.render,
validate: link.validate,
save: link.save,
destroy: link.destroy,
displayInToolbox: true,
enableLineBreaks: true
},
list: {
type: 'list',
iconClassname: 'ce-icon-list-bullet',
make: list.make,
appendCallback: null,
makeSettings: list.makeSettings,
render: list.render,
validate: list.validate,
save: list.save,
destroy: list.destroy,
displayInToolbox: true,
showInlineToolbar: true,
enableLineBreaks: true,
allowedToPaste: true,
},
quote: {
type: 'quote',
iconClassname: 'ce-icon-quote',
makeSettings: quote.makeSettings,
prepare: quote.prepare,
render: quote.render,
validate: quote.validate,
save: quote.save,
destroy: quote.destroy,
displayInToolbox: true,
enableLineBreaks: true,
showInlineToolbar: true,
allowedToPaste: true,
config : {
defaultStyle : 'withPhoto'
}
},
image_extended: {
type: 'image_extended',
iconClassname: 'ce-icon-picture',
appendCallback: image.appendCallback,
prepare: image.prepare,
makeSettings: image.makeSettings,
render: image.render,
save: image.save,
destroy: image.destroy,
isStretched: true,
showInlineToolbar: true,
displayInToolbox: true,
renderOnPastePatterns: image.pastePatterns,
config: {
uploadImage : '/writing/uploadImage',
uploadFromUrl : '/club/fetch'
}
},
instagram: {
type: 'instagram',
iconClassname: 'ce-icon-instagram',
prepare: instagram.prepare,
render: instagram.render,
validate: instagram.validate,
save: instagram.save,
destroy: instagram.destroy,
renderOnPastePatterns: instagram.pastePatterns,
},
tweet: {
type: 'tweet',
iconClassname: 'ce-icon-twitter',
prepare: twitter.prepare,
render: twitter.render,
validate: twitter.validate,
save: twitter.save,
destroy: twitter.destroy,
showInlineToolbar : true,
renderOnPastePatterns: twitter.pastePatterns,
config : {
fetchUrl : ''
}
},
video_extended: {
type: 'video_extended',
make: embed.make,
render: embed.render,
save: embed.save,
destroy: embed.destroy,
validate: embed.validate,
renderOnPastePatterns: embed.pastePatterns,
},
raw: {
type: 'raw',
displayInToolbox: true,
iconClassname: 'raw-plugin-icon',
render: rawPlugin.render,
save: rawPlugin.save,
validate: rawPlugin.validate,
destroy: rawPlugin.destroy,
enableLineBreaks: true,
allowPasteHTML: true
},
attaches: {
type: 'attaches',
displayInToolbox: true,
iconClassname: 'cdx-attaches__icon',
prepare: cdxAttaches.prepare,
render: cdxAttaches.render,
save: cdxAttaches.save,
validate: cdxAttaches.validate,
destroy: cdxAttaches.destroy,
appendCallback: cdxAttaches.appendCallback,
config: {
fetchUrl: '/test',
maxSize: 50000,
}
},
},
data : {
id: +new Date(),
items: [
{
type : 'header',
data : {
text : 'Привет от CodeX'
}
},
{
type : 'paragraph',
data : {
text : 'Пишите нам на team@ifmo.su'
}
},
],
count: 3
}
});
</script>
</html>

112
example/example.html Normal file
View file

@ -0,0 +1,112 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>CodeX Editor example</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
font-size: 14px;
line-height: 1.5em;
}
</style>
</head>
<body>
<div id="codex-editor"></div>
</body>
<link rel="stylesheet" href="tools-inline/term/term.css">
<script src="tools-inline/term/term.js"></script>
<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">-->
<script src="plugins/code/code.js"></script>
<link rel="stylesheet" href="plugins/code/code.css">
<script src="plugins/header/header.js?v=100"></script>
<link rel="stylesheet" href="plugins/header/header.css">
<script src="../build/codex-editor.js?v=108"></script>
<script>
codex = {};
codex.editor = 1;
var editor = new CodexEditor({
holderId : 'codex-editor',
initialBlock : 'text',
tools: {
text: Text,
header: Header,
term: Term,
},
toolsConfig: {
text: {
inlineToolbar : true
},
},
data: {
items: [
{
type : 'text',
data : {
text : 'Привет от CodeX'
}
},
{
type : 'text',
data : {
text : 'В <b>JavaScript</b> <a href="https://ifmo.su/ts-classes">нет возможности</a> назначить свойства при объявлении класса — все необходимые значения нужно определять в конструкторе или других методах. При таком подходе объявление свойств неявное, не всегда ясно какие свойства имеет класс. TS решает эту проблему: здесь можно не только объявить свойства класса, но и назначить им начальные значения'
}
},
{
type: "header",
data: {
text: "ES6 тебя сожрет",
level: 4
}
},
{
type : 'text',
data : {
text : 'Одним из недостатков ES6 классов является невозможность сделать методы и свойства приватными. В TS есть привычные модификаторы: <span class="marked">public</span>, <span class="marked">private</span> и <span class="marked">protected</span>, которые можно использовать как для методов, так и для свойств. По умолчанию, как и в других языках, все свойства имеют модификатор <span class="marked">public</span>.'
}
},
{
type: "header",
data: {
text: "Header 2",
level: 2
}
},
{
type: "header",
data: {
text: "Header 3",
level: 3
}
},
{
type: "header",
data: {
text: "Header 4",
level: 4
}
},
]
}
});
console.log('Editor instance:', editor);
window.editor = editor;
</script>
</html>

View file

@ -0,0 +1,71 @@
/**
* Code Plugin\
* Creates code tag and adds content to this tag
*/
var code = (function (code_plugin) {
var baseClass = 'ce-code';
/**
* Make initial header block
* @param {object} JSON with block data
* @return {Element} element to append
*/
var make_ = function (data) {
var tag = codex.editor.draw.node('TEXTAREA', [ baseClass ], {});
if (data && data.text) {
tag.value = data.text;
}
return tag;
};
/**
* Escapes HTML chars
*
* @param {string} input
* @return {string} escaped string
*/
var escapeHTML_ = function (input) {
var div = document.createElement('DIV'),
text = document.createTextNode(input);
div.appendChild(text);
return div.innerHTML;
};
/**
* Method to render HTML block from JSON
*/
code_plugin.render = function (data) {
return make_(data);
};
/**
* Method to extract JSON data from HTML block
*/
code_plugin.save = function (blockContent) {
var escaped = escapeHTML_(blockContent.value),
data = {
text : escaped
};
return data;
};
code_plugin.validate = function (data) {
if (data.text.trim() == '')
return;
return true;
};
code_plugin.destroy = function () {
code = null;
};
return code_plugin;
})({});

View file

@ -0,0 +1,199 @@
/**
* Embed plugin by gohabereg
* @version 1.0.0
*/
var embed = function (embed_plugin) {
var methods = {
addInternal: function (content) {
codex.editor.content.switchBlock(codex.editor.content.currentNode, content);
var blockContent = codex.editor.content.currentNode.childNodes[0];
blockContent.classList.add('embed__loader');
setTimeout(function () {
blockContent.classList.remove('embed__loader');
}, 1000);
},
getHtmlWithEmbedId: function (type, id) {
return services[type].html.replace(/<\%\= remote\_id \%\>/g, id);
},
makeElementFromHtml: function (html) {
var wrapper = document.createElement('DIV');
wrapper.innerHTML = html;
return wrapper;
},
getRemoteId: function (source, execArray) {
switch(source) {
case 'yandex-music-track':
id = execArray[2]+'/'+execArray[1];
break;
case 'yandex-music-playlist':
id = execArray[1]+'/'+execArray[2];
break;
default:
id = execArray[1];
}
return id;
}
};
var services = {
youtube: {
regex: /^.*(?:(?:youtu\.be\/)|(?:youtube\.com)\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*)(?:[\?\&]t\=(\d*)|)/,
html: '<iframe src="https://www.youtube.com/embed/<%= remote_id %>" style="width:100%;" height="320" frameborder="0" allowfullscreen></iframe>',
height: 320,
width: 580
}
};
embed_plugin.make = function (data, isInternal) {
if (!data.remote_id)
return;
var html = methods.getHtmlWithEmbedId(data.source, data.remote_id),
block = methods.makeElementFromHtml(html);
block.dataset.remoteId = data.remote_id;
block.dataset.source = data.source;
block.dataset.thumbnailUrl = data.thumbnailUrl;
block.classList.add('embed');
// var sidePadding = (600 - services[data.source].width) / 2 + 'px';
// block.style.padding = '30px ' + sidePadding;
if (isInternal) {
methods.addInternal(block);
}
return block;
};
/**
* Saving JSON output.
* Upload data via ajax
*/
embed_plugin.save = function (blockContent) {
if (!blockContent)
return;
var data,
source = blockContent.dataset.source;
data = {
source: source,
remote_id: blockContent.dataset.remoteId,
thumbnailUrl: blockContent.dataset.thumbnailUrl,
height: services[source].height,
width: services[source].width
};
return data;
};
/**
* Render data
*/
embed_plugin.render = function (data) {
return embed_plugin.make(data);
};
embed_plugin.urlPastedCallback = function (url, pattern) {
var execArray = pattern.regex.exec(url),
id = methods.getRemoteId(pattern.type, execArray);
var data = {
source: pattern.type,
remote_id: id,
thumbnailUrl: url
};
embed_plugin.make(data, true);
};
embed_plugin.validate = function (savedData) {
var source = savedData.source,
execArray = services[source].regex.exec(savedData.thumbnailUrl),
remoteId = methods.getRemoteId(source, execArray);
return remoteId == savedData.remote_id;
};
embed_plugin.pastePatterns = [
{
type: 'vk',
regex: /https?:\/\/vk\.com\/.*(?:video)([-0-9]+_[0-9]+)/, // /https?.+vk?.com\/feed\?w=wall\d+_\d+/,
callback: embed_plugin.urlPastedCallback
},
{
type: 'youtube',
regex: /(?:https?:\/{2})?(?:w{3}\.)?youtu(?:be)?\.(?:com|be)(?:\/watch\?v=|\/)([^\s&]+)/,
callback: embed_plugin.urlPastedCallback
},
{
type: 'vimeo',
regex: /(?:http[s]?:\/\/)?(?:www.)?vimeo\.co(?:.+\/([^\/]\d+)(?:#t=[\d]+)?s?$)/,
callback: embed_plugin.urlPastedCallback
},
{
type: 'coub',
regex: /https?:\/\/coub\.com\/view\/([^\/\?\&]+)/,
callback: embed_plugin.urlPastedCallback
},
{
type: 'vine',
regex: /https?:\/\/vine\.co\/v\/([^\/\?\&]+)/,
callback: embed_plugin.urlPastedCallback
},
{
type: 'imgur',
regex: /https?:\/\/(?:i\.)?imgur\.com.*\/([a-zA-Z0-9]+)(?:\.gifv)?/,
callback: embed_plugin.urlPastedCallback
},
{
type: 'gfycat',
regex: /https?:\/\/gfycat\.com(?:\/detail)?\/([a-zA-Z]+)/,
callback: embed_plugin.urlPastedCallback
},
{
type: 'twitch-channel',
regex: /https?:\/\/www.twitch.tv\/([^\/\?\&]*)/,
callback: embed_plugin.urlPastedCallback
},
{
type: 'twitch-video',
regex: /https?:\/\/www.twitch.tv\/(?:[^\/\?\&]*\/v|videos)\/([0-9]*)/,
callback: embed_plugin.urlPastedCallback
},
{
type: 'yandex-music-album',
regex: /https?:\/\/music.yandex.ru\/album\/([0-9]*)/,
callback: embed_plugin.urlPastedCallback
},
{
type: 'yandex-music-track',
regex: /https?:\/\/music.yandex.ru\/album\/([0-9]*)\/track\/([0-9]*)/,
callback: embed_plugin.urlPastedCallback
},
{
type: 'yandex-music-playlist',
regex: /https?:\/\/music.yandex.ru\/users\/([^\/\?\&]*)\/playlists\/([0-9]*)/,
callback: embed_plugin.urlPastedCallback
} ];
embed_plugin.destroy = function () {
embed = null;
};
return embed_plugin;
}({});

View file

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,20 @@
/**
* Plugin styles
*/
.ce-header {
padding: .7em 0;
margin: 0;
line-height: 1.5em;
outline: none;
}
.ce-header p,
.ce-header div{
padding: 0 !important;
margin: 0 !important;
}
/**
* Styles for Plugin icon in Toolbar
*/
.cdx-header-icon {}

View file

@ -0,0 +1,393 @@
/**
* @typedef {Object} HeaderData
* @description Tool's input and output data format
* @property {String} text Header's content
* @property {number} level - Header's level from 1 to 3
*/
/**
* @typedef {Object} HeaderConfig
* @description Tool's config from Editor
* @property {string} placeholder Block's placeholder
*/
/**
* Header block for the CodeX Editor.
*
* @author CodeX Team (team@ifmo.su)
* @copyright CodeX Team 2018
* @license The MIT License (MIT)
* @version 2.0.0
*/
class Header {
/**
* Should this tools be displayed at the Editor's Toolbox
* @returns {boolean}
* @public
*/
static get displayInToolbox() {
return true;
}
/**
* Class for the Toolbox icon
* @returns {string}
* @public
*/
static get iconClassName() {
return 'cdx-header-icon';
}
/**
* Render plugin`s main Element and fill it with saved data
* @param {HeaderData} blockData - previously saved data
* @param {HeaderConfig} blockConfig - Tool's config from Editor
*/
constructor(blockData = {}, blockConfig = {}) {
/**
* Styles
* @type {Object}
*/
this._CSS = {
wrapper: 'ce-header',
settingsButton: 'ce-settings__button',
settingsSelected: 'ce-settings__button--selected',
};
/**
* Tool's settings passed from Editor
* @type {HeaderConfig}
* @private
*/
this._settings = blockConfig;
/**
* Block's data
* @type {HeaderData}
* @private
*/
this._data = blockData || {};
/**
* List of settings buttons
* @type {HTMLElement[]}
*/
this.settingsButtons = [];
/**
* Main Block wrapper
* @type {HTMLElement}
* @private
*/
this._element = this.getTag();
}
/**
* Return Tool's view
* @returns {HTMLHeadingElement}
* @public
*/
render() {
return this._element;
}
/**
* Create Block's settings block
*
* @return {HTMLElement}
*/
renderSettings() {
let holder = document.createElement('DIV');
/** Add type selectors */
this.levels.forEach( level => {
let selectTypeButton = document.createElement('SPAN');
selectTypeButton.classList.add(this._CSS.settingsButton);
/**
* Highlight current level button
*/
if (this.currentLevel.number === level.number) {
selectTypeButton.classList.add(this._CSS.settingsSelected);
}
/**
* Add SVG icon
*/
selectTypeButton.innerHTML = level.svg;
/**
* Save level to its button
*/
selectTypeButton.dataset.level = level.number;
/**
* Set up click handler
*/
selectTypeButton.addEventListener('click', () => {
this.setLevel(level.number);
});
/**
* Append settings button to holder
*/
holder.appendChild(selectTypeButton);
/**
* Save settings buttons
*/
this.settingsButtons.push(selectTypeButton);
});
return holder;
}
/**
* Callback for Block's settings buttons
* @param level
*/
setLevel(level) {
this.data = {
level: level
};
/**
* Highlight button by selected level
*/
this.settingsButtons.forEach(button => {
button.classList.toggle(this._CSS.settingsSelected, parseInt(button.dataset.level) === level);
});
}
/**
* Method that specified how to merge two Text blocks.
* Called by CodeX Editor by backspace at the beginning of the Block
* @param {HeaderData} data
* @public
*/
merge(data) {
let newData = {
text: this.data.text + data.text,
level: this.data.level
};
this.data = newData;
}
/**
* Validate Text block data:
* - check for emptiness
*
* @param {HeaderData} blockData data received after saving
* @returns {boolean} false if saved data is not correct, otherwise true
* @public
*/
validate(blockData) {
return blockData.text.trim() !== '';
}
/**
* Extract Tool's data from the view
* @param {HTMLHeadingElement} toolsContent - Text tools rendered view
* @returns {HeaderData} - saved data
* @public
*/
save(toolsContent) {
/**
* @todo sanitize data
*/
return {
text: toolsContent.innerHTML,
level: this.currentLevel.number
};
}
/**
* Get current Tools`s data
* @returns {HeaderData} Current data
* @private
*/
get data() {
this._data.text = this._element.innerHTML;
this._data.level = this.currentLevel.number;
return this._data;
}
/**
* Store data in plugin:
* - at the this._data property
* - at the HTML
*
* @param {HeaderData} data data to set
* @private
*/
set data(data) {
this._data = data || {};
/**
* If level is set and block in DOM
* then replace it to a new block
*/
if (data.level !== undefined && this._element.parentNode) {
/**
* Create a new tag
* @type {HTMLHeadingElement}
*/
let newHeader = this.getTag();
/**
* Save Block's content
*/
newHeader.innerHTML = this._element.innerHTML;
/**
* Replace blocks
*/
this._element.parentNode.replaceChild(newHeader, this._element);
/**
* Save new block to private variable
* @type {HTMLHeadingElement}
* @private
*/
this._element = newHeader;
}
/**
* If data.text was passed then update block's content
*/
if (data.text !== undefined) {
this._element.innerHTML = this._data.text || '';
}
}
/**
* Get tag for target level
* By default returns second-leveled header
* @return {HTMLElement}
*/
getTag() {
/**
* Create element for current Block's level
*/
let tag = document.createElement(this.currentLevel.tag);
/**
* Add text to block
*/
tag.innerHTML = this._data.text || '';
/**
* Add styles class
*/
tag.classList.add(this._CSS.wrapper);
/**
* Make tag editable
*/
tag.contentEditable = 'true';
/**
* Add Placeholder
*/
tag.dataset.placeholder = this._settings.placeholder || '';
return tag;
}
/**
* Get current level
* @return {level}
*/
get currentLevel() {
let level = this.levels.find( level => level.number === this._data.level);
if (!level) {
level = this.levels[0];
}
return level;
}
/**
* @typedef {object} level
* @property {number} number - level number
* @property {string} tag - tag correspondes with level number
* @property {string} svg - icon
*/
/**
* Available header levels
* @return {level[]}
*/
get levels() {
return [
{
number: 2,
tag: 'H2',
svg: '<svg width="18" height="14" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M2.152 1.494V4.98h4.646V1.494c0-.498.097-.871.293-1.12A.934.934 0 0 1 7.863 0c.324 0 .586.123.786.37.2.246.301.62.301 1.124v9.588c0 .503-.101.88-.304 1.128a.964.964 0 0 1-.783.374.928.928 0 0 1-.775-.378c-.194-.251-.29-.626-.29-1.124V6.989H2.152v4.093c0 .503-.101.88-.304 1.128a.964.964 0 0 1-.783.374.928.928 0 0 1-.775-.378C.097 11.955 0 11.58 0 11.082V1.494C0 .996.095.623.286.374A.929.929 0 0 1 1.066 0c.323 0 .585.123.786.37.2.246.3.62.3 1.124zm10.99 9.288h3.527c.351 0 .62.072.804.216.185.144.277.34.277.588 0 .22-.073.408-.22.56-.146.154-.368.23-.665.23h-4.972c-.338 0-.601-.093-.79-.28a.896.896 0 0 1-.284-.659c0-.162.06-.377.182-.645s.255-.478.399-.631a38.617 38.617 0 0 1 1.621-1.598c.482-.444.827-.735 1.034-.875.369-.261.676-.523.922-.787.245-.263.432-.534.56-.81.129-.278.193-.549.193-.815 0-.288-.069-.546-.206-.773a1.428 1.428 0 0 0-.56-.53 1.618 1.618 0 0 0-.774-.19c-.59 0-1.054.26-1.392.777-.045.068-.12.252-.226.554-.106.302-.225.534-.358.696-.133.162-.328.243-.585.243a.76.76 0 0 1-.56-.223c-.149-.148-.223-.351-.223-.608 0-.31.07-.635.21-.972.139-.338.347-.645.624-.92a3.093 3.093 0 0 1 1.054-.665c.426-.169.924-.253 1.496-.253.69 0 1.277.108 1.764.324.315.144.592.343.83.595.24.252.425.544.558.875.133.33.2.674.2 1.03 0 .558-.14 1.066-.416 1.523-.277.457-.56.815-.848 1.074-.288.26-.771.666-1.45 1.22-.677.554-1.142.984-1.394 1.29a3.836 3.836 0 0 0-.331.44z"/></svg>'
},
{
number: 3,
tag: 'H3',
svg: '<svg width="18" height="14" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M2.152 1.494V4.98h4.646V1.494c0-.498.097-.871.293-1.12A.934.934 0 0 1 7.863 0c.324 0 .586.123.786.37.2.246.301.62.301 1.124v9.588c0 .503-.101.88-.304 1.128a.964.964 0 0 1-.783.374.928.928 0 0 1-.775-.378c-.194-.251-.29-.626-.29-1.124V6.989H2.152v4.093c0 .503-.101.88-.304 1.128a.964.964 0 0 1-.783.374.928.928 0 0 1-.775-.378C.097 11.955 0 11.58 0 11.082V1.494C0 .996.095.623.286.374A.929.929 0 0 1 1.066 0c.323 0 .585.123.786.37.2.246.3.62.3 1.124zm11.61 4.919c.418 0 .778-.123 1.08-.368.301-.245.452-.597.452-1.055 0-.35-.12-.65-.36-.902-.241-.252-.566-.378-.974-.378-.277 0-.505.038-.684.116a1.1 1.1 0 0 0-.426.306 2.31 2.31 0 0 0-.296.49c-.093.2-.178.388-.255.565a.479.479 0 0 1-.245.225.965.965 0 0 1-.409.081.706.706 0 0 1-.5-.22c-.152-.148-.228-.345-.228-.59 0-.236.071-.484.214-.745a2.72 2.72 0 0 1 .627-.746 3.149 3.149 0 0 1 1.024-.568 4.122 4.122 0 0 1 1.368-.214c.44 0 .842.06 1.205.18.364.12.679.294.947.52.267.228.47.49.606.79.136.3.204.622.204.967 0 .454-.099.843-.296 1.168-.198.324-.48.64-.848.95.354.19.653.408.895.653.243.245.426.516.548.813.123.298.184.619.184.964 0 .413-.083.812-.248 1.198-.166.386-.41.73-.732 1.031a3.49 3.49 0 0 1-1.147.708c-.443.17-.932.256-1.467.256a3.512 3.512 0 0 1-1.464-.293 3.332 3.332 0 0 1-1.699-1.64c-.142-.314-.214-.573-.214-.777 0-.263.085-.475.255-.636a.89.89 0 0 1 .637-.242c.127 0 .25.037.367.112a.53.53 0 0 1 .232.27c.236.63.489 1.099.759 1.405.27.306.65.46 1.14.46a1.714 1.714 0 0 0 1.46-.824c.17-.273.256-.588.256-.947 0-.53-.145-.947-.436-1.249-.29-.302-.694-.453-1.212-.453-.09 0-.231.01-.422.028-.19.018-.313.027-.367.027-.25 0-.443-.062-.579-.187-.136-.125-.204-.299-.204-.521 0-.218.081-.394.245-.528.163-.134.406-.2.728-.2h.28z"/></svg>'
},
{
number: 4,
tag: 'H4',
svg: '<svg width="20" height="14" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M2.152 1.494V4.98h4.646V1.494c0-.498.097-.871.293-1.12A.934.934 0 0 1 7.863 0c.324 0 .586.123.786.37.2.246.301.62.301 1.124v9.588c0 .503-.101.88-.304 1.128a.964.964 0 0 1-.783.374.928.928 0 0 1-.775-.378c-.194-.251-.29-.626-.29-1.124V6.989H2.152v4.093c0 .503-.101.88-.304 1.128a.964.964 0 0 1-.783.374.928.928 0 0 1-.775-.378C.097 11.955 0 11.58 0 11.082V1.494C0 .996.095.623.286.374A.929.929 0 0 1 1.066 0c.323 0 .585.123.786.37.2.246.3.62.3 1.124zm13.003 10.09v-1.252h-3.38c-.427 0-.746-.097-.96-.29-.213-.193-.32-.456-.32-.788 0-.085.016-.171.048-.259.031-.088.078-.18.141-.276.063-.097.128-.19.195-.28.068-.09.15-.2.25-.33l3.568-4.774a5.44 5.44 0 0 1 .576-.683.763.763 0 0 1 .542-.212c.682 0 1.023.39 1.023 1.171v5.212h.29c.346 0 .623.047.832.142.208.094.313.3.313.62 0 .26-.086.45-.256.568-.17.12-.427.179-.768.179h-.41v1.252c0 .346-.077.603-.23.771-.152.168-.356.253-.612.253a.78.78 0 0 1-.61-.26c-.154-.173-.232-.427-.232-.764zm-2.895-2.76h2.895V4.91L12.26 8.823z"/></svg>'
}
];
}
/**
* Handle H1-H6 tags on paste to substitute it with header Tool
*
* @private
* @param {HTMLElement} content - pasted element
* @returns {{level: number, text: *}}
*/
static onPasteHandler(content) {
let level = 4;
switch (content.tagName) {
case 'H1':
case 'H2':
level = 2;
break;
case 'H3':
level = 3;
break;
}
return {
level,
text: content.innerHTML
};
}
/**
* Used by Codex Editor paste handling API.
* Provides configuration to handle H1-H6 tags.
*
* @returns {{handler: (function(HTMLElement): {text: string}), tags: string[]}}
*/
static get onPaste() {
return {
handler: Header.onPasteHandler,
tags: ['H1', 'H2', 'H3', 'H4', 'H5', 'H6']
};
}
/**
* Get Tool icon's SVG
* @return {string}
*/
static get toolboxIcon() {
return '<svg width="11" height="14" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path d="M7.6 8.15H2.25v4.525a1.125 1.125 0 0 1-2.25 0V1.125a1.125 1.125 0 1 1 2.25 0V5.9H7.6V1.125a1.125 1.125 0 0 1 2.25 0v11.55a1.125 1.125 0 0 1-2.25 0V8.15z"/></svg>';
}
}

View file

@ -0,0 +1,647 @@
/**
* Image plugin for codex-editor
* @author CodeX Team <team@ifmo.su>
*
* @version 1.3.0
*/
var image = (function (image_plugin) {
/**
* @private
*
* CSS classNames
*/
var elementClasses_ = {
ce_image : 'ce-image',
loading : 'ce-plugin-image__loader',
blockStretched: 'ce-block--stretched',
uploadedImage : {
centered : 'ce-plugin-image__uploaded--centered',
stretched : 'ce-plugin-image__uploaded--stretched'
},
imageCaption : 'ce-plugin-image__caption',
imageWrapper : 'ce-plugin-image__wrapper',
formHolder : 'ce-plugin-image__holder',
uploadButton : 'ce-plugin-image__button',
imagePreview : 'ce-image__preview',
selectorHolder: 'ce-settings-checkbox',
selectorButton: 'ce-settings-checkbox__toggler',
settingsItem: 'ce-image-settings__item',
imageWrapperBordered : 'ce-image__wrapper--bordered',
toggled : 'ce-image-settings__item--toggled'
};
/**
*
* @private
*
* UI methods
*/
var ui_ = {
holder : function () {
var element = document.createElement('DIV');
element.classList.add(elementClasses_.formHolder);
element.classList.add(elementClasses_.ce_image);
return element;
},
uploadButton : function () {
var button = document.createElement('SPAN');
button.classList.add(elementClasses_.uploadButton);
button.innerHTML = '<i class="ce-icon-picture"> </i>';
button.innerHTML += 'Загрузить фотографию';
return button;
},
/**
* @param {object} file - file path
* @param {string} style - css class
* @return {object} image - document IMG tag
*/
image : function (file, styles) {
var image = document.createElement('IMG');
styles.map(function (item) {
image.classList.add(item);
});
image.src = file.url;
image.dataset.bigUrl = file.bigUrl;
return image;
},
wrapper : function () {
var div = document.createElement('div');
div.classList.add(elementClasses_.imageWrapper);
return div;
},
caption : function () {
var div = document.createElement('div');
div.classList.add(elementClasses_.imageCaption);
div.contentEditable = true;
return div;
},
/**
* Draws form for image upload
*/
makeForm : function () {
var holder = ui_.holder(),
uploadButton = ui_.uploadButton();
holder.appendChild(uploadButton);
uploadButton.addEventListener('click', uploadButtonClicked_, false );
image.holder = holder;
return holder;
},
/**
* wraps image and caption
* @param {object} data - image information
* @param {string} imageTypeClass - plugin's style
* @param {boolean} stretched - stretched or not
* @return wrapped block with image and caption
*/
makeImage : function (data, imageTypeClasses, stretched, bordered) {
var file = data,
text = data.caption,
type = data.type,
image = ui_.image(file, imageTypeClasses),
caption = ui_.caption(),
wrapper = ui_.wrapper();
caption.innerHTML = text || '';
wrapper.dataset.stretched = stretched;
wrapper.dataset.bordered = bordered;
/** Appeding to the wrapper */
wrapper.appendChild(image);
wrapper.appendChild(caption);
return wrapper;
},
/**
* @param {HTML} data - Rendered block with image
*/
getImage : function (data) {
var image = data.querySelector('.' + elementClasses_.uploadedImage.centered) ||
data.querySelector('.' + elementClasses_.uploadedImage.stretched);
return image;
},
/**
* wraps image and caption
* @deprecated
* @param {object} data - image information
* @return wrapped block with image and caption
*/
centeredImage : function (data) {
var file = data.file,
text = data.caption,
type = data.type,
image = ui_.image(file, elementClasses_.uploadedImage.centered),
caption = ui_.caption(),
wrapper = ui_.wrapper();
caption.textContent = text;
wrapper.dataset.stretched = 'false';
/** Appeding to the wrapper */
wrapper.appendChild(image);
wrapper.appendChild(caption);
return wrapper;
},
/**
* wraps image and caption
* @deprecated
* @param {object} data - image information
* @return stretched image
*/
stretchedImage : function (data) {
var file = data.file,
text = data.caption,
type = data.type,
image = ui_.image(file, elementClasses_.uploadedImage.stretched),
caption = ui_.caption(),
wrapper = ui_.wrapper();
caption.textContent = text;
wrapper.dataset.stretched = 'true';
/** Appeding to the wrapper */
wrapper.appendChild(image);
wrapper.appendChild(caption);
return wrapper;
}
};
/**
* @private
*
* After render callback
*/
var uploadButtonClicked_ = function (event) {
var url = image_plugin.config.uploadImage,
beforeSend = uploadingCallbacks_.ByClick.beforeSend,
success = uploadingCallbacks_.ByClick.success,
error = uploadingCallbacks_.ByClick.error;
/** Define callbacks */
codex.editor.transport.selectAndUpload({
url : url,
multiple : false,
accept : 'image/*',
beforeSend : beforeSend,
success : success,
error : error
});
};
var methods_ = {
addSelectTypeClickListener : function (el, type) {
el.addEventListener('click', function () {
// el - settings element
switch (type) {
case 'bordered':
methods_.toggleBordered(type, this); break;
case 'stretched':
methods_.toggleStretched(type, this); break;
}
}, false);
},
toggleBordered : function (type, clickedSettingsItem ) {
var current = codex.editor.content.currentNode,
blockContent = current.childNodes[0],
img = ui_.getImage(current),
wrapper = current.querySelector('.' + elementClasses_.imageWrapper);
if (!img) {
return;
}
/**
* Add classes to the IMG tag and to the Settings element
*/
img.classList.toggle(elementClasses_.imageWrapperBordered);
clickedSettingsItem.classList.toggle(elementClasses_.toggled);
/**
* Save settings in dataset
*/
wrapper.dataset.bordered = img.classList.contains(elementClasses_.imageWrapperBordered);
setTimeout(function () {
codex.editor.toolbar.settings.close();
}, 200);
},
toggleStretched : function ( type, clickedSettingsItem ) {
var current = codex.editor.content.currentNode,
blockContent = current.childNodes[0],
img = ui_.getImage(current),
wrapper = current.querySelector('.' + elementClasses_.imageWrapper);
if (!img) {
return;
}
/** Clear classList */
blockContent.classList.add(elementClasses_.blockStretched);
img.classList.toggle(elementClasses_.uploadedImage.stretched);
img.classList.toggle(elementClasses_.uploadedImage.centered);
clickedSettingsItem.classList.toggle(elementClasses_.toggled);
wrapper.dataset.stretched = img.classList.contains(elementClasses_.uploadedImage.stretched);
setTimeout(function () {
codex.editor.toolbar.settings.close();
}, 1000);
}
};
/**
* @private
* Callbacks
*/
var uploadingCallbacks_ = {
ByClick : {
/**
* Before sending ajax request
*/
beforeSend : function () {
var input = codex.editor.transport.input,
files = input.files;
var validFileExtensions = ['jpg', 'jpeg', 'bmp', 'gif', 'png'];
var type = files[0].type.split('/');
var result = validFileExtensions.some(function (ext) {
return ext == type[1];
});
if (!result) {
return;
}
var reader = new FileReader();
reader.readAsDataURL(files[0]);
reader.onload = function (e) {
var data = {
background : false,
border : false,
isstretch : false,
url : e.target.result,
bigUrl : null,
width : null,
height : null,
additionalData : null,
caption : '',
cover : null
};
var newImage = make_(data);
codex.editor.content.switchBlock(image.holder, newImage, 'image');
newImage.classList.add(elementClasses_.imagePreview);
/**
* Change holder to image
*/
image.holder = newImage;
};
},
/** Photo was uploaded successfully */
success : function (result) {
var parsed = JSON.parse(result),
data,
currentBlock = codex.editor.content.currentNode;
/**
* Preparing {Object} data to draw an image
* @uses ceImage.make method
*/
data = parsed.data;
image.holder.classList.remove(elementClasses_.imagePreview);
/**
* Change src of image
*/
var newImage = image.holder.getElementsByTagName('IMG')[0];
newImage.src = parsed.data.url;
newImage.dataset.bigUrl = parsed.data.bigUrl;
newImage.dataset.width = parsed.data.width;
newImage.dataset.height = parsed.data.height;
newImage.dataset.additionalData = parsed.data.additionalData;
},
/** Error callback. Sends notification to user that something happend or plugin doesn't supports method */
error : function (result) {
var oldHolder = image.holder;
var form = ui_.makeForm();
codex.editor.content.switchBlock(oldHolder, form, 'image');
}
},
ByPaste : {
/**
* Direct upload
* Any URL that contains image extension
* @param url
*/
uploadImageFromUrl : function (path) {
var image,
current = codex.editor.content.currentNode,
beforeSend,
success_callback;
/** When image is uploaded to redactors folder */
success_callback = function (data) {
var imageInfo = JSON.parse(data);
var newImage = image.getElementsByTagName('IMG')[0];
newImage.dataset.stretched = false;
newImage.dataset.src = imageInfo.url;
newImage.dataset.bigUrl = imageInfo.bigUrl;
newImage.dataset.width = imageInfo.width;
newImage.dataset.height = imageInfo.height;
newImage.dataset.additionalData = imageInfo.additionalData;
image.classList.remove(elementClasses_.imagePreview);
};
/** Before sending XMLHTTP request */
beforeSend = function () {
var content = current.querySelector('.ce-block__content');
var data = {
background: false,
border: false,
isStretch: false,
file: {
url: path,
bigUrl: null,
width: null,
height: null,
additionalData: null
},
caption: '',
cover: null
};
image = codex.editor.tools.image_extended.render(data);
image.classList.add(elementClasses_.imagePreview);
var img = image.querySelector('img');
codex.editor.content.switchBlock(codex.editor.content.currentNode, image, 'image');
};
/** Preparing data for XMLHTTP */
var data = {
url: image_plugin.config.uploadFromUrl,
type: 'POST',
data : {
url: path
},
beforeSend : beforeSend,
success : success_callback
};
codex.editor.core.ajax(data);
}
}
};
/**
* Image path
* @type {null}
*/
image_plugin.path = null;
/**
* Plugin configuration
*/
image_plugin.config = null;
/**
*
* @private
*
* @param data
* @return {*}
*
*/
var make_ = function ( data ) {
var holder,
classes = [];
if (data) {
if (data.border) {
classes.push(elementClasses_.imageWrapperBordered);
}
if ( data.isstretch || data.isstretch === 'true') {
classes.push(elementClasses_.uploadedImage.stretched);
holder = ui_.makeImage(data, classes, 'true', data.border);
} else {
classes.push(elementClasses_.uploadedImage.centered);
holder = ui_.makeImage(data, classes, 'false', data.border);
}
return holder;
} else {
holder = ui_.makeForm();
return holder;
}
};
/**
* @private
*
* Prepare clear data before save
*
* @param data
*/
var prepareDataForSave_ = function (data) {
};
/**
* @public
* @param config
*/
image_plugin.prepare = function (config) {
image_plugin.config = config;
return Promise.resolve();
};
/**
* @public
*
* this tool works when tool is clicked in toolbox
*/
image_plugin.appendCallback = function (event) {
/** Upload image and call success callback*/
uploadButtonClicked_(event);
};
/**
* @public
*
* @param data
* @return {*}
*/
image_plugin.render = function ( data ) {
return make_(data);
};
/**
* @public
*
* @param block
* @return {{background: boolean, border: boolean, isstretch: boolean, file: {url: (*|string|Object), bigUrl: (null|*), width: *, height: *, additionalData: null}, caption: (string|*|string), cover: null}}
*/
image_plugin.save = function ( block ) {
var content = block,
image = ui_.getImage(content),
caption = content.querySelector('.' + elementClasses_.imageCaption);
var data = {
background : false,
border : content.dataset.bordered === 'true' ? true : false,
isstretch : content.dataset.stretched === 'true' ? true : false,
// file : {
url : image.dataset.src || image.src,
bigUrl : image.dataset.bigUrl,
width : image.width,
height : image.height,
additionalData :null,
// },
caption : caption.innerHTML || '',
cover : null
};
return data;
};
/**
* @public
*
* Settings panel content
* @return {Element} element contains all settings
*/
image_plugin.makeSettings = function () {
var currentNode = codex.editor.content.currentNode,
wrapper = currentNode.querySelector('.' + elementClasses_.imageWrapper),
holder = document.createElement('DIV'),
types = {
stretched : 'На всю ширину',
bordered : 'Добавить рамку'
},
currentImageWrapper = currentNode.querySelector('.' + elementClasses_.imageWrapper ),
currentImageSettings = currentImageWrapper.dataset;
/** Add holder classname */
holder.className = 'ce-image-settings';
/** Now add type selectors */
for (var type in types) {
/**
* Settings template
*/
var settingsItem = document.createElement('DIV'),
selectorsHolder = document.createElement('SPAN'),
selectorsButton = document.createElement('SPAN');
settingsItem.classList.add(elementClasses_.settingsItem);
selectorsHolder.classList.add(elementClasses_.selectorHolder);
selectorsButton.classList.add(elementClasses_.selectorButton);
selectorsHolder.appendChild(selectorsButton);
settingsItem.appendChild(selectorsHolder);
selectTypeButton = document.createTextNode(types[type]);
settingsItem.appendChild(selectTypeButton);
/**
* Activate previously selected settings
*/
if ( currentImageSettings[type] == 'true' ) {
settingsItem.classList.add(elementClasses_.toggled);
}
methods_.addSelectTypeClickListener(settingsItem, type);
holder.appendChild(settingsItem);
}
return holder;
};
/**
* Share as API
*/
image_plugin.uploadImageFromUri = uploadingCallbacks_.ByPaste.uploadImageFromUrl;
image_plugin.pastePatterns = [
{
type: 'image',
regex: /(?:([^:\/?#]+):)?(?:\/\/([^\/?#]*))?([^?#]*\.(?:jpe?g|gif|png))(?:\?([^#]*))?(?:#(.*))?/i,
callback: image_plugin.uploadImageFromUri
},
{
type: 'uploadCare',
regex: /^https:\/\/(uploadcare\.cmtt\.ru|ucarecdn\.com|static[0-9]+\.siliconrus\.cmtt\.ru|static[0-9]+\.cmtt\.ru)/i,
callback: image_plugin.uploadImageFromUri
} ];
image_plugin.destroy = function () {
image = null;
};
return image_plugin;
})({});

View file

@ -0,0 +1,143 @@
/**
* Instagram plugin
* @version 1.0.0
*/
var instagram = (function (instagram_plugin) {
var methods = {
render : function (content) {
codex.editor.content.switchBlock(codex.editor.content.currentNode, content);
setTimeout(function () {
window.instgrm.Embeds.process();
}, 200);
},
/**
* Drawing html content.
*
* @param url
* @returns {Element} blockquote - HTML template for Instagram Embed JS
*/
instagramBlock : function (url) {
var blockquote = codex.editor.draw.node('BLOCKQUOTE', 'instagram-media instagram', {}),
div = codex.editor.draw.node('DIV', '', {}),
paragraph = codex.editor.draw.node('P', 'ce-paste__instagram--p', {}),
anchor = codex.editor.draw.node('A', '', { href : url });
blockquote.dataset.instgrmVersion = 4;
paragraph.appendChild(anchor);
div.appendChild(paragraph);
blockquote.appendChild(div);
return blockquote;
}
};
/**
* Prepare before usage
* Load important scripts to render embed
*/
instagram_plugin.prepare = function () {
return new Promise(function (resolve, reject) {
codex.editor.core.importScript('https://platform.instagram.com/en_US/embeds.js', 'instagram-api').then(function () {
resolve();
}).catch(function () {
reject(Error('Instagram API was not loaded'));
});
});
};
/**
* @private
*
* Make instagram embed via Widgets method
*/
var make_ = function (data, isInternal) {
if (!data.instagram_url)
return;
var block = methods.instagramBlock(data.instagram_url);
if (isInternal) {
setTimeout(function () {
/** Render block */
methods.render(block);
}, 200);
}
if (!isInternal) {
methods.render(block);
}
return block;
};
instagram_plugin.validate = function (data) {
return true;
};
/**
* Saving JSON output.
* Upload data via ajax
*/
instagram_plugin.save = function (blockContent) {
var data;
if (!blockContent)
return;
/** Example */
data = {
instagram_url: blockContent.src
};
return data;
};
instagram_plugin.validate = function (data) {
var checkUrl = new RegExp('http?.+instagram.com\/p?.');
if (!data.instagram_url || checkUrl.exec(data.instagram_url).length == 0)
return;
return true;
};
/**
* Render data
*/
instagram_plugin.render = function (data) {
return make_(data);
};
/**
* callback for instagram url's coming from pasteTool
* Using instagram Embed Widgete to render
* @param url
*/
instagram_plugin.urlPastedCallback = function (url) {
var data = {
instagram_url: url
};
make_(data, true);
};
instagram_plugin.pastePatterns = [
{
type: 'instagram',
regex: /http?.+instagram.com\/p\/([a-zA-Z0-9]*)\S*/,
callback: instagram_plugin.urlPastedCallback
}
];
instagram_plugin.destroy = function () {
instagram = null;
delete window.instgrm;
};
return instagram_plugin;
})({});

View file

Before

Width:  |  Height:  |  Size: 329 B

After

Width:  |  Height:  |  Size: 329 B

View file

@ -0,0 +1,301 @@
/**
* Created by nostr on 29.06.16.
*/
/**
* Link tool plugin
*/
var link = (function (link_plugin) {
var settings = {
defaultText : 'Вставьте ссылку ...',
ENTER_KEY : 13,
currentBlock : null,
currentInput : null,
elementClasses : {
link: 'tool-link-link',
image: 'tool-link-image',
title: 'tool-link-title',
description: 'tool-link-description',
loader: 'tool-link-loader',
error: 'tool-link-error'
}
};
var ui = {
make : function (json) {
var wrapper = ui.wrapper(),
siteImage = ui.image(json.image, settings.elementClasses.image),
siteTitle = ui.title(json.title),
siteDescription = ui.description(json.description),
siteLink = ui.link(json.url, json.url);
wrapper.appendChild(siteImage);
wrapper.appendChild(siteTitle);
wrapper.appendChild(siteLink);
wrapper.appendChild(siteDescription);
siteTitle.contentEditable = true;
siteDescription.contentEditable = true;
return wrapper;
},
mainBlock : function () {
var wrapper = document.createElement('div');
wrapper.classList.add('ceditor-tool-link');
return wrapper;
},
input : function () {
var inputTag = document.createElement('input');
inputTag.classList.add('ceditor-tool-link-input');
inputTag.placeholder = settings.defaultText;
inputTag.contentEditable = false;
return inputTag;
},
wrapper : function () {
var wrapper = document.createElement('div');
wrapper.classList.add('tool-link-panel', 'clearfix');
return wrapper;
},
image : function (imageSrc, imageClass) {
var imageTag = document.createElement('img');
imageTag.classList.add(imageClass);
imageTag.setAttribute('src', imageSrc);
return imageTag;
},
link : function (linkUrl, linkText) {
var linkTag = document.createElement('a');
linkTag.classList.add(settings.elementClasses.link);
linkTag.href = linkUrl;
linkTag.target = '_blank';
linkTag.innerText = linkText;
return linkTag;
},
title : function (titleText) {
var titleTag = document.createElement('div');
titleTag.classList.add('tool-link-content', settings.elementClasses.title);
titleTag.innerHTML = titleText;
return titleTag;
},
description : function (descriptionText) {
var descriptionTag = document.createElement('div');
descriptionTag.classList.add('tool-link-content', settings.elementClasses.description);
descriptionTag.innerHTML = descriptionText;
return descriptionTag;
}
};
var methods = {
blockPasteCallback : function (event) {
var clipboardData = event.clipboardData || window.clipboardData,
pastedData = clipboardData.getData('Text'),
block = event.target.parentNode;
methods.renderLink(pastedData, block);
event.stopPropagation();
},
blockKeyDownCallback : function (event) {
var inputTag = event.target,
block = inputTag.parentNode,
url;
if ( block.classList.contains(settings.elementClasses.error) ) {
block.classList.remove(settings.elementClasses.error);
}
if (event.keyCode == settings.ENTER_KEY) {
url = inputTag.value;
methods.renderLink(url, block);
event.preventDefault();
}
},
renderLink : function (url, block) {
Promise.resolve()
.then(function () {
return methods.urlify(url);
})
.then(function (url) {
/* Show loader gif **/
block.classList.add(settings.elementClasses.loader);
return fetch( link_plugin.config.fetchUrl + '?url=' + encodeURI(url) );
})
.then(function (response) {
if (response.status == '200') {
return response.json();
} else {
return Error('Invalid response status: %o', response);
}
})
.then(function (json) {
methods.composeLinkPreview(json, block);
})
.catch(function (error) {
/* Hide loader gif **/
block.classList.remove(settings.elementClasses.loader);
block.classList.add(settings.elementClasses.error);
codex.editor.core.log('Error while doing things with link paste: %o', 'error', error);
});
},
urlify : function (text) {
var urlRegex = /(https?:\/\/\S+)/g;
var links = text.match(urlRegex);
if (links) {
return links[0];
}
return Promise.reject(Error('Url is not matched'));
},
composeLinkPreview : function (json, currentBlock) {
if (json == {}) {
return;
}
var previewBlock = ui.make(json);
settings.currentInput.remove();
currentBlock.appendChild(previewBlock);
currentBlock.classList.remove(settings.elementClasses.loader);
}
};
link_plugin.prepare = function (config) {
link_plugin.config = config;
return Promise.resolve();
};
/**
* Make initial header block
* @param {object} JSON with block data
* @return {Element} element to append
*/
link_plugin.makeNewBlock = function (data) {
var wrapper = ui.mainBlock(),
tag = ui.input();
settings.currentInput = tag;
wrapper.appendChild(tag);
wrapper.classList.add('ce-link');
/**
* Bind callbacks
**/
tag.addEventListener('paste', methods.blockPasteCallback, false);
tag.addEventListener('keydown', methods.blockKeyDownCallback, false);
return wrapper;
};
/**
* Method to render HTML block from JSON
*/
link_plugin.render = function (json) {
if ( json ) {
var block = ui.mainBlock(),
tag = ui.make(json);
block.classList.add('ce-link');
block.appendChild(tag);
return block;
} else {
var wrapper = ui.mainBlock(),
tag = ui.input();
settings.currentInput = tag;
wrapper.appendChild(tag);
wrapper.classList.add('ce-link');
/**
* Bind callbacks
**/
tag.addEventListener('paste', methods.blockPasteCallback, false);
tag.addEventListener('keydown', methods.blockKeyDownCallback, false);
return wrapper;
}
};
link_plugin.validate = function (data) {
if (data.url.trim() == '' || data.title.trim() == '' || data.description.trim() == '')
return;
return true;
};
/**
* Method to extract JSON data from HTML block
*/
link_plugin.save = function (blockContent) {
var linkElement = settings.elementClasses.link;
var data = {
url : blockContent.querySelector('.' + linkElement).href,
shortLink : blockContent.querySelector('.' + linkElement).textContent || '',
image : blockContent.querySelector('.' + settings.elementClasses.image).src || '',
title : blockContent.querySelector('.' + settings.elementClasses.title).textContent || '',
description : blockContent.querySelector('.' + settings.elementClasses.description).textContent || ''
};
return data;
};
link_plugin.destroy = function () {
link = null;
};
return link_plugin;
})({});

View file

@ -0,0 +1,209 @@
/**
* Code Plugin\
* Creates code tag and adds content to this tag
*/
var list = (function (list_plugin) {
/**
* CSS class names
*/
var elementClasses_ = {
pluginWrapper: 'cdx-plugin-list',
li: 'cdx-plugin-list__li',
settings: 'cdx-plugin-list__settings',
settingsItem: 'cdx-plugin-settings__item'
};
var LIST_ITEM_TAG = 'LI';
var ui = {
make: function (blockType) {
var wrapper = this.block(blockType || 'UL', elementClasses_.pluginWrapper);
wrapper.dataset.type = blockType;
wrapper.contentEditable = true;
wrapper.addEventListener('keydown', methods_.keyDown);
return wrapper;
},
block: function (blockType, blockClass) {
var block = document.createElement(blockType);
if (blockClass) block.classList.add(blockClass);
return block;
},
button: function (buttonType) {
var types = {
unordered: '<i class="ce-icon-list-bullet"></i>Обычный',
ordered: '<i class="ce-icon-list-numbered"></i>Нумерованный'
},
button = document.createElement('DIV');
button.innerHTML = types[buttonType];
button.classList.add(elementClasses_.settingsItem);
return button;
}
};
var methods_ = {
/**
* Changes block type => OL or UL
* @param event
* @param blockType
*/
changeBlockStyle : function (event, blockType) {
var currentBlock = codex.editor.content.currentNode,
newEditable = ui.make(blockType),
oldEditable = currentBlock.querySelector('[contenteditable]');
newEditable.dataset.type = blockType;
newEditable.innerHTML = oldEditable.innerHTML;
newEditable.classList.add(elementClasses_.pluginWrapper);
codex.editor.content.switchBlock(currentBlock, newEditable, 'list');
},
keyDown: function (e) {
var controlKeyPressed = e.ctrlKey || e.metaKey,
keyCodeForA = 65;
/**
* If CTRL+A (CMD+A) was pressed, we should select only one list item,
* not all <OL> or <UI>
*/
if (controlKeyPressed && e.keyCode == keyCodeForA) {
e.preventDefault();
/**
* Select <LI> content
*/
methods_.selectListItem();
}
},
/**
* Select all content of <LI> with caret
*/
selectListItem : function () {
var selection = window.getSelection(),
currentSelectedNode = selection.anchorNode.parentNode,
range = new Range();
/**
* Search for <LI> element
*/
while ( currentSelectedNode && currentSelectedNode.tagName != LIST_ITEM_TAG ) {
currentSelectedNode = currentSelectedNode.parentNode;
}
range.selectNodeContents(currentSelectedNode);
selection.removeAllRanges();
selection.addRange(range);
}
};
/**
* Method to render HTML block from JSON
*/
list_plugin.render = function (data) {
var type = data && (data.type == 'ordered' || data.type == 'OL') ? 'OL' : 'UL',
tag = ui.make(type),
newLi;
if (data && data.items) {
data.items.forEach(function (element, index, array) {
newLi = ui.block('li', elementClasses_.li);
newLi.innerHTML = element || '';
tag.appendChild(newLi);
});
} else {
newLi = ui.block('li', elementClasses_.li);
tag.appendChild(newLi);
}
return tag;
};
list_plugin.validate = function (data) {
var isEmpty = data.items.every(function (item) {
return item.trim() === '';
});
if (isEmpty) {
return;
}
if (data.type != 'UL' && data.type != 'OL') {
console.warn('CodeX Editor List-tool: wrong list type passed %o', data.type);
return;
}
return true;
};
/**
* Method to extract JSON data from HTML block
*/
list_plugin.save = function (blockContent) {
var data = {
type : null,
items : []
},
litsItemContent = '',
isEmptyItem = false;
for (var index = 0; index < blockContent.childNodes.length; index++) {
litsItemContent = blockContent.childNodes[index].innerHTML;
isEmptyItem = !blockContent.childNodes[index].textContent.trim();
if (!isEmptyItem) {
data.items.push(litsItemContent);
}
}
data.type = blockContent.dataset.type;
return data;
};
list_plugin.makeSettings = function () {
var holder = document.createElement('DIV');
/** Add holder classname */
holder.className = elementClasses_.settings;
var orderedButton = ui.button('ordered'),
unorderedButton = ui.button('unordered');
orderedButton.addEventListener('click', function (event) {
methods_.changeBlockStyle(event, 'OL');
codex.editor.toolbar.settings.close();
});
unorderedButton.addEventListener('click', function (event) {
methods_.changeBlockStyle(event, 'UL');
codex.editor.toolbar.settings.close();
});
holder.appendChild(orderedButton);
holder.appendChild(unorderedButton);
return holder;
};
list_plugin.destroy = function () {
list = null;
};
return list_plugin;
})({});

View file

Before

Width:  |  Height:  |  Size: 13 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View file

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -0,0 +1,532 @@
/**
*
* Quote plugin
*/
var quote = (function (quote_plugin) {
/**
* @private
*
* CSS styles
*/
var elementClasses_ = {
ce_quote : 'ce-quote',
quoteText : 'ce_quote--text',
quoteAuthor : 'ce_quote--author',
authorsJob : 'ce_quote--job',
authorsPhoto : 'authorsPhoto',
authorsPhotoWrapper : 'authorsPhoto-wrapper',
authorsPhotoWrapper_preview : 'authorsPhotoWrapper_preview',
simple : {
text : 'quoteStyle-simple--text'
},
withCaption : {
blockquote : 'quoteStyle-withCaption--blockquote',
author : 'quoteStyle-withCaption--author'
},
withPhoto : {
photo : 'quoteStyle-withPhoto--photo',
author : 'quoteStyle-withPhoto--author',
job : 'quoteStyle-withPhoto--job',
quote : 'quoteStyle-withPhoto--quote',
wrapper : 'quoteStyle-withPhoto--wrapper',
authorHolder : 'quoteStyle-withPhoto--authorWrapper'
},
settings : {
holder : 'cdx-plugin-settings--horisontal',
caption : 'ce_plugin_quote--caption',
buttons : 'cdx-plugin-settings__item',
selectedType : 'ce-quote-settings--selected'
}
};
/**
* @private
*
*
*/
var methods_ = {
changeStyleClicked : function () {
var changeStyleButton = this,
quote = codex.editor.content.currentNode.querySelector('.' + elementClasses_.ce_quote),
newStyle = changeStyleButton.dataset.style,
styleSelectors = this.parentNode.childNodes;
quote.dataset.quoteStyle = newStyle;
/**
* Mark selected style button
*/
for (var i = styleSelectors.length - 1; i >= 0; i--) {
styleSelectors[i].classList.remove(elementClasses_.settings.selectedType);
}
this.classList.add(elementClasses_.settings.selectedType);
},
/**
* @deprecated
*/
selectTypeQuoteStyle : function (type) {
var quoteStyleFunction;
/**
* Choose Quote style to replace
*/
switch (type) {
case 'simple':
quoteStyleFunction = methods_.makeSimpleQuote;
break;
case 'withCaption':
quoteStyleFunction = methods_.makeQuoteWithCaption;
break;
case 'withPhoto':
quoteStyleFunction = methods_.makeQuoteWithPhoto;
break;
}
return quoteStyleFunction;
},
/**
* @deprecated
*/
addSelectTypeClickListener : function (el, quoteStyle) {
el.addEventListener('click', function () {
/**
* Parsing currentNode to JSON.
*/
var parsedOldQuote = methods_.parseBlockQuote(),
newStyledQuote = quoteStyle(parsedOldQuote);
var wrapper = codex.editor.content.composeNewBlock(newStyledQuote, 'quote');
wrapper.appendChild(newStyledQuote);
codex.editor.content.switchBlock(codex.editor.content.currentNode, newStyledQuote, 'quote');
/** Close settings after replacing */
codex.editor.toolbar.settings.close();
}, false);
},
/**
* @deprecated
*/
makeSimpleQuote : function (data) {
var wrapper = ui_.makeBlock('BLOCKQUOTE', [elementClasses_.simple.text, elementClasses_.quoteText]);
wrapper.innerHTML = data.text || '';
wrapper.dataset.quoteStyle = 'simple';
wrapper.classList.add(elementClasses_.ce_quote);
wrapper.contentEditable = 'true';
return wrapper;
},
/**
* @deprecated
*/
makeQuoteWithCaption : function (data) {
var wrapper = ui_.blockquote(),
text = ui_.makeBlock('DIV', [elementClasses_.withCaption.blockquote, elementClasses_.quoteText]),
author = ui_.makeBlock('DIV', [elementClasses_.withCaption.author, elementClasses_.quoteAuthor]);
/* make text block ontentEditable */
text.contentEditable = 'true';
text.innerHTML = data.text || '';
/* make Author contentEditable */
author.contentEditable = 'true';
author.innerHTML = data.cite || '';
/* Appending created components */
wrapper.dataset.quoteStyle = 'withCaption';
wrapper.classList.add(elementClasses_.ce_quote);
wrapper.appendChild(text);
wrapper.appendChild(author);
return wrapper;
},
makeQuoteWithPhoto : function (data) {
var wrapper = ui_.blockquote(),
photo = ui_.makeBlock('DIV', [ elementClasses_.withPhoto.photo ]),
author = ui_.makeBlock('DIV', [elementClasses_.withPhoto.author, elementClasses_.quoteAuthor]),
job = ui_.makeBlock('DIV', [elementClasses_.withPhoto.job, elementClasses_.authorsJob]),
quote = ui_.makeBlock('DIV', [elementClasses_.withPhoto.quote, elementClasses_.quoteText]);
/* Default Image src */
if (!data.image) {
var icon = ui_.makeBlock('SPAN', [ 'ce-icon-picture' ]);
photo.appendChild(icon);
} else {
var authorsPhoto = ui_.img(elementClasses_.authorsPhoto);
authorsPhoto.src = data.image;
authorsPhoto.dataset.bigUrl = data.image;
photo.classList.add(elementClasses_.authorsPhotoWrapper);
photo.appendChild(authorsPhoto);
}
photo.addEventListener('click', fileUploadClicked_, false);
/* make author block contentEditable */
author.contentEditable = 'true';
author.innerHTML = data.cite || '';
/* Author's position and job */
job.contentEditable = 'true';
job.innerHTML = data.caption || '';
var authorsWrapper = ui_.makeBlock('DIV', [ elementClasses_.withPhoto.authorHolder ]);
authorsWrapper.appendChild(author);
authorsWrapper.appendChild(job);
/* make quote text contentEditable */
quote.contentEditable = 'true';
quote.innerHTML = data.text || '';
wrapper.classList.add(elementClasses_.ce_quote);
wrapper.classList.add(elementClasses_.withPhoto.wrapper);
wrapper.dataset.quoteStyle = 'withPhoto';
wrapper.appendChild(quote);
wrapper.appendChild(photo);
wrapper.appendChild(authorsWrapper);
return wrapper;
},
parseBlockQuote : function (block) {
var currentNode = block || codex.editor.content.currentNode,
photo = currentNode.getElementsByTagName('img')[0],
author = currentNode.querySelector('.' + elementClasses_.quoteAuthor),
job = currentNode.querySelector('.' + elementClasses_.authorsJob),
quote ;
/** Simple quote text placed in Blockquote tag*/
if ( currentNode.dataset.quoteStyle == 'simple' ) {
quote = currentNode.innerHTML || '';
} else {
quote = currentNode.querySelector('.' + elementClasses_.quoteText).innerHTML;
}
if (job) {
job = job.innerHTML || '';
}
if (author) {
author = author.innerHTML || '';
}
if (photo) {
photo = photo.dataset.bigUrl;
}
var data = {
style : currentNode.dataset.quoteStyle,
text : quote,
author : author,
job : job,
photo : photo
};
return data;
}
};
/**
* @private
*
* Author image Uploader
*/
var fileUploadClicked_ = function () {
var beforeSend = photoUploadingCallbacks_.beforeSend,
success = photoUploadingCallbacks_.success,
error = photoUploadingCallbacks_.error;
codex.editor.transport.selectAndUpload({
beforeSend: beforeSend,
success: success,
error: error
});
};
/**
* @private
*
*/
var ui_ = {
wrapper : function ($classList) {
var el = document.createElement('DIV');
el.classList.add($classList);
return el;
},
blockquote : function () {
var el = document.createElement('BLOCKQUOTE');
return el;
},
img : function (attribute) {
var imageTag = document.createElement('IMG');
imageTag.classList.add(attribute);
return imageTag;
},
makeBlock : function (tag, classList) {
var el = document.createElement(tag);
if ( classList ) {
for( var i = 0; i < classList.length; i++)
el.className += ' ' + classList[i];
}
return el;
}
};
/**
* @private
*
* Callbacks
*/
var photoUploadingCallbacks_ = {
preview_ : function (e) {
var uploadImageWrapper = codex.editor.content.currentNode.querySelector('.' + elementClasses_.withPhoto.photo),
authorsPhoto = ui_.img(elementClasses_.authorsPhoto);
/** Appending uploaded image */
uploadImageWrapper.classList.add(elementClasses_.authorsPhotoWrapper, elementClasses_.authorsPhotoWrapper_preview);
authorsPhoto.src = e.target.result;
/** Remove icon from image wrapper */
uploadImageWrapper.innerHTML = '';
uploadImageWrapper.appendChild(authorsPhoto);
},
beforeSend : function () {
var input = codex.editor.transport.input,
files = input.files,
file = files[0],
fileReader = new FileReader();
fileReader.readAsDataURL(file);
fileReader.onload = photoUploadingCallbacks_.preview_;
},
/**
* Success callbacks for uploaded photo.
* Replace upload icon with uploaded photo
*/
success : function (result) {
var parsed = JSON.parse(result),
filename = parsed.filename,
uploadImageWrapper = codex.editor.content.currentNode.querySelector('.' + elementClasses_.withPhoto.photo);
var img = uploadImageWrapper.querySelector('IMG');
img.src = parsed.data.file.bigUrl;
img.dataset.bigUrl = parsed.data.file.bigUrl;
uploadImageWrapper.classList.remove(elementClasses_.authorsPhotoWrapper_preview);
},
/** Error callback. Sends notification to user that something happend or plugin doesn't supports method */
error : function (result) {
console.log('Can\'t upload an image');
}
};
/**
* @private
*
* Make Quote from JSON datasets
*/
var make_ = function (data) {
var tag;
if (data && data.size) {
data.style = quote_plugin.config.defaultStyle;
/**
* Supported types
*/
switch (data.style) {
case 'simple':
tag = methods_.makeSimpleQuote(data);
break;
case 'withCaption':
tag = methods_.makeQuoteWithCaption(data);
break;
case 'withPhoto':
tag = methods_.makeQuoteWithPhoto(data);
break;
}
tag.dataset.quoteStyle = data.size;
} else {
var settings = {
'text' : null,
'format' : 'html',
'cite' : null,
'caption': null,
'size' : null,
'image' : null
};
tag = methods_.makeQuoteWithPhoto(settings);
}
return tag;
};
var prepareDataForSave_ = function (data) {
if (data.size == 'withPhoto') {
data.size = 'small';
}
/** Make paragraphs */
data.text = codex.editor.content.wrapTextWithParagraphs(data.text);
return data;
};
/**
* @public
*
* Renderer
*
* @param data
*/
quote_plugin.render = function (data) {
return make_(data);
};
quote_plugin.validate = function (output) {
if (typeof output.text != 'string') {
return;
}
return output;
};
quote_plugin.save = function (blockContent) {
/**
* Extracts JSON quote data from HTML block
* @param {Text} text, {Text} author, {Object} photo
*/
var parsedblock = methods_.parseBlockQuote(blockContent);
var outputData = {
'text' : parsedblock.text,
'format' : 'html',
'cite' : parsedblock.author || '',
'caption': parsedblock.job || '',
'size' : parsedblock.style,
'image' : parsedblock.photo
};
return prepareDataForSave_(outputData);
};
/**
* @public
*
* Draws settings
*/
quote_plugin.makeSettings = function (data) {
var holder = document.createElement('DIV'),
types = {
big : 'По центру',
small : 'Врезка'
},
selectTypeButton;
/** Add holder classname */
holder.className = elementClasses_.settings.holder;
/** Now add type selectors */
for (var type in types) {
selectTypeButton = document.createElement('SPAN');
selectTypeButton.textContent = types[type];
selectTypeButton.className = elementClasses_.settings.buttons;
selectTypeButton.dataset.style = type;
if ( type == quote_plugin.config.defaultStyle ) {
selectTypeButton.classList.add(quoteTools.styles.settings.selectedType);
}
// var quoteStyle = quoteTools.selectTypeQuoteStyle(type);
selectTypeButton.addEventListener('click', methods_.changeStyleClicked, false);
// quoteTools.addSelectTypeClickListener(selectTypeButton, quoteStyle);
holder.appendChild(selectTypeButton);
}
return holder;
};
/**
* @public
* Default path to redactors images
* @type {null}
*/
quote_plugin.path = null;
/**
* @public
*
* @type {null}
*/
quote_plugin.config = null;
/**
* @public
*
* @param config
*/
quote_plugin.prepare = function (config) {
quote_plugin.config = config;
return Promise.resolve();
};
quote_plugin.destroy = function () {
quote = null;
};
return quote_plugin;
})({});

View file

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

Before

Width:  |  Height:  |  Size: 1.5 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

View file

@ -0,0 +1,39 @@
/**
* Plugin for CodeX.Editor
* Implements RAW-data block
*/
var rawPlugin = function (plugin) {
var editor = codex.editor;
plugin.render = function (data) {
var input = editor.draw.node('TEXTAREA', 'raw-plugin__input', {});
input.placeholder = 'Вставьте HTML код';
if (data && data.raw) {
input.value = data.raw;
}
return input;
};
plugin.save = function (block) {
return {
raw: block.value
};
};
plugin.validate = function (data) {
if (data.raw.trim() === '') {
return;
}
return true;
};
plugin.destroy = function () {
rawPlugin = null;
};
return plugin;
}({});

View file

@ -2,11 +2,21 @@
* Empty paragraph placeholder
*/
.ce-paragraph {
padding: 0.7em 0 !important;
line-height: 1.7em;
.ce-text {
padding: 10px 0;
line-height: 1.6em;
outline: none;
}
.ce-text p:first-of-type {
margin-top: 0;
}
.ce-text p:last-of-type {
margin-bottom: 0;
}
.ce-paragraph:empty::before,
.ce-paragraph p:empty::before{
content : attr(data-placeholder);
@ -31,4 +41,4 @@
}
.ce-paragraph p:last-of-type{
margin-bottom: 0;
}
}

View file

@ -0,0 +1,171 @@
/**
* Base Text block for the CodeX Editor.
* Represents simple paragraph
*
* @author CodeX Team (team@ifmo.su)
* @copyright CodeX Team 2017
* @license The MIT License (MIT)
* @version 2.0.1
*/
/**
* @typedef {Object} TextData
* @description Tool's input and output data format
* @property {String} text Paragraph's content. Can include HTML tags: <a><b><i>
*/
class Text {
/**
* Should this tools be displayed at the Editor's Toolbox
* @returns {boolean}
* @public
*/
static get displayInToolbox() {
return false;
}
/**
* Class for the Toolbox icon
* @returns {string}
* @public
*/
static get iconClassName() {
return 'cdx-text-icon';
}
/**
* Render plugin`s main Element and fill it with saved data
* @param {TextData} savedData previously saved data
*/
constructor(savedData = {}) {
this._CSS = {
wrapper: 'ce-text'
};
this._data = {};
this._element = this.drawView();
this.data = savedData;
}
/**
* Create Tool's view
* @return {HTMLElement}
* @private
*/
drawView() {
let div = document.createElement('DIV');
div.classList.add(this._CSS.wrapper);
div.contentEditable = true;
return div;
}
/**
* Return Tool's view
* @returns {HTMLDivElement}
* @public
*/
render() {
return this._element;
}
/**
* Method that specified how to merge two Text blocks.
* Called by CodeX Editor by backspace at the beginning of the Block
* @param {TextData} data
* @public
*/
merge(data) {
let newData = {
text : this.data.text + data.text
};
this.data = newData;
}
/**
* Validate Text block data:
* - check for emptiness
*
* @param {TextData} savedData data received after saving
* @returns {boolean} false if saved data is not correct, otherwise true
* @public
*/
validate(savedData) {
if (savedData.text.trim() === '') {
return false;
}
return true;
}
/**
* Extract Tool's data from the view
* @param {HTMLDivElement} toolsContent - Text tools rendered view
* @returns {TextData} - saved data
* @public
*/
save(toolsContent) {
/**
* @todo sanitize data
*/
return {
text: toolsContent.innerHTML
};
}
/**
* Get current Tools`s data
* @returns {TextData} Current data
* @private
*/
get data() {
let text = this._element.innerHTML;
this._data.text = text;
return this._data;
}
/**
* Store data in plugin:
* - at the this._data property
* - at the HTML
*
* @param {TextData} data data to set
* @private
*/
set data(data) {
this._data = data || {};
this._element.innerHTML = this._data.text || '';
}
/**
* Handle pasted DIV and P tags.
*
* @private
* @param {HTMLElement} content - pasted element
* @returns {{text: string}}
*/
static onPasteHandler(content) {
return {
text: content.innerHTML
};
}
/**
* Used by Codex Editor paste handling API.
* Provides configuration to handle DIV and P tags.
*
* @returns {{handler: (function(HTMLElement): {text: string}), tags: string[]}}
*/
static get onPaste() {
return {
handler: Text.onPasteHandler,
tags: ['P', 'DIV']
};
}
}

View file

@ -0,0 +1,259 @@
/**
* Twitter plugin
* @version 1.0.0
*/
var twitter = (function (twitter_plugin) {
/**
* User's configuration object
*/
var config_ = {};
/**
* CSS classes
*/
var css_ = {
pluginWrapper : 'cdx-tweet'
};
var methods = {
/**
* Twitter render method appends content after block
* @param tweetId
*/
twitter : function (data, twitterBlock) {
var tweet = methods.drawTwitterHolder(),
twittersCaption = methods.drawTwittersCaptionBlock();
if (data.caption) {
twittersCaption.innerHTML = data.caption;
}
/**
* add created tweet to holder
*/
tweet.appendChild(twitterBlock);
// setTimeout(function() {
window.twttr.widgets.createTweet(data.id_str, twitterBlock).then(tweetInsertedCallback_);
// }, 1000);
tweet.classList.add('ce-redactor__loader');
if (codex.editor.content.currentNode) {
tweet.dataset.statusUrl = data.status_url;
codex.editor.content.switchBlock(codex.editor.content.currentNode, tweet, 'tweet');
}
/**
* in case if we need extra data
*/
if ( !data.user ) {
codex.editor.core.ajax({
url : config_.fetchUrl + '?tweetId=' + data.id_str,
type: 'GET',
success: function (result) {
methods.saveTwitterData(result, tweet);
}
});
} else {
tweet.dataset.profileImageUrl = data.user.profile_image_url;
tweet.dataset.profileImageUrlHttps = data.user.profile_image_url_https;
tweet.dataset.screenName = data.user.screen_name;
tweet.dataset.name = data.user.name;
tweet.dataset.id = +data.id;
tweet.dataset.idStr = data.id_str;
tweet.dataset.text = data.text;
tweet.dataset.createdAt = data.created_at;
tweet.dataset.statusUrl = data.status_url;
tweet.dataset.media = data.media;
tweet.classList.remove('ce-redactor__loader');
}
/**
* add caption to tweet
*/
setTimeout(function () {
tweet.appendChild(twittersCaption);
}, 1000);
return tweet;
},
drawTwitterHolder : function () {
var block = document.createElement('DIV');
block.classList.add(css_.pluginWrapper);
return block;
},
drawTwitterBlock : function () {
var block = codex.editor.draw.node('DIV', '', { height: '20px' });
return block;
},
drawTwittersCaptionBlock : function () {
var block = codex.editor.draw.node('DIV', [ 'ce-twitter__caption' ], { contentEditable : true });
return block;
},
saveTwitterData : function (result, tweet) {
var data = JSON.parse(result),
twitterContent = tweet;
setTimeout(function () {
/**
* Save twitter data via data-attributes
*/
twitterContent.dataset.profileImageUrl = data.user.profile_image_url;
twitterContent.dataset.profileImageUrlHttps = data.user.profile_image_url_https;
twitterContent.dataset.screenName = data.user.screen_name;
twitterContent.dataset.name = data.user.name;
twitterContent.dataset.id = +data.id;
twitterContent.dataset.idStr = data.id_str;
twitterContent.dataset.text = data.text;
twitterContent.dataset.createdAt = data.created_at;
twitterContent.dataset.media = data.entities.urls.length > 0 ? 'false' : 'true';
}, 50);
}
};
/**
* @private
* Fires after tweet widget rendered
*/
function tweetInsertedCallback_(widget) {
var pluginWrapper = findParent_( widget, css_.pluginWrapper );
pluginWrapper.classList.remove('ce-redactor__loader');
}
/**
* @private
* Find closiest parent Element with CSS class
*/
function findParent_(el, cls) {
while ((el = el.parentElement) && !el.classList.contains(cls));
return el;
}
/**
* Prepare twitter scripts
* @param {object} config
*/
twitter_plugin.prepare = function (config) {
/**
* Save configs
*/
config_ = config;
return new Promise(function (resolve, reject) {
codex.editor.core.importScript('https://platform.twitter.com/widgets.js', 'twitter-api').then(function () {
resolve();
}).catch(function () {
reject(Error('Twitter API was not loaded'));
});
});
};
/**
* @private
*
* @param data
* @returns {*}
*/
make_ = function (data) {
if (!data.id || !data.status_url)
return;
if (!data.id_str) {
data.id_str = data.status_url.match(/[^\/]+$/)[0];
}
var twitterBlock = methods.drawTwitterBlock();
var tweet = methods.twitter(data, twitterBlock);
return tweet;
};
twitter_plugin.validate = function (data) {
return true;
};
twitter_plugin.save = function (blockContent) {
var data,
caption = blockContent.querySelector('.ce-twitter__caption');
data = {
media:blockContent.dataset.media,
conversation:false,
user:{
profile_image_url: blockContent.dataset.profileImageUrl,
profile_image_url_https: blockContent.dataset.profileImageUrlHttps,
screen_name: blockContent.dataset.screenName,
name: blockContent.dataset.name
},
id: blockContent.dataset.id || blockContent.dataset.tweetId,
id_str : blockContent.dataset.idStr,
text: blockContent.dataset.text,
created_at: blockContent.dataset.createdAt,
status_url: blockContent.dataset.statusUrl,
caption: caption.innerHTML || ''
};
return data;
};
twitter_plugin.render = function (data) {
return make_(data);
};
twitter_plugin.urlPastedCallback = function (url) {
var tweetId,
arr,
data;
arr = url.split('/');
tweetId = arr.pop();
/** Example */
data = {
'media' : true,
'conversation' : false,
'user' : null,
'id' : +tweetId,
'text' : null,
'created_at' : null,
'status_url' : url,
'caption' : null
};
make_(data);
};
twitter_plugin.pastePatterns = [
{
type: 'twitter',
regex: /http?.+twitter.com?.+\//,
callback: twitter_plugin.urlPastedCallback
}
];
twitter_plugin.destroy = function () {
twitter = null;
delete window.twttr;
};
return twitter_plugin;
})({});

View file

@ -0,0 +1,3 @@
<svg width="19" height="12" viewBox="0 0 19 12" xmlns="http://www.w3.org/2000/svg">
<path fill="#388AE5" d="M17.839 5.525a1.105 1.105 0 0 1-.015 1.547l-4.943 4.943a1.105 1.105 0 1 1-1.562-1.562l4.137-4.137-4.078-4.078A1.125 1.125 0 1 1 12.97.648l4.796 4.796c.026.026.05.053.074.08zm-14.952.791l4.137 4.137a1.105 1.105 0 1 1-1.562 1.562L.519 7.072a1.105 1.105 0 0 1-.015-1.547c.023-.028.048-.055.074-.081L5.374.647a1.125 1.125 0 0 1 1.591 1.591L2.887 6.316z" id="a"/>
</svg>

After

Width:  |  Height:  |  Size: 476 B

View file

@ -0,0 +1,23 @@
/**
* Styles for the Term tool
*
* CodeX Editor Inline Toolbar plugin
*/
.ce-term-tool__icon {
background: url('term.svg') no-repeat center center;
}
.ce-term-tool__icon--active {
background-image: url('term-blue.svg');
}
.marked {
background: rgba(251,241,241,0.78);
color: #C44545;
padding: 4px 6px;
border-radius: 2px;
margin: 0 2px;
font-family: Menlo, Monaco, Consolas, Courier New, monospace;
font-size: 0.9em;
}

View file

@ -0,0 +1,156 @@
/**
* Term plugin for the CodeX Editor
*
* Allows to wrap inline fragment and style it somehow.
*/
class Term {
/**
* @param {object} api - CodeX Editor API
*/
constructor(api) {
this.api = api;
/**
* Toolbar Button
*
* @type {HTMLElement|null}
*/
this.button = null;
/**
* Tag represented the term
*
* @type {string}
*/
this.tag = 'SPAN';
/**
* Class name for term-tag
*
* @type {string}
*/
this.CSS = 'marked';
/**
* CSS classes
*/
this.iconClasses = {
base: 'ce-inline-tool',
term: 'ce-term-tool__icon',
active: 'ce-term-tool__icon--active'
};
}
/**
* Specifies Tool as Inline Toolbar Tool
*
* @return {boolean}
*/
static get isInline() {
return true;
}
/**
* Create button element for Toolbar
*
* @return {HTMLElement}
*/
render() {
this.button = document.createElement('button');
this.button.classList.add(this.iconClasses.base, this.iconClasses.term);
return this.button;
}
/**
* Wrap/Unwrap selected fragment
*
* @param {Range} range - selected fragment
*/
surround(range) {
if (!range) {
return;
}
let termWrapper = this.api.selection.findParentTag(this.tag, this.CSS);
/**
* If start or end of selection is in the highlighted block
*/
if (termWrapper) {
this.unwrap(termWrapper);
} else {
this.wrap(range);
}
}
/**
* Wrap selection with term-tag
*
* @param {Range} range - selected fragment
*/
wrap(range) {
/**
* Create a wrapper for highlighting
*/
let span = document.createElement(this.tag);
span.classList.add(this.CSS);
/**
* SurroundContent throws an error if the Range splits a non-Text node with only one of its boundary points
* @see {@link https://developer.mozilla.org/en-US/docs/Web/API/Range/surroundContents}
*
* // range.surroundContents(span);
*/
span.appendChild(range.extractContents());
range.insertNode(span);
/**
* Expand (add) selection to highlighted block
*/
this.api.selection.expandToTag(span);
}
/**
* Unwrap term-tag
*
* @param {HTMLElement} termWrapper - term wrapper tag
*/
unwrap(termWrapper) {
/**
* Expand selection to all term-tag
*/
this.api.selection.expandToTag(termWrapper);
let sel = window.getSelection();
let range = sel.getRangeAt(0);
let unwrappedContent = range.extractContents();
/**
* Remove empty term-tag
*/
termWrapper.parentNode.removeChild(termWrapper);
/**
* Insert extracted content
*/
range.insertNode(unwrappedContent);
/**
* Restore selection
*/
sel.removeAllRanges();
sel.addRange(range);
}
/**
* Check and change Term's state for current selection
*/
checkState() {
const termTag = this.api.selection.findParentTag(this.tag, this.CSS);
this.button.classList.toggle(this.iconClasses.active, !!termTag);
}
}

View file

@ -0,0 +1,3 @@
<svg width="19" height="12" viewBox="0 0 19 12" xmlns="http://www.w3.org/2000/svg">
<path fill="#7B7E89" d="M17.839 5.525a1.105 1.105 0 0 1-.015 1.547l-4.943 4.943a1.105 1.105 0 1 1-1.562-1.562l4.137-4.137-4.078-4.078A1.125 1.125 0 1 1 12.97.648l4.796 4.796c.026.026.05.053.074.08zm-14.952.791l4.137 4.137a1.105 1.105 0 1 1-1.562 1.562L.519 7.072a1.105 1.105 0 0 1-.015-1.547c.023-.028.048-.055.074-.081L5.374.647a1.125 1.125 0 0 1 1.591 1.591L2.887 6.316z" id="a"/>
</svg>

After

Width:  |  Height:  |  Size: 478 B

Binary file not shown.

View file

@ -1,72 +0,0 @@
<?xml version="1.0" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg xmlns="http://www.w3.org/2000/svg">
<metadata>Copyright (C) 2016 by original authors @ fontello.com</metadata>
<defs>
<font id="codex-editor" horiz-adv-x="1000" >
<font-face font-family="codex-editor" font-weight="400" font-stretch="normal" units-per-em="1000" ascent="850" descent="-150" />
<missing-glyph horiz-adv-x="1000" />
<glyph glyph-name="instagram" unicode="&#xe800;" d="M690 350q0 26-6 50l176 0 0-344q0-56-39-96t-95-40l-592 0q-56 0-95 40t-39 96l0 344 174 0q-4-32-4-50 0-106 76-183t184-77q106 0 183 77t77 183z m36 430q56 0 95-39t39-95l0-146-218 0q-78 110-212 110-138 0-212-110l-218 0 0 146q0 56 39 95t95 39l592 0z m64-166l0 72q0 24-24 24l-72 0q-24 0-24-24l0-72q0-8 7-16t17-8l72 0q24 0 24 24z m-200-264q0-66-47-113t-113-47-113 47-47 113q0 68 47 114t113 46 113-46 47-114z" horiz-adv-x="860" />
<glyph glyph-name="picture" unicode="&#xe801;" d="M357 529q0-45-31-76t-76-32-76 32-31 76 31 75 76 32 76-32 31-75z m572-215v-250h-786v107l178 179 90-89 285 285z m53 393h-893q-7 0-12-5t-6-13v-678q0-7 6-13t12-5h893q7 0 13 5t5 13v678q0 8-5 13t-13 5z m89-18v-678q0-37-26-63t-63-27h-893q-36 0-63 27t-26 63v678q0 37 26 63t63 27h893q37 0 63-27t26-63z" horiz-adv-x="1071.4" />
<glyph glyph-name="cog" unicode="&#xe802;" d="M911 295l-133-56q-8-22-12-31l55-133-79-79-135 53q-9-4-31-12l-55-134-112 0-56 133q-11 4-33 13l-132-55-78 79 53 134q-1 3-4 9t-6 12-4 11l-131 55 0 112 131 56 14 33-54 132 78 79 133-54q22 9 33 13l55 132 112 0 56-132q14-5 31-13l133 55 80-79-54-135q6-12 12-30l133-56 0-112z m-447-111q69 0 118 48t49 118-49 119-118 50-119-50-49-119 49-118 119-48z" horiz-adv-x="928" />
<glyph glyph-name="link" unicode="&#xe803;" d="M812 171q0 23-15 38l-116 116q-16 16-38 16-24 0-40-18 1-1 10-10t12-12 9-11 7-14 2-15q0-23-16-38t-38-16q-8 0-15 2t-14 7-11 9-12 12-10 10q-19-17-19-40 0-23 16-38l115-116q15-15 38-15 22 0 38 15l82 81q15 16 15 37z m-392 394q0 22-15 38l-115 115q-16 16-38 16-22 0-38-15l-82-82q-16-15-16-37 0-22 16-38l116-116q15-15 38-15 23 0 40 17-2 2-11 11t-12 12-8 10-7 14-2 16q0 22 15 38t38 15q9 0 16-2t14-7 10-8 12-12 11-11q18 17 18 41z m500-394q0-67-48-113l-82-81q-46-47-113-47-68 0-114 48l-115 115q-46 47-46 114 0 68 49 116l-49 49q-48-49-116-49-67 0-114 47l-116 116q-47 47-47 114t47 113l82 82q47 46 114 46 67 0 114-47l114-116q47-46 47-113 0-69-49-117l49-49q48 49 116 49 67 0 114-47l116-116q47-47 47-114z" horiz-adv-x="928.6" />
<glyph glyph-name="unlink" unicode="&#xe804;" d="M245 141l-143-143q-5-5-13-5-6 0-13 5-5 5-5 13t5 13l143 142q6 5 13 5t13-5q5-5 5-12t-5-13z m94-23v-179q0-8-5-13t-13-5-12 5-5 13v179q0 8 5 13t12 5 13-5 5-13z m-125 125q0-8-5-13t-13-5h-178q-8 0-13 5t-5 13 5 13 13 5h178q8 0 13-5t5-13z m706-72q0-67-48-113l-82-81q-46-47-113-47-68 0-114 48l-186 187q-12 11-24 31l134 10 152-153q15-15 38-15t38 15l82 81q15 16 15 37 0 23-15 38l-153 154 10 133q20-12 31-23l188-188q47-48 47-114z m-345 404l-133-10-152 153q-16 16-38 16-22 0-38-15l-82-82q-16-15-16-37 0-22 16-38l153-153-10-134q-20 12-32 24l-187 187q-47 48-47 114 0 67 47 113l82 82q47 46 114 46 67 0 114-47l186-187q12-12 23-32z m354-46q0-8-5-13t-13-5h-179q-8 0-13 5t-5 13 5 12 13 5h179q8 0 13-5t5-12z m-304 303v-178q0-8-5-13t-13-5-13 5-5 13v178q0 8 5 13t13 5 13-5 5-13z m227-84l-143-143q-6-5-13-5t-12 5q-5 6-5 13t5 13l142 143q6 5 13 5t13-5q5-6 5-13t-5-13z" horiz-adv-x="928.6" />
<glyph glyph-name="code" unicode="&#xe805;" d="M344 69l-28-28q-5-5-12-5t-13 5l-260 260q-6 6-6 13t6 13l260 260q5 6 13 6t12-6l28-28q6-5 6-13t-6-12l-219-220 219-219q6-6 6-13t-6-13z m330 596l-208-721q-2-7-9-11t-13-1l-34 9q-8 3-11 9t-2 14l208 720q3 8 9 11t13 2l35-10q7-2 11-9t1-13z m367-364l-260-260q-6-5-13-5t-13 5l-28 28q-5 6-5 13t5 13l219 219-219 220q-5 5-5 12t5 13l28 28q6 6 13 6t13-6l260-260q5-5 5-13t-5-13z" horiz-adv-x="1071.4" />
<glyph glyph-name="quote" unicode="&#xe806;" d="M146 680q146 0 184-146 38-140-40-302-80-168-224-204-32-8-66-8l0 70q112 0 182 108 54 86 26 146-16 36-62 36-60 0-103 44t-43 106 43 106 103 44z m420 0q146 0 184-146 38-140-40-302-80-168-224-204-32-8-66-8l0 70q112 0 182 108 54 86 26 146-16 36-62 36-60 0-103 44t-43 106 43 106 103 44z" horiz-adv-x="762" />
<glyph glyph-name="trash" unicode="&#xe807;" d="M50 458q122-70 330-70t330 70l-54-486q-2-14-35-36t-100-43-141-21-140 21-100 43-36 36z m488 300q94-18 158-55t64-71l0-10q0-58-112-99t-268-41-268 41-112 99l0 10q0 34 64 71t158 55l42 48q22 26 70 26l92 0q52 0 70-26z m-54-112l84 0q-92 110-104 126-14 16-32 16l-102 0q-22 0-32-16l-106-126 84 0 64 66 82 0z" horiz-adv-x="760" />
<glyph glyph-name="down-big" unicode="&#xe808;" d="M899 386q0-30-21-50l-363-364q-22-21-51-21-29 0-50 21l-363 364q-21 20-21 50 0 29 21 51l41 41q22 21 51 21 29 0 50-21l164-164v393q0 29 21 50t51 22h71q29 0 50-22t21-50v-393l164 164q21 21 51 21 29 0 50-21l42-41q21-22 21-51z" horiz-adv-x="928.6" />
<glyph glyph-name="up-big" unicode="&#xe809;" d="M899 308q0-28-21-50l-42-42q-21-21-50-21-30 0-51 21l-164 164v-393q0-29-20-47t-51-19h-71q-30 0-51 19t-21 47v393l-164-164q-20-21-50-21t-50 21l-42 42q-21 21-21 50 0 30 21 51l363 363q20 21 50 21 30 0 51-21l363-363q21-22 21-51z" horiz-adv-x="928.6" />
<glyph glyph-name="header" unicode="&#xe80a;" d="M939-79q-25 0-74 2t-75 2q-24 0-73-2t-74-2q-14 0-21 12t-7 25q0 17 9 26t22 9 29 4 25 9q18 11 18 78l0 218q0 12-1 17-7 3-28 3h-376q-22 0-29-3 0-5 0-17l-1-207q0-79 21-92 9-5 26-7t32-2 25-8 11-26q0-14-7-26t-20-13q-26 0-78 2t-77 2q-24 0-71-2t-71-2q-13 0-20 12t-7 25q0 17 9 25t20 10 26 4 24 9q18 13 18 80l-1 31v454q0 2 1 14t0 21-1 21-2 24-4 20-6 18-9 10q-8 5-25 6t-29 2-23 7-10 26q0 14 6 26t20 13q26 0 78-2t77-2q23 0 71 2t70 2q14 0 21-13t7-26q0-17-9-25t-22-8-28-2-24-7q-19-12-19-90l1-178q0-12 0-18 7-2 22-2h390q14 0 21 2 1 6 1 18l0 178q0 78-19 90-10 6-33 7t-37 7-14 28q0 14 7 26t21 13q24 0 74-2t73-2q24 0 72 2t72 2q14 0 21-13t7-26q0-17-10-25t-22-8-29-2-24-7q-20-13-20-90l1-526q0-66 19-78 9-6 25-8t30-2 23-9 10-25q0-14-6-26t-20-13z" horiz-adv-x="1000" />
<glyph glyph-name="paragraph" unicode="&#xe80b;" d="M713 745v-41q0-16-10-34t-24-18q-28 0-30-1-15-3-18-17-2-6-2-36v-643q0-14-10-24t-24-10h-60q-14 0-24 10t-10 24v680h-80v-680q0-14-9-24t-25-10h-60q-14 0-24 10t-10 24v277q-82 7-137 33-70 33-107 100-36 65-36 145 0 92 50 159 49 66 116 89 62 21 233 21h267q14 0 24-10t10-24z" horiz-adv-x="714.3" />
<glyph glyph-name="align-left" unicode="&#xe80c;" d="M1000 100v-71q0-15-11-25t-25-11h-928q-15 0-25 11t-11 25v71q0 15 11 25t25 11h928q15 0 25-11t11-25z m-214 214v-71q0-15-11-25t-25-11h-714q-15 0-25 11t-11 25v71q0 15 11 25t25 11h714q15 0 25-11t11-25z m143 215v-72q0-14-11-25t-25-11h-857q-15 0-25 11t-11 25v72q0 14 11 25t25 10h857q14 0 25-10t11-25z m-215 214v-72q0-14-10-25t-25-10h-643q-15 0-25 10t-11 25v72q0 14 11 25t25 11h643q14 0 25-11t10-25z" horiz-adv-x="1000" />
<glyph glyph-name="align-center" unicode="&#xe80d;" d="M1000 100v-71q0-15-11-25t-25-11h-928q-15 0-25 11t-11 25v71q0 15 11 25t25 11h928q15 0 25-11t11-25z m-214 214v-71q0-15-11-25t-25-11h-500q-14 0-25 11t-11 25v71q0 15 11 25t25 11h500q15 0 25-11t11-25z m143 215v-72q0-14-11-25t-25-11h-786q-14 0-25 11t-11 25v72q0 14 11 25t25 10h786q14 0 25-10t11-25z m-215 214v-72q0-14-10-25t-25-10h-358q-14 0-25 10t-10 25v72q0 14 10 25t25 11h358q14 0 25-11t10-25z" horiz-adv-x="1000" />
<glyph glyph-name="align-right" unicode="&#xe80e;" d="M1000 100v-71q0-15-11-25t-25-11h-928q-15 0-25 11t-11 25v71q0 15 11 25t25 11h928q15 0 25-11t11-25z m0 214v-71q0-15-11-25t-25-11h-714q-14 0-25 11t-11 25v71q0 15 11 25t25 11h714q15 0 25-11t11-25z m0 215v-72q0-14-11-25t-25-11h-857q-14 0-25 11t-11 25v72q0 14 11 25t25 10h857q15 0 25-10t11-25z m0 214v-72q0-14-11-25t-25-10h-643q-14 0-25 10t-10 25v72q0 14 10 25t25 11h643q15 0 25-11t11-25z" horiz-adv-x="1000" />
<glyph glyph-name="font" unicode="&#xe80f;" d="M405 538l-95-251q18 0 76-1t89-1q11 0 32 1-48 141-102 252z m-405-617l1 44q13 4 31 7t32 6 28 8 25 17 17 28l132 344 156 404h72q4-8 6-12l114-268q19-44 60-144t63-153q9-19 33-80t40-95q11-25 19-31 11-9 49-17t47-11q4-22 4-32 0-2-1-7t0-8q-35 0-106 5t-107 4q-42 0-120-4t-99-4q0 24 2 43l73 16q1 0 7 1t9 2 8 3 9 4 6 4 5 6 1 8q0 9-17 54t-40 99-24 56l-251 1q-14-32-43-109t-28-91q0-12 8-21t24-14 27-7 32-5 23-2q1-11 1-32 0-5-2-16-32 0-97 6t-97 6q-5 0-15-3t-12-2q-45-8-105-8z" horiz-adv-x="928.6" />
<glyph glyph-name="bold" unicode="&#xe810;" d="M310 1q41-18 78-18 210 0 210 187 0 64-23 101-15 24-35 41t-37 26-45 14-47 6-53 1q-40 0-56-6 0-29 0-88t-1-89q0-4 0-37t0-54 2-47 7-37z m-8 417q23-4 61-4 46 0 80 7t61 25 41 50 15 79q0 39-16 68t-45 46-60 24-69 8q-28 0-73-7 0-28 3-84t2-85q0-15 0-45t-1-44q0-26 1-38z m-302-497l1 53q9 2 48 9t59 15q4 7 7 15t4 19 4 18 1 21 0 19v36q0 548-12 572-2 5-12 8t-25 6-28 4-27 3-17 2l-2 46q55 1 190 6t208 6q13 0 38-1t38 0q39 0 76-7t72-24 60-39 41-59 16-76q0-29-9-54t-22-40-36-32-41-25-47-22q86-20 144-75t57-138q0-56-20-101t-52-72-77-48-91-27-98-8q-25 0-74 2t-74 1q-59 0-171-6t-129-7z" horiz-adv-x="785.7" />
<glyph glyph-name="medium" unicode="&#xe811;" d="M1000 107v-117h-358v117h75v516h-4l-175-633h-136l-173 633h-4v-516h75v-117h-300v117h39q11 0 24 11t12 21v491q0 10-12 22t-24 13h-39v116h375l123-458h4l124 458h374v-116h-40q-10 0-23-13t-12-22v-491q0-11 12-21t23-11h40z" horiz-adv-x="1000" />
<glyph glyph-name="italic" unicode="&#xe812;" d="M0-78l10 48q3 1 45 12t62 21q16 19 23 56 1 4 35 162t63 303 29 165v14q-13 7-30 11t-39 4-32 3l10 58q19-2 67-4t84-4 67-1q27 0 55 1t67 4 55 4q-2-22-10-50-17-6-57-16t-60-19q-5-10-8-23t-5-23-4-25-4-24q-15-82-49-234t-43-198q-1-5-7-32t-11-51-9-46-4-32l1-10q9-3 103-18-2-24-9-55-6 0-18-1t-18-1q-16 0-49 6t-48 6q-77 1-115 1-28 0-79-5t-68-7z" horiz-adv-x="571.4" />
<glyph glyph-name="list-bullet" unicode="&#xe813;" d="M214 64q0-44-31-76t-76-31-76 31-31 76 31 76 76 31 76-31 31-76z m0 286q0-45-31-76t-76-31-76 31-31 76 31 76 76 31 76-31 31-76z m786-232v-107q0-7-5-13t-13-5h-678q-8 0-13 5t-5 13v107q0 7 5 12t13 6h678q7 0 13-6t5-12z m-786 518q0-45-31-76t-76-31-76 31-31 76 31 76 76 31 76-31 31-76z m786-232v-108q0-7-5-12t-13-5h-678q-8 0-13 5t-5 12v108q0 7 5 12t13 5h678q7 0 13-5t5-12z m0 285v-107q0-7-5-12t-13-6h-678q-8 0-13 6t-5 12v107q0 8 5 13t13 5h678q7 0 13-5t5-13z" horiz-adv-x="1000" />
<glyph glyph-name="list-numbered" unicode="&#xe814;" d="M213-54q0-45-31-70t-75-26q-60 0-96 37l31 49q28-25 60-25 16 0 28 8t12 24q0 35-59 31l-14 31q4 6 18 24t24 31 20 21v1q-9 0-27-1t-27 0v-30h-59v85h186v-49l-53-65q28-6 45-27t17-49z m1 350v-89h-202q-4 20-4 30 0 29 14 52t31 38 37 27 31 24 14 25q0 14-9 22t-22 7q-25 0-45-32l-47 33q13 28 40 44t59 16q40 0 68-23t28-63q0-28-19-51t-42-36-42-28-20-30h71v34h59z m786-178v-107q0-8-5-13t-13-5h-678q-8 0-13 5t-5 13v107q0 8 5 13t13 5h678q7 0 13-6t5-12z m-786 502v-56h-187v56h60q0 22 0 68t1 67v7h-1q-5-10-28-30l-40 42 76 71h59v-225h60z m786-216v-108q0-7-5-12t-13-5h-678q-8 0-13 5t-5 12v108q0 7 5 12t13 5h678q7 0 13-5t5-12z m0 285v-107q0-7-5-12t-13-6h-678q-8 0-13 6t-5 12v107q0 8 5 13t13 5h678q7 0 13-5t5-13z" horiz-adv-x="1000" />
<glyph glyph-name="strike" unicode="&#xe815;" d="M982 350q8 0 13-5t5-13v-36q0-7-5-12t-13-5h-964q-8 0-13 5t-5 12v36q0 8 5 13t13 5h964z m-712 36q-16 19-29 44-27 54-27 105 0 101 75 173 74 71 219 71 28 0 94-11 36-7 98-27 6-21 12-66 8-68 8-102 0-10-3-25l-7-2-46 4-8 1q-28 83-58 114-49 51-117 51-64 0-102-33-37-32-37-81 0-41 37-78t156-72q38-12 96-37 33-16 53-29h-414z m282-143h230q4-22 4-51 0-62-23-119-13-30-40-58-20-19-61-45-44-27-85-37-45-12-113-12-64 0-109 13l-78 23q-32 8-40 15-5 5-5 12v8q0 60-1 87 0 17 0 38l1 20v25l57 1q8-19 17-40t12-31 7-15q20-32 45-52 24-20 59-32 33-12 73-12 36 0 78 15 43 14 68 48 26 34 26 72 0 47-45 87-19 16-77 40z" horiz-adv-x="1000" />
<glyph glyph-name="underline" unicode="&#xe816;" d="M27 726q-21 1-25 2l-2 49q7 0 22 0 34 0 63-2 74-4 92-4 48 0 94 2 65 2 82 3 31 0 48 1l-1-8 1-36v-5q-33-5-69-5-33 0-44-14-7-7-7-73 0-8 0-18t0-15l1-127 8-157q3-69 28-112 20-33 54-52 49-26 98-26 58 0 107 16 31 10 55 28 27 20 37 36 20 31 29 63 12 41 12 128 0 44-2 72t-6 68-8 89l-2 33q-3 37-13 49-19 20-43 19l-56-1-8 2 1 48h47l114-6q43-2 110 6l10-2q3-21 3-28 0-4-2-17-25-7-47-8-41-6-44-9-8-8-8-23 0-4 0-15t1-17q5-11 13-221 3-109-9-170-8-42-23-68-21-36-62-69-42-31-102-49-61-19-142-19-93 0-159 26-66 26-99 68-34 42-47 109-9 45-9 132v186q0 105-9 119-14 20-82 22z m830-787v36q0 8-5 13t-13 5h-821q-8 0-13-5t-5-13v-36q0-8 5-13t13-5h821q8 0 13 5t5 13z" horiz-adv-x="857.1" />
<glyph glyph-name="table" unicode="&#xe817;" d="M286 82v107q0 8-5 13t-13 5h-179q-7 0-13-5t-5-13v-107q0-8 5-13t13-5h179q8 0 13 5t5 13z m0 214v108q0 7-5 12t-13 5h-179q-7 0-13-5t-5-12v-108q0-7 5-12t13-5h179q8 0 13 5t5 12z m285-214v107q0 8-5 13t-12 5h-179q-8 0-13-5t-5-13v-107q0-8 5-13t13-5h179q7 0 12 5t5 13z m-285 429v107q0 8-5 13t-13 5h-179q-7 0-13-5t-5-13v-107q0-8 5-13t13-5h179q8 0 13 5t5 13z m285-215v108q0 7-5 12t-12 5h-179q-8 0-13-5t-5-12v-108q0-7 5-12t13-5h179q7 0 12 5t5 12z m286-214v107q0 8-5 13t-13 5h-178q-8 0-13-5t-5-13v-107q0-8 5-13t13-5h178q8 0 13 5t5 13z m-286 429v107q0 8-5 13t-12 5h-179q-8 0-13-5t-5-13v-107q0-8 5-13t13-5h179q7 0 12 5t5 13z m286-215v108q0 7-5 12t-13 5h-178q-8 0-13-5t-5-12v-108q0-7 5-12t13-5h178q8 0 13 5t5 12z m0 215v107q0 8-5 13t-13 5h-178q-8 0-13-5t-5-13v-107q0-8 5-13t13-5h178q8 0 13 5t5 13z m72 178v-607q0-37-27-63t-63-26h-750q-36 0-63 26t-26 63v607q0 37 26 63t63 27h750q37 0 63-27t27-63z" horiz-adv-x="928.6" />
<glyph glyph-name="ellipsis-vert" unicode="&#xe818;" d="M214 154v-108q0-22-15-37t-38-16h-107q-23 0-38 16t-16 37v108q0 22 16 38t38 15h107q22 0 38-15t15-38z m0 285v-107q0-22-15-38t-38-15h-107q-23 0-38 15t-16 38v107q0 23 16 38t38 16h107q22 0 38-16t15-38z m0 286v-107q0-22-15-38t-38-16h-107q-23 0-38 16t-16 38v107q0 22 16 38t38 16h107q22 0 38-16t15-38z" horiz-adv-x="214.3" />
<glyph glyph-name="columns" unicode="&#xe819;" d="M89-7h340v643h-358v-625q0-8 6-13t12-5z m768 18v625h-357v-643h339q8 0 13 5t5 13z m72 678v-678q0-37-27-63t-63-27h-750q-36 0-63 27t-26 63v678q0 37 26 63t63 27h750q37 0 63-27t27-63z" horiz-adv-x="928.6" />
<glyph glyph-name="smile" unicode="&#xe81a;" d="M633 250q-21-67-77-109t-127-41-128 41-77 109q-4 14 3 27t21 18q14 4 27-2t17-22q14-44 52-72t85-28 84 28 52 72q4 15 18 22t27 2 21-18 2-27z m-276 243q0-30-21-51t-50-21-51 21-21 51 21 50 51 21 50-21 21-50z m286 0q0-30-21-51t-51-21-50 21-21 51 21 50 50 21 51-21 21-50z m143-143q0 73-29 139t-76 114-114 76-138 28-139-28-114-76-76-114-29-139 29-139 76-113 114-77 139-28 138 28 114 77 76 113 29 139z m71 0q0-117-57-215t-156-156-215-58-216 58-155 156-58 215 58 215 155 156 216 58 215-58 156-156 57-215z" horiz-adv-x="857.1" />
<glyph glyph-name="newspaper" unicode="&#xe81b;" d="M571 564h-214v-214h214v214z m72-357v-71h-357v71h357z m0 429v-357h-357v357h357z m357-429v-71h-286v71h286z m0 143v-71h-286v71h286z m0 143v-72h-286v72h286z m0 143v-72h-286v72h286z m-857-536v536h-72v-536q0-14 11-25t25-11 25 11 11 25z m928 0v607h-857v-607q0-18-6-36h828q14 0 25 11t10 25z m72 679v-679q0-45-31-76t-76-31h-929q-44 0-76 31t-31 76v607h143v72h1000z" horiz-adv-x="1142.9" />
<glyph glyph-name="twitter" unicode="&#xe81c;" d="M904 622q-37-54-90-93 0-8 0-23 0-73-21-145t-64-139-103-117-144-82-181-30q-151 0-276 81 19-2 43-2 126 0 224 77-59 1-105 36t-64 89q19-3 34-3 24 0 48 6-63 13-104 62t-41 115v2q38-21 82-23-37 25-59 64t-22 86q0 49 25 91 68-83 164-133t208-55q-5 21-5 41 0 75 53 127t127 53q79 0 132-57 61 12 114 44-20-64-79-100 52 6 104 28z" horiz-adv-x="928.6" />
<glyph glyph-name="facebook-squared" unicode="&#xe81d;" d="M696 779q67 0 114-48t47-113v-536q0-66-47-113t-114-48h-104v332h111l16 130h-127v83q0 31 13 46t51 16l68 1v115q-35 5-100 5-75 0-121-45t-45-126v-95h-112v-130h112v-332h-297q-67 0-114 48t-47 113v536q0 66 47 113t114 48h535z" horiz-adv-x="857.1" />
<glyph glyph-name="vkontakte" unicode="&#xe81e;" d="M1070 560q13-36-84-164-13-18-36-48-44-55-50-73-10-23 7-45 10-12 46-46h0l1 0 0-1 1-1q79-73 107-123 2-3 4-7t3-15 0-19-14-15-33-7l-143-3q-13-2-31 3t-29 13l-11 6q-17 12-39 36t-38 43-34 33-32 8q-1 0-4-2t-10-8-12-16-9-29-4-44q0-8-2-15t-4-10l-2-3q-10-11-30-12h-64q-40-3-81 9t-74 29-57 37-40 32l-14 14q-5 5-15 17t-40 50-59 85-68 117-73 152q-4 9-4 15t2 9l2 3q9 11 32 11l153 1q7-1 13-4t9-4l3-2q9-6 13-18 11-28 26-58t23-45l9-16q16-34 31-58t27-38 23-22 19-8 15 3q1 1 3 3t7 12 7 26 5 46 0 69q-1 23-5 41t-7 26l-4 6q-14 19-47 24-8 2 3 14 9 10 21 17 29 14 133 13 46-1 75-7 12-3 19-8t12-13 5-18 2-26 0-30-2-40 0-46q0-6-1-23t0-27 2-22 6-22 13-14q4-1 9-2t15 6 21 19 29 38 38 60q33 58 60 125 2 6 5 10t6 6l2 2 3 1t8 2 11 0l160 1q22 3 36-1t17-10z" horiz-adv-x="1071.4" />
</font>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

Binary file not shown.

View file

@ -1,6 +0,0 @@
<?xml version="1.0" ?>
<!DOCTYPE svg PUBLIC '-//W3C//DTD SVG 1.1//EN' 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'><svg height="32px" style="enable-background:new 0 0 28 32;" version="1.1" viewBox="0 0 28 32" width="28px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><g id="Layer_1"/>
<g id="hash">
<path d="M28,12V8h-5.004l1-8h-4l-1,8h-7.998l1-8h-4l-1,8H0v4h6.498L5.5,20H0v4h5l-1,8h4l1-8h8l-1.002,8H20 l1-8h7v-4h-6.5l0.996-8H28z M17.5,20h-8l0.998-8h7.998L17.5,20z" style="fill:#707684;"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 569 B

View file

@ -1,16 +0,0 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="12px" height="12px" viewBox="0 0 12 12" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 3.7 (28169) - http://www.bohemiancoding.com/sketch -->
<title>Combined Shape</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="redactor-toolbar-opened" transform="translate(-84.000000, -472.000000)" fill="#2b304a">
<g id="+-copy-hovered" transform="translate(76.000000, 265.000000)">
<g id="PLUS" transform="translate(0.000000, 199.000000)">
<path d="M15.125,12.875 L15.125,9.12893087 C15.125,8.50034732 14.6213203,8 14,8 C13.3743479,8 12.875,8.50543957 12.875,9.12893087 L12.875,12.875 L9.12893087,12.875 C8.50034732,12.875 8,13.3786797 8,14 C8,14.6256521 8.50543957,15.125 9.12893087,15.125 L12.875,15.125 L12.875,18.8710691 C12.875,19.4996527 13.3786797,20 14,20 C14.6256521,20 15.125,19.4945604 15.125,18.8710691 L15.125,15.125 L18.8710691,15.125 C19.4996527,15.125 20,14.6213203 20,14 C20,13.3743479 19.4945604,12.875 18.8710691,12.875 L15.125,12.875 Z" id="Combined-Shape" transform="translate(14.000000, 14.000000) rotate(-270.000000) translate(-14.000000, -14.000000) "></path>
</g>
</g>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.4 KiB

View file

@ -1,63 +0,0 @@
@font-face {
font-family: 'codex_editor';
src: url('fonts/codex_editor/codex-editor.eot?20895205');
src: url('fonts/codex_editor/codex-editor.eot?20895205#iefix') format('embedded-opentype'),
url('fonts/codex_editor/codex-editor.woff?20895205') format('woff'),
url('fonts/codex_editor/codex-editor.ttf?20895205') format('truetype'),
url('fonts/codex_editor/codex-editor.svg?20895205#codex_editor') format('svg');
font-weight: normal;
font-style: normal;
}
[class^="ce-icon-"]:before,
[class*="ce-icon-"]:before {
font-family: "codex_editor";
font-style: normal;
font-weight: normal;
speak: none;
display: inline-block;
text-decoration: inherit;
width: 1em;
margin-right: .2em;
text-align: center;
font-variant: normal;
text-transform: none;
line-height: 1em;
/* Animation center compensation - margins should be symmetric */
margin-left: .2em;
-moz-osx-font-smoothing: grayscale;
}
.ce-icon-instagram:before { content: '\e800'; } /* '' */
.ce-icon-picture:before { content: '\e801'; } /* '' */
.ce-icon-cog:before { content: '\e802'; } /* '' */
.ce-icon-link:before { content: '\e803'; } /* '' */
.ce-icon-unlink:before { content: '\e804'; } /* '' */
.ce-icon-code:before { content: '\e805'; } /* '' */
.ce-icon-quote:before { content: '\e806'; } /* '' */
.ce-icon-trash:before { content: '\e807'; } /* '' */
.ce-icon-down-big:before { content: '\e808'; } /* '' */
.ce-icon-up-big:before { content: '\e809'; } /* '' */
.ce-icon-header:before { content: '\e80a'; } /* '' */
.ce-icon-paragraph:before { content: '\e80b'; } /* '' */
.ce-icon-align-left:before { content: '\e80c'; } /* '' */
.ce-icon-align-center:before { content: '\e80d'; } /* '' */
.ce-icon-align-right:before { content: '\e80e'; } /* '' */
.ce-icon-font:before { content: '\e80f'; } /* '' */
.ce-icon-bold:before { content: '\e810'; } /* '' */
.ce-icon-medium:before { content: '\e811'; } /* '' */
.ce-icon-italic:before { content: '\e812'; } /* '' */
.ce-icon-list-bullet:before { content: '\e813'; } /* '' */
.ce-icon-list-numbered:before { content: '\e814'; } /* '' */
.ce-icon-strike:before { content: '\e815'; } /* '' */
.ce-icon-underline:before { content: '\e816'; } /* '' */
.ce-icon-table:before { content: '\e817'; } /* '' */
.ce-icon-ellipsis-vert:before { content: '\e818'; } /* '' */
.ce-icon-columns:before { content: '\e819'; } /* '' */
.ce-icon-smile:before { content: '\e81a'; } /* '' */
.ce-icon-newspaper:before { content: '\e81b'; } /* '' */
.ce-icon-twitter:before { content: '\e81c'; } /* '' */
.ce-icon-facebook-squared:before { content: '\e81d'; } /* '' */
.ce-icon-vkontakte:before { content: '\e81e'; } /* '' */

View file

@ -1,92 +0,0 @@
/**
* Codex Editor Anchors module
*
* @author Codex Team
* @version 1.0
*/
module.exports = function (anchors) {
let editor = codex.editor;
anchors.input = null;
anchors.currentNode = null;
anchors.settingsOpened = function (currentBlock) {
anchors.currentNode = currentBlock;
anchors.input.value = anchors.currentNode.dataset.anchor || '';
};
anchors.anchorChanged = function (e) {
var newAnchor = e.target.value = anchors.rusToTranslit(e.target.value);
anchors.currentNode.dataset.anchor = newAnchor;
if (newAnchor.trim() !== '') {
anchors.currentNode.classList.add(editor.ui.className.BLOCK_WITH_ANCHOR);
} else {
anchors.currentNode.classList.remove(editor.ui.className.BLOCK_WITH_ANCHOR);
}
};
anchors.keyDownOnAnchorInput = function (e) {
if (e.keyCode == editor.core.keys.ENTER) {
e.preventDefault();
e.stopPropagation();
e.target.blur();
editor.toolbar.settings.close();
}
};
anchors.keyUpOnAnchorInput = function (e) {
if (e.keyCode >= editor.core.keys.LEFT && e.keyCode <= editor.core.keys.DOWN) {
e.stopPropagation();
}
};
anchors.rusToTranslit = function (string) {
var ru = [
'А', 'Б', 'В', 'Г', 'Д', 'Е', 'Ё', 'Ж', 'З', 'И', 'Й',
'К', 'Л', 'М', 'Н', 'О', 'П', 'Р', 'С', 'Т', 'У', 'Ф',
'Х', 'Ц', 'Ч', 'Ш', 'Щ', 'Ь', 'Ы', 'Ь', 'Э', 'Ю', 'Я'
],
en = [
'A', 'B', 'V', 'G', 'D', 'E', 'E', 'Zh', 'Z', 'I', 'Y',
'K', 'L', 'M', 'N', 'O', 'P', 'R', 'S', 'T', 'U', 'F',
'H', 'C', 'Ch', 'Sh', 'Sch', '', 'Y', '', 'E', 'Yu', 'Ya'
];
for (var i = 0; i < ru.length; i++) {
string = string.split(ru[i]).join(en[i]);
string = string.split(ru[i].toLowerCase()).join(en[i].toLowerCase());
}
string = string.replace(/[^0-9a-zA-Z_]+/g, '-');
return string;
};
return anchors;
}({});

View file

@ -1,911 +0,0 @@
/**
* @module Codex Editor Callbacks module
* @description Module works with editor added Elements
*
* @author Codex Team
* @version 1.4.0
*/
module.exports = (function (callbacks) {
let editor = codex.editor;
/**
* used by UI module
* @description Routes all keydowns on document
* @param {Object} event
*/
callbacks.globalKeydown = function (event) {
switch (event.keyCode) {
case editor.core.keys.ENTER : enterKeyPressed_(event); break;
}
};
/**
* used by UI module
* @description Routes all keydowns on redactors area
* @param {Object} event
*/
callbacks.redactorKeyDown = function (event) {
switch (event.keyCode) {
case editor.core.keys.TAB : tabKeyPressedOnRedactorsZone_(event); break;
case editor.core.keys.ENTER : enterKeyPressedOnRedactorsZone_(event); break;
case editor.core.keys.ESC : escapeKeyPressedOnRedactorsZone_(event); break;
default : defaultKeyPressedOnRedactorsZone_(event); break;
}
};
/**
* used by UI module
* @description Routes all keyup events
* @param {Object} event
*/
callbacks.globalKeyup = function (event) {
switch (event.keyCode) {
case editor.core.keys.UP :
case editor.core.keys.LEFT :
case editor.core.keys.RIGHT :
case editor.core.keys.DOWN : arrowKeyPressed_(event); break;
}
};
/**
* @param {Object} event
* @private
*
* Handles behaviour when tab pressed
* @description if Content is empty show toolbox (if it is closed) or leaf tools
* uses Toolbars toolbox module to handle the situation
*/
var tabKeyPressedOnRedactorsZone_ = function (event) {
/**
* Wait for solution. Would like to know the behaviour
* @todo Add spaces
*/
event.preventDefault();
if (!editor.core.isBlockEmpty(editor.content.currentNode)) {
return;
}
if ( !editor.toolbar.opened ) {
editor.toolbar.open();
}
if (editor.toolbar.opened && !editor.toolbar.toolbox.opened) {
editor.toolbar.toolbox.open();
} else {
editor.toolbar.toolbox.leaf();
}
};
/**
* Handles global EnterKey Press
* @see enterPressedOnBlock_
* @param {Object} event
*/
var enterKeyPressed_ = function () {
if (editor.content.editorAreaHightlighted) {
/**
* it means that we lose input index, saved index before is not correct
* therefore we need to set caret when we insert new block
*/
editor.caret.inputIndex = -1;
enterPressedOnBlock_();
}
};
/**
* Callback for enter key pressing in first-level block area
*
* @param {Event} event
* @private
*
* @description Inserts new block with initial type from settings
*/
var enterPressedOnBlock_ = function () {
var NEW_BLOCK_TYPE = editor.settings.initialBlockPlugin;
editor.content.insertBlock({
type : NEW_BLOCK_TYPE,
block : editor.tools[NEW_BLOCK_TYPE].render()
}, true );
editor.toolbar.move();
editor.toolbar.open();
};
/**
* ENTER key handler
*
* @param {Object} event
* @private
*
* @description Makes new block with initial type from settings
*/
var enterKeyPressedOnRedactorsZone_ = function (event) {
if (event.target.contentEditable == 'true') {
/** Update input index */
editor.caret.saveCurrentInputIndex();
}
var currentInputIndex = editor.caret.getCurrentInputIndex() || 0,
workingNode = editor.content.currentNode,
tool = workingNode.dataset.tool,
isEnterPressedOnToolbar = editor.toolbar.opened &&
editor.toolbar.current &&
event.target == editor.state.inputs[currentInputIndex];
/** The list of tools which needs the default browser behaviour */
var enableLineBreaks = editor.tools[tool].enableLineBreaks;
/** This type of block creates when enter is pressed */
var NEW_BLOCK_TYPE = editor.settings.initialBlockPlugin;
/**
* When toolbar is opened, select tool instead of making new paragraph
*/
if ( isEnterPressedOnToolbar ) {
event.preventDefault();
editor.toolbar.toolbox.toolClicked(event);
editor.toolbar.close();
/**
* Stop other listeners callback executions
*/
event.stopPropagation();
event.stopImmediatePropagation();
return;
}
/**
* Allow paragraph lineBreaks with shift enter
* Or if shiftkey pressed and enter and enabledLineBreaks, the let new block creation
*/
if ( event.shiftKey || enableLineBreaks ) {
event.stopPropagation();
event.stopImmediatePropagation();
return;
}
var currentSelection = window.getSelection(),
currentSelectedNode = currentSelection.anchorNode,
caretAtTheEndOfText = editor.caret.position.atTheEnd(),
isTextNodeHasParentBetweenContenteditable = false;
/**
* Allow making new <p> in same block by SHIFT+ENTER and forbids to prevent default browser behaviour
*/
if ( event.shiftKey && !enableLineBreaks ) {
editor.callback.enterPressedOnBlock(editor.content.currentBlock, event);
event.preventDefault();
return;
}
/**
* Workaround situation when caret at the Text node that has some wrapper Elements
* Split block cant handle this.
* We need to save default behavior
*/
isTextNodeHasParentBetweenContenteditable = currentSelectedNode && currentSelectedNode.parentNode.contentEditable != 'true';
/**
* Split blocks when input has several nodes and caret placed in textNode
*/
if (
currentSelectedNode.nodeType == editor.core.nodeTypes.TEXT &&
!isTextNodeHasParentBetweenContenteditable &&
!caretAtTheEndOfText
) {
event.preventDefault();
editor.core.log('Splitting Text node...');
editor.content.splitBlock(currentInputIndex);
/** Show plus button when next input after split is empty*/
if (!editor.state.inputs[currentInputIndex + 1].textContent.trim()) {
editor.toolbar.showPlusButton();
}
} else {
var islastNode = editor.content.isLastNode(currentSelectedNode);
if ( islastNode && caretAtTheEndOfText ) {
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();
editor.core.log('ENTER clicked in last textNode. Create new BLOCK');
editor.content.insertBlock({
type: NEW_BLOCK_TYPE,
block: editor.tools[NEW_BLOCK_TYPE].render()
}, true);
editor.toolbar.move();
editor.toolbar.open();
/** Show plus button with empty block */
editor.toolbar.showPlusButton();
}
}
/** get all inputs after new appending block */
editor.ui.saveInputs();
};
/**
* Escape behaviour
* @param event
* @private
*
* @description Closes toolbox and toolbar. Prevents default behaviour
*/
var escapeKeyPressedOnRedactorsZone_ = function (event) {
/** Close all toolbar */
editor.toolbar.close();
/** Close toolbox */
editor.toolbar.toolbox.close();
event.preventDefault();
};
/**
* @param {Event} event
* @private
*
* closes and moves toolbar
*/
var arrowKeyPressed_ = function (event) {
editor.content.workingNodeChanged();
/* Closing toolbar */
editor.toolbar.close();
editor.toolbar.move();
};
/**
* @private
* @param {Event} event
*
* @description Closes all opened bars from toolbar.
* If block is mark, clears highlightning
*/
var defaultKeyPressedOnRedactorsZone_ = function () {
editor.toolbar.close();
if (!editor.toolbar.inline.actionsOpened) {
editor.toolbar.inline.close();
editor.content.clearMark();
}
};
/**
* Handler when clicked on redactors area
*
* @protected
* @param event
*
* @description Detects clicked area. If it is first-level block area, marks as detected and
* on next enter press will be inserted new block
* Otherwise, save carets position (input index) and put caret to the editable zone.
*
* @see detectWhenClickedOnFirstLevelBlockArea_
*
*/
callbacks.redactorClicked = function (event) {
detectWhenClickedOnFirstLevelBlockArea_();
editor.content.workingNodeChanged(event.target);
editor.ui.saveInputs();
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
*/
editor.toolbar.move();
editor.toolbar.open();
var inputIsEmpty = !editor.content.currentNode.textContent.trim(),
currentNodeType = editor.content.currentNode.dataset.tool,
isInitialType = currentNodeType == editor.settings.initialBlockPlugin;
/** Hide plus buttons */
editor.toolbar.hidePlusButton();
if (!inputIsEmpty) {
/** Mark current block */
editor.content.markBlock();
}
if ( isInitialType && inputIsEmpty ) {
/** Show plus button */
editor.toolbar.showPlusButton();
}
};
/**
* This method allows to define, is caret in contenteditable element or not.
*
* @private
*
* @description Otherwise, if we get TEXT node from range container, that will means we have input index.
* In this case we use default browsers behaviour (if plugin allows that) or overwritten action.
* Therefore, to be sure that we've clicked first-level block area, we should have currentNode, which always
* specifies to the first-level block. Other cases we just ignore.
*/
var detectWhenClickedOnFirstLevelBlockArea_ = function () {
var selection = window.getSelection(),
anchorNode = selection.anchorNode,
flag = false;
if (selection.rangeCount === 0) {
editor.content.editorAreaHightlighted = true;
} else {
if (!editor.core.isDomNode(anchorNode)) {
anchorNode = anchorNode.parentNode;
}
/** Already founded, without loop */
if (anchorNode.contentEditable == 'true') {
flag = true;
}
while (anchorNode.contentEditable != 'true') {
anchorNode = anchorNode.parentNode;
if (anchorNode.contentEditable == 'true') {
flag = true;
}
if (anchorNode == document.body) {
break;
}
}
/** If editable element founded, flag is "TRUE", Therefore we return "FALSE" */
editor.content.editorAreaHightlighted = !flag;
}
};
/**
* Toolbar button click handler
*
* @param {Object} event - cursor to the button
* @protected
*
* @description gets current tool and calls render method
*/
callbacks.toolbarButtonClicked = function (event) {
var button = this;
editor.toolbar.current = button.dataset.type;
editor.toolbar.toolbox.toolClicked(event);
editor.toolbar.close();
};
/**
* Show or Hide toolbox when plus button is clicked
*/
callbacks.plusButtonClicked = function () {
if (!editor.nodes.toolbox.classList.contains('opened')) {
editor.toolbar.toolbox.open();
} else {
editor.toolbar.toolbox.close();
}
};
/**
* Block handlers for KeyDown events
*
* @protected
* @param {Object} event
*
* Handles keydowns on block
* @see blockRightOrDownArrowPressed_
* @see backspacePressed_
* @see blockLeftOrUpArrowPressed_
*/
callbacks.blockKeydown = function (event) {
let block = event.target; // event.target is input
switch (event.keyCode) {
case editor.core.keys.DOWN:
case editor.core.keys.RIGHT:
blockRightOrDownArrowPressed_(event);
break;
case editor.core.keys.BACKSPACE:
backspacePressed_(block, event);
break;
case editor.core.keys.UP:
case editor.core.keys.LEFT:
blockLeftOrUpArrowPressed_(event);
break;
}
};
/**
* RIGHT or DOWN keydowns on block
*
* @param {Object} event
* @private
*
* @description watches the selection and gets closest editable element.
* Uses method getDeepestTextNodeFromPosition to get the last node of next block
* Sets caret if it is contenteditable
*/
var blockRightOrDownArrowPressed_ = function (event) {
var selection = window.getSelection(),
inputs = editor.state.inputs,
focusedNode = selection.anchorNode,
focusedNodeHolder;
/** Check for caret existance */
if (!focusedNode) {
return false;
}
/** Looking for closest (parent) contentEditable element of focused node */
while (focusedNode.contentEditable != 'true') {
focusedNodeHolder = focusedNode.parentNode;
focusedNode = focusedNodeHolder;
}
/** Input index in DOM level */
var editableElementIndex = 0;
while (focusedNode != inputs[editableElementIndex]) {
editableElementIndex ++;
}
/**
* Founded contentEditable element doesn't have childs
* Or maybe New created block
*/
if (!focusedNode.textContent) {
editor.caret.setToNextBlock(editableElementIndex);
return;
}
/**
* Do nothing when caret doesn not reaches the end of last child
*/
var caretInLastChild = false,
caretAtTheEndOfText = false;
var lastChild,
deepestTextnode;
lastChild = focusedNode.childNodes[focusedNode.childNodes.length - 1 ];
if (editor.core.isDomNode(lastChild)) {
deepestTextnode = editor.content.getDeepestTextNodeFromPosition(lastChild, lastChild.childNodes.length);
} else {
deepestTextnode = lastChild;
}
caretInLastChild = selection.anchorNode == deepestTextnode;
caretAtTheEndOfText = deepestTextnode.length == selection.anchorOffset;
if ( !caretInLastChild || !caretAtTheEndOfText ) {
editor.core.log('arrow [down|right] : caret does not reached the end');
return false;
}
editor.caret.setToNextBlock(editableElementIndex);
};
/**
* LEFT or UP keydowns on block
*
* @param {Object} event
* @private
*
* watches the selection and gets closest editable element.
* Uses method getDeepestTextNodeFromPosition to get the last node of previous block
* Sets caret if it is contenteditable
*
*/
var blockLeftOrUpArrowPressed_ = function (event) {
var selection = window.getSelection(),
inputs = editor.state.inputs,
focusedNode = selection.anchorNode,
focusedNodeHolder;
/** Check for caret existance */
if (!focusedNode) {
return false;
}
/**
* LEFT or UP not at the beginning
*/
if ( selection.anchorOffset !== 0) {
return false;
}
/** Looking for parent contentEditable block */
while (focusedNode.contentEditable != 'true') {
focusedNodeHolder = focusedNode.parentNode;
focusedNode = focusedNodeHolder;
}
/** Input index in DOM level */
var editableElementIndex = 0;
while (focusedNode != inputs[editableElementIndex]) {
editableElementIndex ++;
}
/**
* Do nothing if caret is not at the beginning of first child
*/
var caretInFirstChild = false,
caretAtTheBeginning = false;
var firstChild,
deepestTextnode;
/**
* Founded contentEditable element doesn't have childs
* Or maybe New created block
*/
if (!focusedNode.textContent) {
editor.caret.setToPreviousBlock(editableElementIndex);
return;
}
firstChild = focusedNode.childNodes[0];
if (editor.core.isDomNode(firstChild)) {
deepestTextnode = editor.content.getDeepestTextNodeFromPosition(firstChild, 0);
} else {
deepestTextnode = firstChild;
}
caretInFirstChild = selection.anchorNode == deepestTextnode;
caretAtTheBeginning = selection.anchorOffset === 0;
if ( caretInFirstChild && caretAtTheBeginning ) {
editor.caret.setToPreviousBlock(editableElementIndex);
}
};
/**
* Handles backspace keydown
*
* @param {Element} block
* @param {Object} event
* @private
*
* @description if block is empty, delete the block and set caret to the previous block
* If block is not empty, try to merge two blocks - current and previous
* But it we try'n to remove first block, then we should set caret to the next block, not previous.
* If we removed the last block, create new one
*/
var backspacePressed_ = function (block, event) {
var currentInputIndex = editor.caret.getCurrentInputIndex(),
range,
selectionLength,
firstLevelBlocksCount;
if (editor.core.isNativeInput(event.target)) {
/** If input value is empty - remove block */
if (event.target.value.trim() == '') {
block.remove();
} else {
return;
}
}
if (block.textContent.trim()) {
range = editor.content.getRange();
selectionLength = range.endOffset - range.startOffset;
if (editor.caret.position.atStart() && !selectionLength && editor.state.inputs[currentInputIndex - 1]) {
editor.content.mergeBlocks(currentInputIndex);
} else {
return;
}
}
if (!selectionLength) {
block.remove();
}
firstLevelBlocksCount = editor.nodes.redactor.childNodes.length;
/**
* If all blocks are removed
*/
if (firstLevelBlocksCount === 0) {
/** update currentNode variable */
editor.content.currentNode = null;
/** Inserting new empty initial block */
editor.ui.addInitialBlock();
/** Updating inputs state after deleting last block */
editor.ui.saveInputs();
/** Set to current appended block */
window.setTimeout(function () {
editor.caret.setToPreviousBlock(1);
}, 10);
} else {
if (editor.caret.inputIndex !== 0) {
/** Target block is not first */
editor.caret.setToPreviousBlock(editor.caret.inputIndex);
} else {
/** If we try to delete first block */
editor.caret.setToNextBlock(editor.caret.inputIndex);
}
}
editor.toolbar.move();
if (!editor.toolbar.opened) {
editor.toolbar.open();
}
/** Updating inputs state */
editor.ui.saveInputs();
/** Prevent default browser behaviour */
event.preventDefault();
};
/**
* used by UI module
* Clicks on block settings button
*
* @param {Object} event
* @protected
* @description Opens toolbar settings
*/
callbacks.showSettingsButtonClicked = function (event) {
/**
* Get type of current block
* It uses to append settings from tool.settings property.
* ...
* Type is stored in data-type attribute on block
*/
var currentToolType = editor.content.currentNode.dataset.tool;
editor.toolbar.settings.toggle(currentToolType);
/** Close toolbox when settings button is active */
editor.toolbar.toolbox.close();
editor.toolbar.settings.hideRemoveActions();
};
return callbacks;
})({});

View file

@ -1,305 +0,0 @@
/**
* Codex Editor Caret Module
*
* @author Codex Team
* @version 1.0
*/
module.exports = (function (caret) {
let editor = codex.editor;
/**
* @var {int} InputIndex - editable element in DOM
*/
caret.inputIndex = null;
/**
* @var {int} offset - caret position in a text node.
*/
caret.offset = null;
/**
* @var {int} focusedNodeIndex - we get index of child node from first-level block
*/
caret.focusedNodeIndex = null;
/**
* Creates Document Range and sets caret to the element.
* @protected
* @uses caret.save if you need to save caret position
* @param {Element} el - Changed Node.
*/
caret.set = function ( el, index, offset) {
offset = offset || caret.offset || 0;
index = index || caret.focusedNodeIndex || 0;
var childs = el.childNodes,
nodeToSet;
if ( childs.length === 0 ) {
nodeToSet = el;
} else {
nodeToSet = childs[index];
}
/** If Element is INPUT */
if (el.contentEditable != 'true') {
el.focus();
return;
}
if (editor.core.isDomNode(nodeToSet)) {
nodeToSet = editor.content.getDeepestTextNodeFromPosition(nodeToSet, nodeToSet.childNodes.length);
}
var range = document.createRange(),
selection = window.getSelection();
window.setTimeout(function () {
range.setStart(nodeToSet, offset);
range.setEnd(nodeToSet, offset);
selection.removeAllRanges();
selection.addRange(range);
editor.caret.saveCurrentInputIndex();
}, 20);
};
/**
* @protected
* Updates index of input and saves it in caret object
*/
caret.saveCurrentInputIndex = function () {
/** Index of Input that we paste sanitized content */
var selection = window.getSelection(),
inputs = editor.state.inputs,
focusedNode = selection.anchorNode,
focusedNodeHolder;
if (!focusedNode) {
return;
}
/** Looking for parent contentEditable block */
while (focusedNode.contentEditable != 'true') {
focusedNodeHolder = focusedNode.parentNode;
focusedNode = focusedNodeHolder;
}
/** Input index in DOM level */
var editableElementIndex = 0;
while (focusedNode != inputs[editableElementIndex]) {
editableElementIndex ++;
}
caret.inputIndex = editableElementIndex;
};
/**
* Returns current input index (caret object)
*/
caret.getCurrentInputIndex = function () {
return caret.inputIndex;
};
/**
* @param {int} index - index of first-level block after that we set caret into next input
*/
caret.setToNextBlock = function (index) {
var inputs = editor.state.inputs,
nextInput = inputs[index + 1];
if (!nextInput) {
editor.core.log('We are reached the end');
return;
}
/**
* When new Block created or deleted content of input
* We should add some text node to set caret
*/
if (!nextInput.childNodes.length) {
var emptyTextElement = document.createTextNode('');
nextInput.appendChild(emptyTextElement);
}
editor.caret.inputIndex = index + 1;
editor.caret.set(nextInput, 0, 0);
editor.content.workingNodeChanged(nextInput);
};
/**
* @param {int} index - index of target input.
* Sets caret to input with this index
*/
caret.setToBlock = function (index) {
var inputs = editor.state.inputs,
targetInput = inputs[index];
if ( !targetInput ) {
return;
}
/**
* When new Block created or deleted content of input
* We should add some text node to set caret
*/
if (!targetInput.childNodes.length) {
var emptyTextElement = document.createTextNode('');
targetInput.appendChild(emptyTextElement);
}
editor.caret.inputIndex = index;
editor.caret.set(targetInput, 0, 0);
editor.content.workingNodeChanged(targetInput);
};
/**
* @param {int} index - index of input
*/
caret.setToPreviousBlock = function (index) {
index = index || 0;
var inputs = editor.state.inputs,
previousInput = inputs[index - 1],
lastChildNode,
lengthOfLastChildNode,
emptyTextElement;
if (!previousInput) {
editor.core.log('We are reached first node');
return;
}
lastChildNode = editor.content.getDeepestTextNodeFromPosition(previousInput, previousInput.childNodes.length);
lengthOfLastChildNode = lastChildNode.length;
/**
* When new Block created or deleted content of input
* We should add some text node to set caret
*/
if (!previousInput.childNodes.length) {
emptyTextElement = document.createTextNode('');
previousInput.appendChild(emptyTextElement);
}
editor.caret.inputIndex = index - 1;
editor.caret.set(previousInput, previousInput.childNodes.length - 1, lengthOfLastChildNode);
editor.content.workingNodeChanged(inputs[index - 1]);
};
caret.position = {
atStart : function () {
var selection = window.getSelection(),
anchorOffset = selection.anchorOffset,
anchorNode = selection.anchorNode,
firstLevelBlock = editor.content.getFirstLevelBlock(anchorNode),
pluginsRender = firstLevelBlock.childNodes[0];
if (!editor.core.isDomNode(anchorNode)) {
anchorNode = anchorNode.parentNode;
}
var isFirstNode = anchorNode === pluginsRender.childNodes[0],
isOffsetZero = anchorOffset === 0;
return isFirstNode && isOffsetZero;
},
atTheEnd : function () {
var selection = window.getSelection(),
anchorOffset = selection.anchorOffset,
anchorNode = selection.anchorNode;
/** Caret is at the end of input */
return !anchorNode || !anchorNode.length || anchorOffset === anchorNode.length;
}
};
/**
* Inserts node at the caret location
* @param {HTMLElement|DocumentFragment} node
*/
caret.insertNode = function (node) {
var selection, range,
lastNode = node;
if (node.nodeType == editor.core.nodeTypes.DOCUMENT_FRAGMENT) {
lastNode = node.lastChild;
}
selection = window.getSelection();
range = selection.getRangeAt(0);
range.deleteContents();
range.insertNode(node);
range.setStartAfter(lastNode);
range.collapse(true);
selection.removeAllRanges();
selection.addRange(range);
};
return caret;
})({});

View file

@ -1,805 +0,0 @@
/**
* Codex Editor Content Module
* Works with DOM
*
* @module Codex Editor content module
*
* @author Codex Team
* @version 1.3.13
*
* @description Module works with Elements that have been appended to the main DOM
*/
module.exports = (function (content) {
let editor = codex.editor;
/**
* Links to current active block
* @type {null | Element}
*/
content.currentNode = null;
/**
* clicked in redactor area
* @type {null | Boolean}
*/
content.editorAreaHightlighted = null;
/**
* @deprecated
* Synchronizes redactor with original textarea
*/
content.sync = function () {
editor.core.log('syncing...');
/**
* Save redactor content to editor.state
*/
editor.state.html = editor.nodes.redactor.innerHTML;
};
/**
* Appends background to the block
*
* @description add CSS class to highlight visually first-level block area
*/
content.markBlock = function () {
editor.content.currentNode.classList.add(editor.ui.className.BLOCK_HIGHLIGHTED);
};
/**
* Clear background
*
* @description clears styles that highlights block
*/
content.clearMark = function () {
if (editor.content.currentNode) {
editor.content.currentNode.classList.remove(editor.ui.className.BLOCK_HIGHLIGHTED);
}
};
/**
* Finds first-level block
*
* @param {Element} node - selected or clicked in redactors area node
* @protected
*
* @description looks for first-level block.
* gets parent while node is not first-level
*/
content.getFirstLevelBlock = function (node) {
if (!editor.core.isDomNode(node)) {
node = node.parentNode;
}
if (node === editor.nodes.redactor || node === document.body) {
return null;
} else {
while(!node.classList.contains(editor.ui.className.BLOCK_CLASSNAME)) {
node = node.parentNode;
}
return node;
}
};
/**
* Trigger this event when working node changed
* @param {Element} targetNode - first-level of this node will be current
* @protected
*
* @description If targetNode is first-level then we set it as current else we look for parents to find first-level
*/
content.workingNodeChanged = function (targetNode) {
/** Clear background from previous marked block before we change */
editor.content.clearMark();
if (!targetNode) {
return;
}
content.currentNode = content.getFirstLevelBlock(targetNode);
};
/**
* Replaces one redactor block with another
* @protected
* @param {Element} targetBlock - block to replace. Mostly currentNode.
* @param {Element} newBlock
* @param {string} newBlockType - type of new block; we need to store it to data-attribute
*
* [!] Function does not saves old block content.
* You can get it manually and pass with newBlock.innerHTML
*/
content.replaceBlock = function (targetBlock, newBlock) {
if (!targetBlock || !newBlock) {
editor.core.log('replaceBlock: missed params');
return;
}
/** If target-block is not a frist-level block, then we iterate parents to find it */
while(!targetBlock.classList.contains(editor.ui.className.BLOCK_CLASSNAME)) {
targetBlock = targetBlock.parentNode;
}
/** Replacing */
editor.nodes.redactor.replaceChild(newBlock, targetBlock);
/**
* Set new node as current
*/
editor.content.workingNodeChanged(newBlock);
/**
* Add block handlers
*/
editor.ui.addBlockHandlers(newBlock);
/**
* Save changes
*/
editor.ui.saveInputs();
};
/**
* @protected
*
* Inserts new block to redactor
* Wrapps block into a DIV with BLOCK_CLASSNAME class
*
* @param blockData {object}
* @param blockData.block {Element} element with block content
* @param blockData.type {string} block plugin
* @param needPlaceCaret {bool} pass true to set caret in new block
*
*/
content.insertBlock = function ( blockData, needPlaceCaret ) {
var workingBlock = editor.content.currentNode,
newBlockContent = blockData.block,
blockType = blockData.type,
isStretched = blockData.stretched;
var newBlock = composeNewBlock_(newBlockContent, blockType, isStretched);
if (workingBlock) {
editor.core.insertAfter(workingBlock, newBlock);
} else {
/**
* If redactor is empty, append as first child
*/
editor.nodes.redactor.appendChild(newBlock);
}
/**
* Block handler
*/
editor.ui.addBlockHandlers(newBlock);
/**
* Set new node as current
*/
editor.content.workingNodeChanged(newBlock);
/**
* Save changes
*/
editor.ui.saveInputs();
if ( needPlaceCaret ) {
/**
* If we don't know input index then we set default value -1
*/
var currentInputIndex = editor.caret.getCurrentInputIndex() || -1;
if (currentInputIndex == -1) {
var editableElement = newBlock.querySelector('[contenteditable]'),
emptyText = document.createTextNode('');
editableElement.appendChild(emptyText);
editor.caret.set(editableElement, 0, 0);
editor.toolbar.move();
editor.toolbar.showPlusButton();
} else {
if (currentInputIndex === editor.state.inputs.length - 1)
return;
/** Timeout for browsers execution */
window.setTimeout(function () {
/** Setting to the new input */
editor.caret.setToNextBlock(currentInputIndex);
editor.toolbar.move();
editor.toolbar.open();
}, 10);
}
}
/**
* Block is inserted, wait for new click that defined focusing on editors area
* @type {boolean}
*/
content.editorAreaHightlighted = false;
};
/**
* Replaces blocks with saving content
* @protected
* @param {Element} noteToReplace
* @param {Element} newNode
* @param {Element} blockType
*/
content.switchBlock = function (blockToReplace, newBlock, tool) {
tool = tool || editor.content.currentNode.dataset.tool;
var newBlockComposed = composeNewBlock_(newBlock, tool);
/** Replacing */
editor.content.replaceBlock(blockToReplace, newBlockComposed);
/** Save new Inputs when block is changed */
editor.ui.saveInputs();
};
/**
* Iterates between child noted and looking for #text node on deepest level
* @protected
*
* @param {Element} block - node where find
* @param {int} postiton - starting postion
* Example: childNodex.length to find from the end
* or 0 to find from the start
* @return {Text} block
* @uses DFS
*/
content.getDeepestTextNodeFromPosition = function (block, position) {
/**
* Clear Block from empty and useless spaces with trim.
* Such nodes we should remove
*/
var blockChilds = block.childNodes,
index,
node,
text;
for(index = 0; index < blockChilds.length; index++) {
node = blockChilds[index];
if (node.nodeType == editor.core.nodeTypes.TEXT) {
text = node.textContent.trim();
/** Text is empty. We should remove this child from node before we start DFS
* decrease the quantity of childs.
*/
if (text === '') {
block.removeChild(node);
position--;
}
}
}
if (block.childNodes.length === 0) {
return document.createTextNode('');
}
/** Setting default position when we deleted all empty nodes */
if ( position < 0 )
position = 1;
var lookingFromStart = false;
/** For looking from START */
if (position === 0) {
lookingFromStart = true;
position = 1;
}
while ( position ) {
/** initial verticle of node. */
if ( lookingFromStart ) {
block = block.childNodes[0];
} else {
block = block.childNodes[position - 1];
}
if ( block.nodeType == editor.core.nodeTypes.TAG ) {
position = block.childNodes.length;
} else if (block.nodeType == editor.core.nodeTypes.TEXT ) {
position = 0;
}
}
return block;
};
/**
* @private
* @param {Element} block - current plugins render
* @param {String} tool - plugins name
* @param {Boolean} isStretched - make stretched block or not
*
* @description adds necessary information to wrap new created block by first-level holder
*/
var composeNewBlock_ = function (block, tool, isStretched) {
var newBlock = editor.draw.node('DIV', editor.ui.className.BLOCK_CLASSNAME, {}),
blockContent = editor.draw.node('DIV', editor.ui.className.BLOCK_CONTENT, {});
blockContent.appendChild(block);
newBlock.appendChild(blockContent);
if (isStretched) {
blockContent.classList.add(editor.ui.className.BLOCK_STRETCHED);
}
newBlock.dataset.tool = tool;
return newBlock;
};
/**
* Returns Range object of current selection
* @protected
*/
content.getRange = function () {
var selection = window.getSelection().getRangeAt(0);
return selection;
};
/**
* Divides block in two blocks (after and before caret)
*
* @protected
* @param {int} inputIndex - target input index
*
* @description splits current input content to the separate blocks
* When enter is pressed among the words, that text will be splited.
*/
content.splitBlock = function (inputIndex) {
var selection = window.getSelection(),
anchorNode = selection.anchorNode,
anchorNodeText = anchorNode.textContent,
caretOffset = selection.anchorOffset,
textBeforeCaret,
textNodeBeforeCaret,
textAfterCaret,
textNodeAfterCaret;
var currentBlock = editor.content.currentNode.querySelector('[contentEditable]');
textBeforeCaret = anchorNodeText.substring(0, caretOffset);
textAfterCaret = anchorNodeText.substring(caretOffset);
textNodeBeforeCaret = document.createTextNode(textBeforeCaret);
if (textAfterCaret) {
textNodeAfterCaret = document.createTextNode(textAfterCaret);
}
var previousChilds = [],
nextChilds = [],
reachedCurrent = false;
if (textNodeAfterCaret) {
nextChilds.push(textNodeAfterCaret);
}
for ( var i = 0, child; !!(child = currentBlock.childNodes[i]); i++) {
if ( child != anchorNode ) {
if ( !reachedCurrent ) {
previousChilds.push(child);
} else {
nextChilds.push(child);
}
} else {
reachedCurrent = true;
}
}
/** Clear current input */
editor.state.inputs[inputIndex].innerHTML = '';
/**
* Append all childs founded before anchorNode
*/
var previousChildsLength = previousChilds.length;
for(i = 0; i < previousChildsLength; i++) {
editor.state.inputs[inputIndex].appendChild(previousChilds[i]);
}
editor.state.inputs[inputIndex].appendChild(textNodeBeforeCaret);
/**
* Append text node which is after caret
*/
var nextChildsLength = nextChilds.length,
newNode = document.createElement('div');
for(i = 0; i < nextChildsLength; i++) {
newNode.appendChild(nextChilds[i]);
}
newNode = newNode.innerHTML;
/** This type of block creates when enter is pressed */
var NEW_BLOCK_TYPE = editor.settings.initialBlockPlugin;
/**
* Make new paragraph with text after caret
*/
editor.content.insertBlock({
type : NEW_BLOCK_TYPE,
block : editor.tools[NEW_BLOCK_TYPE].render({
text : newNode
})
}, true );
};
/**
* Merges two blocks current and target
* If target index is not exist, then previous will be as target
*
* @protected
* @param {int} currentInputIndex
* @param {int} targetInputIndex
*
* @description gets two inputs indexes and merges into one
*/
content.mergeBlocks = function (currentInputIndex, targetInputIndex) {
/** If current input index is zero, then prevent method execution */
if (currentInputIndex === 0) {
return;
}
var targetInput,
currentInputContent = editor.state.inputs[currentInputIndex].innerHTML;
if (!targetInputIndex) {
targetInput = editor.state.inputs[currentInputIndex - 1];
} else {
targetInput = editor.state.inputs[targetInputIndex];
}
targetInput.innerHTML += currentInputContent;
};
/**
* Iterates all right siblings and parents, which has right siblings
* while it does not reached the first-level block
*
* @param {Element} node
* @return {boolean}
*/
content.isLastNode = function (node) {
// console.log('погнали перебор родителей');
var allChecked = false;
while ( !allChecked ) {
// console.log('Смотрим на %o', node);
// console.log('Проверим, пустые ли соседи справа');
if ( !allSiblingsEmpty_(node) ) {
// console.log('Есть непустые соседи. Узел не последний. Выходим.');
return false;
}
node = node.parentNode;
/**
* Проверяем родителей до тех пор, пока не найдем блок первого уровня
*/
if ( node.classList.contains(editor.ui.className.BLOCK_CONTENT) ) {
allChecked = true;
}
}
return true;
};
/**
* Checks if all element right siblings is empty
* @param node
*/
var allSiblingsEmpty_ = function (node) {
/**
* Нужно убедиться, что после пустого соседа ничего нет
*/
var sibling = node.nextSibling;
while ( sibling ) {
if (sibling.textContent.length) {
return false;
}
sibling = sibling.nextSibling;
}
return true;
};
/**
* @public
*
* @param {string} htmlData - html content as string
* @param {string} plainData - plain text
* @return {string} - html content as string
*/
content.wrapTextWithParagraphs = function (htmlData, plainData) {
if (!htmlData.trim()) {
return wrapPlainTextWithParagraphs(plainData);
}
var wrapper = document.createElement('DIV'),
newWrapper = document.createElement('DIV'),
i,
paragraph,
firstLevelBlocks = ['DIV', 'P'],
blockTyped,
node;
/**
* Make HTML Element to Wrap Text
* It allows us to work with input data as HTML content
*/
wrapper.innerHTML = htmlData;
paragraph = document.createElement('P');
for (i = 0; i < wrapper.childNodes.length; i++) {
node = wrapper.childNodes[i];
blockTyped = firstLevelBlocks.indexOf(node.tagName) != -1;
/**
* If node is first-levet
* we add this node to our new wrapper
*/
if ( blockTyped ) {
/**
* If we had splitted inline nodes to paragraph before
*/
if ( paragraph.childNodes.length ) {
newWrapper.appendChild(paragraph.cloneNode(true));
/** empty paragraph */
paragraph = null;
paragraph = document.createElement('P');
}
newWrapper.appendChild(node.cloneNode(true));
} else {
/** Collect all inline nodes to one as paragraph */
paragraph.appendChild(node.cloneNode(true));
/** if node is last we should append this node to paragraph and paragraph to new wrapper */
if ( i == wrapper.childNodes.length - 1 ) {
newWrapper.appendChild(paragraph.cloneNode(true));
}
}
}
return newWrapper.innerHTML;
};
/**
* Splits strings on new line and wraps paragraphs with <p> tag
* @param plainText
* @returns {string}
*/
var wrapPlainTextWithParagraphs = function (plainText) {
if (!plainText) return '';
return '<p>' + plainText.split('\n\n').join('</p><p>') + '</p>';
};
/**
* Finds closest Contenteditable parent from Element
* @param {Element} node element looking from
* @return {Element} node contenteditable
*/
content.getEditableParent = function (node) {
while (node && node.contentEditable != 'true') {
node = node.parentNode;
}
return node;
};
/**
* Clear editors content
*
* @param {Boolean} all if true, delete all article data (content, id, etc.)
*/
content.clear = function (all) {
editor.nodes.redactor.innerHTML = '';
editor.content.sync();
editor.ui.saveInputs();
if (all) {
editor.state.blocks = {};
} else if (editor.state.blocks) {
editor.state.blocks.items = [];
}
editor.content.currentNode = null;
};
/**
*
* Load new data to editor
* If editor is not empty, just append articleData.items
*
* @param articleData.items
*/
content.load = function (articleData) {
var currentContent = Object.assign({}, editor.state.blocks);
editor.content.clear();
if (!Object.keys(currentContent).length) {
editor.state.blocks = articleData;
} else if (!currentContent.items) {
currentContent.items = articleData.items;
editor.state.blocks = currentContent;
} else {
currentContent.items = currentContent.items.concat(articleData.items);
editor.state.blocks = currentContent;
}
editor.renderer.makeBlocksFromData();
};
return content;
})({});

View file

@ -1,389 +0,0 @@
/**
* Codex Editor Core
*
* @author Codex Team
* @version 1.1.3
*/
module.exports = (function (core) {
let editor = codex.editor;
/**
* @public
*
* Editor preparing method
* @return Promise
*/
core.prepare = function (userSettings) {
return new Promise(function (resolve, reject) {
if ( userSettings ) {
editor.settings.tools = userSettings.tools || editor.settings.tools;
}
if (userSettings.data) {
editor.state.blocks = userSettings.data;
}
if (userSettings.initialBlockPlugin) {
editor.settings.initialBlockPlugin = userSettings.initialBlockPlugin;
}
if (userSettings.sanitizer) {
editor.settings.sanitizer = userSettings.sanitizer;
}
editor.hideToolbar = userSettings.hideToolbar;
editor.settings.placeholder = userSettings.placeholder || '';
editor.nodes.holder = document.getElementById(userSettings.holderId || editor.settings.holderId);
if (typeof editor.nodes.holder === undefined || editor.nodes.holder === null) {
reject(Error("Holder wasn't found by ID: #" + userSettings.holderId));
} else {
resolve();
}
});
};
/**
* Logging method
* @param type = ['log', 'info', 'warn']
*/
core.log = function (msg, type, arg) {
type = type || 'log';
if (!arg) {
arg = msg || 'undefined';
msg = '[codex-editor]: %o';
} else {
msg = '[codex-editor]: ' + msg;
}
try{
if ( 'console' in window && window.console[ type ] ) {
if ( arg ) window.console[ type ]( msg, arg );
else window.console[ type ]( msg );
}
}catch(e) {}
};
/**
* @protected
*
* Helper for insert one element after another
*/
core.insertAfter = function (target, element) {
target.parentNode.insertBefore(element, target.nextSibling);
};
/**
* @const
*
* Readable DOM-node types map
*/
core.nodeTypes = {
TAG : 1,
TEXT : 3,
COMMENT : 8,
DOCUMENT_FRAGMENT: 11
};
/**
* @const
* Readable keys map
*/
core.keys = { BACKSPACE: 8, TAB: 9, ENTER: 13, SHIFT: 16, CTRL: 17, ALT: 18, ESC: 27, SPACE: 32, LEFT: 37, UP: 38, DOWN: 40, RIGHT: 39, DELETE: 46, META: 91 };
/**
* @protected
*
* Check object for DOM node
*/
core.isDomNode = function (el) {
return el && typeof el === 'object' && el.nodeType && el.nodeType == this.nodeTypes.TAG;
};
/**
* Checks passed object for emptiness
* @require ES5 - Object.keys
* @param {object}
*/
core.isEmpty = function ( obj ) {
return Object.keys(obj).length === 0;
};
/**
* Native Ajax
* @param {String} settings.url - request URL
* @param {function} settings.beforeSend - returned value will be passed as context to the Success, Error and Progress callbacks
* @param {function} settings.success
* @param {function} settings.progress
*/
core.ajax = function (settings) {
if (!settings || !settings.url) {
return;
}
var XMLHTTP = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP'),
encodedString,
isFormData,
prop;
settings.async = true;
settings.type = settings.type || 'GET';
settings.data = settings.data || '';
settings['content-type'] = settings['content-type'] || 'application/json; charset=utf-8';
if (settings.type == 'GET' && settings.data) {
settings.url = /\?/.test(settings.url) ? settings.url + '&' + settings.data : settings.url + '?' + settings.data;
} else {
encodedString = '';
for(prop in settings.data) {
encodedString += (prop + '=' + encodeURIComponent(settings.data[prop]) + '&');
}
}
if (settings.withCredentials) {
XMLHTTP.withCredentials = true;
}
/**
* Value returned in beforeSend funtion will be passed as context to the other response callbacks
* If beforeSend returns false, AJAX will be blocked
*/
let responseContext,
beforeSendResult;
if (typeof settings.beforeSend === 'function') {
beforeSendResult = settings.beforeSend.call();
if (beforeSendResult === false) {
return;
}
}
XMLHTTP.open( settings.type, settings.url, settings.async );
/**
* If we send FormData, we need no content-type header
*/
isFormData = isFormData_(settings.data);
if (!isFormData) {
if (settings.type !== 'POST') {
XMLHTTP.setRequestHeader('Content-type', settings['content-type']);
} else {
XMLHTTP.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
}
}
XMLHTTP.setRequestHeader('X-Requested-With', 'XMLHttpRequest');
responseContext = beforeSendResult || XMLHTTP;
if (typeof settings.progress === 'function') {
XMLHTTP.upload.onprogress = settings.progress.bind(responseContext);
}
XMLHTTP.onreadystatechange = function () {
if (XMLHTTP.readyState === 4) {
if (XMLHTTP.status === 200) {
if (typeof settings.success === 'function') {
settings.success.call(responseContext, XMLHTTP.responseText);
}
} else {
if (typeof settings.error === 'function') {
settings.error.call(responseContext, XMLHTTP.responseText, XMLHTTP.status);
}
}
}
};
if (isFormData) {
// Sending FormData
XMLHTTP.send(settings.data);
} else {
// POST requests
XMLHTTP.send(encodedString);
}
return XMLHTTP;
};
/**
* Appends script to head of document
* @return Promise
*/
core.importScript = function (scriptPath, instanceName) {
return new Promise(function (resolve, reject) {
let script;
/** Script is already loaded */
if ( !instanceName ) {
reject('Instance name is missed');
} else if ( document.getElementById(editor.scriptPrefix + instanceName) ) {
resolve(scriptPath);
}
script = document.createElement('SCRIPT');
script.async = true;
script.defer = true;
script.id = editor.scriptPrefix + instanceName;
script.onload = function () {
resolve(scriptPath);
};
script.onerror = function () {
reject(scriptPath);
};
script.src = scriptPath;
document.head.appendChild(script);
});
};
/**
* Function for checking is it FormData object to send.
* @param {Object} object to check
* @return boolean
*/
var isFormData_ = function (object) {
return object instanceof FormData;
};
/**
* Check block
* @param target
* @description Checks target is it native input
*/
core.isNativeInput = function (target) {
var nativeInputAreas = ['INPUT', 'TEXTAREA'];
return nativeInputAreas.indexOf(target.tagName) != -1;
};
/**
* Check if block is empty
* We should check block textContent, child native inputs and some exceptions like IMG and IFRAME
*
* @param block
* @returns {boolean}
*/
core.isBlockEmpty = function (block) {
const EXCEPTION_TAGS = ['IMG', 'IFRAME'];
var nativeInputs = block.querySelectorAll('textarea, input'),
nativeInputsAreEmpty = true,
textContentIsEmpty = !block.textContent.trim();
Array.prototype.forEach.call(nativeInputs, function (input) {
if (input.type == 'textarea' || input.type == 'text') {
nativeInputsAreEmpty = nativeInputsAreEmpty && !input.value.trim();
}
});
return textContentIsEmpty && nativeInputsAreEmpty && !EXCEPTION_TAGS.includes(block.tagName);
};
return core;
})({});

View file

@ -1,98 +0,0 @@
/**
* Codex Editor Destroyer module
*
* @auhor Codex Team
* @version 1.0
*/
module.exports = function (destroyer) {
let editor = codex.editor;
destroyer.removeNodes = function () {
editor.nodes.wrapper.remove();
editor.nodes.notifications.remove();
};
destroyer.destroyPlugins = function () {
for (var tool in editor.tools) {
if (typeof editor.tools[tool].destroy === 'function') {
editor.tools[tool].destroy();
}
}
};
destroyer.destroyScripts = function () {
var scripts = document.getElementsByTagName('SCRIPT');
for (var i = 0; i < scripts.length; i++) {
if (scripts[i].id.indexOf(editor.scriptPrefix) + 1) {
scripts[i].remove();
i--;
}
}
};
/**
* Delete editor data from webpage.
* You should send settings argument with boolean flags:
* @param settings.ui- remove redactor event listeners and DOM nodes
* @param settings.scripts - remove redactor scripts from DOM
* @param settings.plugins - remove plugin's objects
* @param settings.core - remove editor core. You can remove core only if UI and scripts flags is true
* }
*
*/
destroyer.destroy = function (settings) {
if (!settings || typeof settings !== 'object') {
return;
}
if (settings.ui) {
destroyer.removeNodes();
editor.listeners.removeAll();
}
if (settings.scripts) {
destroyer.destroyScripts();
}
if (settings.plugins) {
destroyer.destroyPlugins();
}
if (settings.ui && settings.scripts && settings.core) {
delete codex.editor;
}
};
return destroyer;
}({});

View file

@ -1,316 +0,0 @@
/**
* Codex Editor Draw module
*
* @author Codex Team
* @version 1.0.
*/
module.exports = (function (draw) {
/**
* Base editor wrapper
*/
draw.wrapper = function () {
var wrapper = document.createElement('div');
wrapper.className += 'codex-editor';
return wrapper;
};
/**
* Content-editable holder
*/
draw.redactor = function () {
var redactor = document.createElement('div');
redactor.className += 'ce-redactor';
return redactor;
};
draw.ceBlock = function () {
var block = document.createElement('DIV');
block.className += 'ce_block';
return block;
};
/**
* Empty toolbar with toggler
*/
draw.toolbar = function () {
var bar = document.createElement('div');
bar.className += 'ce-toolbar';
return bar;
};
draw.toolbarContent = function () {
var wrapper = document.createElement('DIV');
wrapper.classList.add('ce-toolbar__content');
return wrapper;
};
/**
* Inline toolbar
*/
draw.inlineToolbar = function () {
var bar = document.createElement('DIV');
bar.className += 'ce-toolbar-inline';
return bar;
};
/**
* Wrapper for inline toobar buttons
*/
draw.inlineToolbarButtons = function () {
var wrapper = document.createElement('DIV');
wrapper.className += 'ce-toolbar-inline__buttons';
return wrapper;
};
/**
* For some actions
*/
draw.inlineToolbarActions = function () {
var wrapper = document.createElement('DIV');
wrapper.className += 'ce-toolbar-inline__actions';
return wrapper;
};
draw.inputForLink = function () {
var input = document.createElement('INPUT');
input.type = 'input';
input.className += 'inputForLink';
input.placeholder = 'Вставьте ссылку ...';
input.setAttribute('form', 'defaultForm');
input.setAttribute('autofocus', 'autofocus');
return input;
};
/**
* @todo Desc
*/
draw.blockButtons = function () {
var block = document.createElement('div');
block.className += 'ce-toolbar__actions';
return block;
};
/**
* Block settings panel
*/
draw.blockSettings = function () {
var settings = document.createElement('div');
settings.className += 'ce-settings';
return settings;
};
draw.defaultSettings = function () {
var div = document.createElement('div');
div.classList.add('ce-settings_default');
return div;
};
draw.pluginsSettings = function () {
var div = document.createElement('div');
div.classList.add('ce-settings_plugin');
return div;
};
draw.plusButton = function () {
var button = document.createElement('span');
button.className = 'ce-toolbar__plus';
// button.innerHTML = '<i class="ce-icon-plus"></i>';
return button;
};
/**
* Settings button in toolbar
*/
draw.settingsButton = function () {
var toggler = document.createElement('span');
toggler.className = 'ce-toolbar__settings-btn';
/** Toggler button*/
toggler.innerHTML = '<i class="ce-icon-cog"></i>';
return toggler;
};
/**
* Redactor tools wrapper
*/
draw.toolbox = function () {
var wrapper = document.createElement('div');
wrapper.className = 'ce-toolbar__tools';
return wrapper;
};
/**
* @protected
*
* Draws tool buttons for toolbox
*
* @param {String} type
* @param {String} classname
* @returns {Element}
*/
draw.toolbarButton = function (type, classname) {
var button = document.createElement('li'),
toolIcon = document.createElement('i'),
toolTitle = document.createElement('span');
button.dataset.type = type;
button.setAttribute('title', type);
toolIcon.classList.add(classname);
toolTitle.classList.add('ce_toolbar_tools--title');
button.appendChild(toolIcon);
button.appendChild(toolTitle);
return button;
};
/**
* @protected
*
* Draws tools for inline toolbar
*
* @param {String} type
* @param {String} classname
*/
draw.toolbarButtonInline = function (type, classname) {
var button = document.createElement('BUTTON'),
toolIcon = document.createElement('I');
button.type = 'button';
button.dataset.type = type;
toolIcon.classList.add(classname);
button.appendChild(toolIcon);
return button;
};
/**
* Redactor block
*/
draw.block = function (tagName, content) {
var node = document.createElement(tagName);
node.innerHTML = content || '';
return node;
};
/**
* Creates Node with passed tagName and className
* @param {string} tagName
* @param {string} className
* @param {object} properties - allow to assign properties
*/
draw.node = function ( tagName, className, properties ) {
var el = document.createElement( tagName );
if ( className ) el.className = className;
if ( properties ) {
for (var name in properties) {
el[name] = properties[name];
}
}
return el;
};
/**
* Unavailable plugin block
*/
draw.unavailableBlock = function () {
var wrapper = document.createElement('DIV');
wrapper.classList.add('cdx-unavailable-block');
return wrapper;
};
return draw;
})({});

View file

@ -1,192 +0,0 @@
/**
* Codex Editor Listeners module
*
* @author Codex Team
* @version 1.0
*/
/**
* Module-decorator for event listeners assignment
*/
module.exports = function (listeners) {
var allListeners = [];
/**
* Search methods
*
* byElement, byType and byHandler returns array of suitable listeners
* one and all takes element, eventType, and handler and returns first (all) suitable listener
*
*/
listeners.search = function () {
var byElement = function (element, context) {
var listenersOnElement = [];
context = context || allListeners;
for (var i = 0; i < context.length; i++) {
var listener = context[i];
if (listener.element === element) {
listenersOnElement.push(listener);
}
}
return listenersOnElement;
};
var byType = function (eventType, context) {
var listenersWithType = [];
context = context || allListeners;
for (var i = 0; i < context.length; i++) {
var listener = context[i];
if (listener.type === eventType) {
listenersWithType.push(listener);
}
}
return listenersWithType;
};
var byHandler = function (handler, context) {
var listenersWithHandler = [];
context = context || allListeners;
for (var i = 0; i < context.length; i++) {
var listener = context[i];
if (listener.handler === handler) {
listenersWithHandler.push(listener);
}
}
return listenersWithHandler;
};
var one = function (element, eventType, handler) {
var result = allListeners;
if (element)
result = byElement(element, result);
if (eventType)
result = byType(eventType, result);
if (handler)
result = byHandler(handler, result);
return result[0];
};
var all = function (element, eventType, handler) {
var result = allListeners;
if (element)
result = byElement(element, result);
if (eventType)
result = byType(eventType, result);
if (handler)
result = byHandler(handler, result);
return result;
};
return {
byElement : byElement,
byType : byType,
byHandler : byHandler,
one : one,
all : all
};
}();
listeners.add = function (element, eventType, handler, isCapture) {
element.addEventListener(eventType, handler, isCapture);
var data = {
element: element,
type: eventType,
handler: handler
};
var alreadyAddedListener = listeners.search.one(element, eventType, handler);
if (!alreadyAddedListener) {
allListeners.push(data);
}
};
listeners.remove = function (element, eventType, handler) {
element.removeEventListener(eventType, handler);
var existingListeners = listeners.search.all(element, eventType, handler);
for (var i = 0; i < existingListeners.length; i++) {
var index = allListeners.indexOf(existingListeners[i]);
if (index > 0) {
allListeners.splice(index, 1);
}
}
};
listeners.removeAll = function () {
allListeners.map(function (current) {
listeners.remove(current.element, current.type, current.handler);
});
};
listeners.get = function (element, eventType, handler) {
return listeners.search.all(element, eventType, handler);
};
return listeners;
}({});

View file

@ -1,231 +0,0 @@
/**
* Codex Editor Notification Module
*
* @author Codex Team
* @version 1.0
*/
module.exports = (function (notifications) {
let editor = codex.editor;
var queue = [];
var addToQueue = function (settings) {
queue.push(settings);
var index = 0;
while ( index < queue.length && queue.length > 5) {
if (queue[index].type == 'confirm' || queue[index].type == 'prompt') {
index++;
continue;
}
queue[index].close();
queue.splice(index, 1);
}
};
notifications.createHolder = function () {
var holder = editor.draw.node('DIV', 'cdx-notifications-block');
editor.nodes.notifications = document.body.appendChild(holder);
return holder;
};
/**
* Error notificator. Shows block with message
* @protected
*/
notifications.errorThrown = function (errorMsg, event) {
editor.notifications.notification({message: 'This action is not available currently', type: event.type});
};
/**
*
* Appends notification
*
* settings = {
* type - notification type (reserved types: alert, confirm, prompt). Just add class 'cdx-notification-'+type
* message - notification message
* okMsg - confirm button text (default - 'Ok')
* cancelBtn - cancel button text (default - 'Cancel'). Only for confirm and prompt types
* confirm - function-handler for ok button click
* cancel - function-handler for cancel button click. Only for confirm and prompt types
* time - time (in seconds) after which notification will close (default - 10s)
* }
*
* @param settings
*/
notifications.notification = function (constructorSettings) {
/** Private vars and methods */
var notification = null,
cancel = null,
type = null,
confirm = null,
inputField = null;
var confirmHandler = function () {
close();
if (typeof confirm !== 'function' ) {
return;
}
if (type == 'prompt') {
confirm(inputField.value);
return;
}
confirm();
};
var cancelHandler = function () {
close();
if (typeof cancel !== 'function' ) {
return;
}
cancel();
};
/** Public methods */
function create(settings) {
if (!(settings && settings.message)) {
editor.core.log('Can\'t create notification. Message is missed');
return;
}
settings.type = settings.type || 'alert';
settings.time = settings.time*1000 || 10000;
var wrapper = editor.draw.node('DIV', 'cdx-notification'),
message = editor.draw.node('DIV', 'cdx-notification__message'),
input = editor.draw.node('INPUT', 'cdx-notification__input'),
okBtn = editor.draw.node('SPAN', 'cdx-notification__ok-btn'),
cancelBtn = editor.draw.node('SPAN', 'cdx-notification__cancel-btn');
message.textContent = settings.message;
okBtn.textContent = settings.okMsg || 'ОК';
cancelBtn.textContent = settings.cancelMsg || 'Отмена';
editor.listeners.add(okBtn, 'click', confirmHandler);
editor.listeners.add(cancelBtn, 'click', cancelHandler);
wrapper.appendChild(message);
if (settings.type == 'prompt') {
wrapper.appendChild(input);
}
wrapper.appendChild(okBtn);
if (settings.type == 'prompt' || settings.type == 'confirm') {
wrapper.appendChild(cancelBtn);
}
wrapper.classList.add('cdx-notification-' + settings.type);
wrapper.dataset.type = settings.type;
notification = wrapper;
type = settings.type;
confirm = settings.confirm;
cancel = settings.cancel;
inputField = input;
if (settings.type != 'prompt' && settings.type != 'confirm') {
window.setTimeout(close, settings.time);
}
};
/**
* Show notification block
*/
function send() {
editor.nodes.notifications.appendChild(notification);
inputField.focus();
editor.nodes.notifications.classList.add('cdx-notification__notification-appending');
window.setTimeout(function () {
editor.nodes.notifications.classList.remove('cdx-notification__notification-appending');
}, 100);
addToQueue({type: type, close: close});
};
/**
* Remove notification block
*/
function close() {
notification.remove();
};
if (constructorSettings) {
create(constructorSettings);
send();
}
return {
create: create,
send: send,
close: close
};
};
notifications.clear = function () {
editor.nodes.notifications.innerHTML = '';
queue = [];
};
return notifications;
})({});

View file

@ -1,36 +0,0 @@
/**
* Codex Editor Parser Module
*
* @author Codex Team
* @version 1.1
*/
module.exports = (function (parser) {
let editor = codex.editor;
/** inserting text */
parser.insertPastedContent = function (blockType, tag) {
editor.content.insertBlock({
type : blockType.type,
block : blockType.render({
text : tag.innerHTML
})
});
};
/**
* Check DOM node for display style: separated block or child-view
*/
parser.isFirstLevelBlock = function (node) {
return node.nodeType == editor.core.nodeTypes.TAG &&
node.classList.contains(editor.ui.className.BLOCK_CLASSNAME);
};
return parser;
})({});

View file

@ -1,278 +0,0 @@
/**
* Codex Editor Paste module
*
* @author Codex Team
* @version 1.1.1
*/
module.exports = function (paste) {
let editor = codex.editor;
var patterns = [];
paste.prepare = function () {
var tools = editor.tools;
for (var tool in tools) {
if (!tools[tool].renderOnPastePatterns || !Array.isArray(tools[tool].renderOnPastePatterns)) {
continue;
}
tools[tool].renderOnPastePatterns.map(function (pattern) {
patterns.push(pattern);
});
}
return Promise.resolve();
};
/**
* Saves data
* @param event
*/
paste.pasted = function (event) {
var clipBoardData = event.clipboardData || window.clipboardData,
content = clipBoardData.getData('Text');
var result = analize(content);
if (result) {
event.preventDefault();
event.stopImmediatePropagation();
}
return result;
};
/**
* Analizes pated string and calls necessary method
*/
var analize = function (string) {
var result = false,
content = editor.content.currentNode,
plugin = content.dataset.tool;
patterns.map( function (pattern) {
var execArray = pattern.regex.exec(string),
match = execArray && execArray[0];
if ( match && match === string.trim()) {
/** current block is not empty */
if ( content.textContent.trim() && plugin == editor.settings.initialBlockPlugin ) {
pasteToNewBlock_();
}
pattern.callback(string, pattern);
result = true;
}
});
return result;
};
var pasteToNewBlock_ = function () {
/** Create new initial block */
editor.content.insertBlock({
type : editor.settings.initialBlockPlugin,
block : editor.tools[editor.settings.initialBlockPlugin].render({
text : ''
})
}, false);
};
/**
* This method prevents default behaviour.
*
* @param {Object} event
* @protected
*
* @description We get from clipboard pasted data, sanitize, make a fragment that contains of this sanitized nodes.
* Firstly, we need to memorize the caret position. We can do that by getting the range of selection.
* After all, we insert clear fragment into caret placed position. Then, we should move the caret to the last node
*/
paste.blockPasteCallback = function (event) {
if (!needsToHandlePasteEvent(event.target)) {
return;
}
/** Prevent default behaviour */
event.preventDefault();
/** get html pasted data - dirty data */
var htmlData = event.clipboardData.getData('text/html'),
plainData = event.clipboardData.getData('text/plain');
/** Temporary DIV that is used to work with text's paragraphs as DOM-elements*/
var paragraphs = editor.draw.node('DIV', '', {}),
cleanData,
wrappedData;
/** Create fragment, that we paste to range after proccesing */
cleanData = editor.sanitizer.clean(htmlData);
/**
* We wrap pasted text with <p> tags to split it logically
* @type {string}
*/
wrappedData = editor.content.wrapTextWithParagraphs(cleanData, plainData);
paragraphs.innerHTML = wrappedData;
/**
* If there only one paragraph, just insert in at the caret location
*/
if (paragraphs.childNodes.length == 1) {
emulateUserAgentBehaviour(paragraphs.firstChild);
return;
}
insertPastedParagraphs(paragraphs.childNodes);
};
/**
* Checks if we should handle paste event on block
* @param block
*
* @return {boolean}
*/
var needsToHandlePasteEvent = function (block) {
/** If area is input or textarea then allow default behaviour */
if ( editor.core.isNativeInput(block) ) {
return false;
}
var editableParent = editor.content.getEditableParent(block);
/** Allow paste when event target placed in Editable element */
if (!editableParent) {
return false;
}
return true;
};
/**
* Inserts new initial plugin blocks with data in paragraphs
*
* @param {Array} paragraphs - array of paragraphs (<p></p>) whit content, that should be inserted
*/
var insertPastedParagraphs = function (paragraphs) {
var NEW_BLOCK_TYPE = editor.settings.initialBlockPlugin,
currentNode = editor.content.currentNode;
paragraphs.forEach(function (paragraph) {
/** Don't allow empty paragraphs */
if (editor.core.isBlockEmpty(paragraph)) {
return;
}
editor.content.insertBlock({
type : NEW_BLOCK_TYPE,
block : editor.tools[NEW_BLOCK_TYPE].render({
text : paragraph.innerHTML
})
});
editor.caret.inputIndex++;
});
editor.caret.setToPreviousBlock(editor.caret.getCurrentInputIndex() + 1);
/**
* If there was no data in working node, remove it
*/
if (editor.core.isBlockEmpty(currentNode)) {
currentNode.remove();
editor.ui.saveInputs();
}
};
/**
* Inserts node content at the caret position
*
* @param {Node} node - DOM node (could be DocumentFragment), that should be inserted at the caret location
*/
var emulateUserAgentBehaviour = function (node) {
var newNode;
if (node.childElementCount) {
newNode = document.createDocumentFragment();
node.childNodes.forEach(function (current) {
if (!editor.core.isDomNode(current) && current.data.trim() === '') {
return;
}
newNode.appendChild(current.cloneNode(true));
});
} else {
newNode = document.createTextNode(node.textContent);
}
editor.caret.insertNode(newNode);
};
return paste;
}({});

View file

@ -1,203 +0,0 @@
/**
* Codex Editor Renderer Module
*
* @author Codex Team
* @version 1.0
*/
module.exports = (function (renderer) {
let editor = codex.editor;
/**
* Asyncronously parses input JSON to redactor blocks
*/
renderer.makeBlocksFromData = function () {
/**
* If redactor is empty, add first paragraph to start writing
*/
if (editor.core.isEmpty(editor.state.blocks) || !editor.state.blocks.items.length) {
editor.ui.addInitialBlock();
return;
}
Promise.resolve()
/** First, get JSON from state */
.then(function () {
return editor.state.blocks;
})
/** Then, start to iterate they */
.then(editor.renderer.appendBlocks)
/** Write log if something goes wrong */
.catch(function (error) {
editor.core.log('Error while parsing JSON: %o', 'error', error);
});
};
/**
* Parses JSON to blocks
* @param {object} data
* @return Primise -> nodeList
*/
renderer.appendBlocks = function (data) {
var blocks = data.items;
/**
* Sequence of one-by-one blocks appending
* Uses to save blocks order after async-handler
*/
var nodeSequence = Promise.resolve();
for (var index = 0; index < blocks.length ; index++ ) {
/** Add node to sequence at specified index */
editor.renderer.appendNodeAtIndex(nodeSequence, blocks, index);
}
};
/**
* Append node at specified index
*/
renderer.appendNodeAtIndex = function (nodeSequence, blocks, index) {
/** We need to append node to sequence */
nodeSequence
/** first, get node async-aware */
.then(function () {
return editor.renderer.getNodeAsync(blocks, index);
})
/**
* second, compose editor-block from JSON object
*/
.then(editor.renderer.createBlockFromData)
/**
* now insert block to redactor
*/
.then(function (blockData) {
/**
* blockData has 'block', 'type' and 'stretched' information
*/
editor.content.insertBlock(blockData);
/** Pass created block to next step */
return blockData.block;
})
/** Log if something wrong with node */
.catch(function (error) {
editor.core.log('Node skipped while parsing because %o', 'error', error);
});
};
/**
* Asynchronously returns block data from blocksList by index
* @return Promise to node
*/
renderer.getNodeAsync = function (blocksList, index) {
return Promise.resolve().then(function () {
return {
tool : blocksList[index],
position : index
};
});
};
/**
* Creates editor block by JSON-data
*
* @uses render method of each plugin
*
* @param {Object} toolData.tool
* { header : {
* text: '',
* type: 'H3', ...
* }
* }
* @param {Number} toolData.position - index in input-blocks array
* @return {Object} with type and Element
*/
renderer.createBlockFromData = function ( toolData ) {
/** New parser */
var block,
tool = toolData.tool,
pluginName = tool.type;
/** Get first key of object that stores plugin name */
// for (var pluginName in blockData) break;
/** Check for plugin existance */
if (!editor.tools[pluginName]) {
throw Error(`Plugin «${pluginName}» not found`);
}
/** Check for plugin having render method */
if (typeof editor.tools[pluginName].render != 'function') {
throw Error(`Plugin «${pluginName}» must have «render» method`);
}
if ( editor.tools[pluginName].available === false ) {
block = editor.draw.unavailableBlock();
block.innerHTML = editor.tools[pluginName].loadingMessage;
/**
* Saver will extract data from initial block data by position in array
*/
block.dataset.inputPosition = toolData.position;
} else {
/** New Parser */
block = editor.tools[pluginName].render(tool.data);
}
/** is first-level block stretched */
var stretched = editor.tools[pluginName].isStretched || false;
/** Retrun type and block */
return {
type : pluginName,
block : block,
stretched : stretched
};
};
return renderer;
})({});

View file

@ -1,80 +0,0 @@
/**
* Codex Sanitizer
*/
module.exports = (function (sanitizer) {
/** HTML Janitor library */
let janitor = require('html-janitor');
/** Codex Editor */
let editor = codex.editor;
sanitizer.prepare = function () {
if (editor.settings.sanitizer && !editor.core.isEmpty(editor.settings.sanitizer)) {
Config.CUSTOM = editor.settings.sanitizer;
}
};
/**
* Basic config
*/
var Config = {
/** User configuration */
CUSTOM : null,
BASIC : {
tags: {
p: {},
a: {
href: true,
target: '_blank',
rel: 'nofollow'
}
}
}
};
sanitizer.Config = Config;
/**
*
* @param userCustomConfig
* @returns {*}
* @private
*
* @description If developer uses editor's API, then he can customize sane restrictions.
* Or, sane config can be defined globally in editors initialization. That config will be used everywhere
* At least, if there is no config overrides, that API uses BASIC Default configation
*/
let init_ = function (userCustomConfig) {
let configuration = userCustomConfig || Config.CUSTOM || Config.BASIC;
return new janitor(configuration);
};
/**
* Cleans string from unwanted tags
* @protected
* @param {String} dirtyString - taint string
* @param {Object} customConfig - allowed tags
*/
sanitizer.clean = function (dirtyString, customConfig) {
let janitorInstance = init_(customConfig);
return janitorInstance.clean(dirtyString);
};
return sanitizer;
})({});

View file

@ -1,165 +0,0 @@
/**
* Codex Editor Saver
*
* @author Codex Team
* @version 1.1.0
*/
module.exports = (function (saver) {
let editor = codex.editor;
/**
* @public
* Save blocks
*/
saver.save = function () {
/** Save html content of redactor to memory */
editor.state.html = editor.nodes.redactor.innerHTML;
/** Clean jsonOutput state */
editor.state.jsonOutput = [];
return saveBlocks(editor.nodes.redactor.childNodes);
};
/**
* @private
* Save each block data
*
* @param blocks
* @returns {Promise.<TResult>}
*/
let saveBlocks = function (blocks) {
let data = [];
for(let index = 0; index < blocks.length; index++) {
data.push(getBlockData(blocks[index]));
}
return Promise.all(data)
.then(makeOutput)
.catch(editor.core.log);
};
/** Save and validate block data */
let getBlockData = function (block) {
return saveBlockData(block)
.then(validateBlockData)
.catch(editor.core.log);
};
/**
* @private
* Call block`s plugin save method and return saved data
*
* @param block
* @returns {Object}
*/
let saveBlockData = function (block) {
let pluginName = block.dataset.tool;
/** Check for plugin existence */
if (!editor.tools[pluginName]) {
editor.core.log(`Plugin «${pluginName}» not found`, 'error');
return {data: null, pluginName: null};
}
/** Check for plugin having save method */
if (typeof editor.tools[pluginName].save !== 'function') {
editor.core.log(`Plugin «${pluginName}» must have save method`, 'error');
return {data: null, pluginName: null};
}
/** Result saver */
let blockContent = block.childNodes[0],
pluginsContent = blockContent.childNodes[0],
position = pluginsContent.dataset.inputPosition;
/** If plugin wasn't available then return data from cache */
if ( editor.tools[pluginName].available === false ) {
return Promise.resolve({data: codex.editor.state.blocks.items[position].data, pluginName});
}
return Promise.resolve(pluginsContent)
.then(editor.tools[pluginName].save)
.then(data => Object({data, pluginName}));
};
/**
* Call plugin`s validate method. Return false if validation failed
*
* @param data
* @param pluginName
* @returns {Object|Boolean}
*/
let validateBlockData = function ({data, pluginName}) {
if (!data || !pluginName) {
return false;
}
if (editor.tools[pluginName].validate) {
let result = editor.tools[pluginName].validate(data);
/**
* Do not allow invalid data
*/
if (!result) {
return false;
}
}
return {data, pluginName};
};
/**
* Compile article output
*
* @param savedData
* @returns {{time: number, version, items: (*|Array)}}
*/
let makeOutput = function (savedData) {
savedData = savedData.filter(blockData => blockData);
let items = savedData.map(blockData => Object({type: blockData.pluginName, data: blockData.data}));
editor.state.jsonOutput = items;
return {
id: editor.state.blocks.id || null,
time: +new Date(),
version: editor.version,
items
};
};
return saver;
})({});

View file

@ -1,593 +0,0 @@
/**
* Inline toolbar
*
* Contains from tools:
* Bold, Italic, Underline and Anchor
*
* @author Codex Team
* @version 1.0
*/
module.exports = (function (inline) {
let editor = codex.editor;
inline.buttonsOpened = null;
inline.actionsOpened = null;
inline.wrappersOffset = null;
/**
* saving selection that need for execCommand for styling
*
*/
inline.storedSelection = null;
/**
* @protected
*
* Open inline toobar
*/
inline.show = function () {
var currentNode = editor.content.currentNode,
tool = currentNode.dataset.tool,
plugin;
/**
* tool allowed to open inline toolbar
*/
plugin = editor.tools[tool];
if (!plugin.showInlineToolbar)
return;
var selectedText = inline.getSelectionText(),
toolbar = editor.nodes.inlineToolbar.wrapper;
if (selectedText.length > 0) {
/** Move toolbar and open */
editor.toolbar.inline.move();
/** Open inline toolbar */
toolbar.classList.add('opened');
/** show buttons of inline toolbar */
editor.toolbar.inline.showButtons();
}
};
/**
* @protected
*
* Closes inline toolbar
*/
inline.close = function () {
var toolbar = editor.nodes.inlineToolbar.wrapper;
toolbar.classList.remove('opened');
};
/**
* @private
*
* Moving toolbar
*/
inline.move = function () {
if (!this.wrappersOffset) {
this.wrappersOffset = this.getWrappersOffset();
}
var coords = this.getSelectionCoords(),
defaultOffset = 0,
toolbar = editor.nodes.inlineToolbar.wrapper,
newCoordinateX,
newCoordinateY;
if (toolbar.offsetHeight === 0) {
defaultOffset = 40;
}
newCoordinateX = coords.x - this.wrappersOffset.left;
newCoordinateY = coords.y + window.scrollY - this.wrappersOffset.top - defaultOffset - toolbar.offsetHeight;
toolbar.style.transform = `translate3D(${Math.floor(newCoordinateX)}px, ${Math.floor(newCoordinateY)}px, 0)`;
/** Close everything */
editor.toolbar.inline.closeButtons();
editor.toolbar.inline.closeAction();
};
/**
* @private
*
* Tool Clicked
*/
inline.toolClicked = function (event, type) {
/**
* For simple tools we use default browser function
* For more complicated tools, we should write our own behavior
*/
switch (type) {
case 'createLink' : editor.toolbar.inline.createLinkAction(event, type); break;
default : editor.toolbar.inline.defaultToolAction(type); break;
}
/**
* highlight buttons
* after making some action
*/
editor.nodes.inlineToolbar.buttons.childNodes.forEach(editor.toolbar.inline.hightlight);
};
/**
* @private
*
* Saving wrappers offset in DOM
*/
inline.getWrappersOffset = function () {
var wrapper = editor.nodes.wrapper,
offset = this.getOffset(wrapper);
this.wrappersOffset = offset;
return offset;
};
/**
* @private
*
* Calculates offset of DOM element
*
* @param el
* @returns {{top: number, left: number}}
*/
inline.getOffset = function ( el ) {
var _x = 0;
var _y = 0;
while( el && !isNaN( el.offsetLeft ) && !isNaN( el.offsetTop ) ) {
_x += (el.offsetLeft + el.clientLeft);
_y += (el.offsetTop + el.clientTop);
el = el.offsetParent;
}
return { top: _y, left: _x };
};
/**
* @private
*
* Calculates position of selected text
* @returns {{x: number, y: number}}
*/
inline.getSelectionCoords = function () {
var sel = document.selection, range;
var x = 0, y = 0;
if (sel) {
if (sel.type != 'Control') {
range = sel.createRange();
range.collapse(true);
x = range.boundingLeft;
y = range.boundingTop;
}
} else if (window.getSelection) {
sel = window.getSelection();
if (sel.rangeCount) {
range = sel.getRangeAt(0).cloneRange();
if (range.getClientRects) {
range.collapse(true);
var rect = range.getClientRects()[0];
if (!rect) {
return;
}
x = rect.left;
y = rect.top;
}
}
}
return { x: x, y: y };
};
/**
* @private
*
* Returns selected text as String
* @returns {string}
*/
inline.getSelectionText = function () {
var selectedText = '';
// all modern browsers and IE9+
if (window.getSelection) {
selectedText = window.getSelection().toString();
}
return selectedText;
};
/** Opens buttons block */
inline.showButtons = function () {
var buttons = editor.nodes.inlineToolbar.buttons;
buttons.classList.add('opened');
editor.toolbar.inline.buttonsOpened = true;
/** highlight buttons */
editor.nodes.inlineToolbar.buttons.childNodes.forEach(editor.toolbar.inline.hightlight);
};
/** Makes buttons disappear */
inline.closeButtons = function () {
var buttons = editor.nodes.inlineToolbar.buttons;
buttons.classList.remove('opened');
editor.toolbar.inline.buttonsOpened = false;
};
/** Open buttons defined action if exist */
inline.showActions = function () {
var action = editor.nodes.inlineToolbar.actions;
action.classList.add('opened');
editor.toolbar.inline.actionsOpened = true;
};
/** Close actions block */
inline.closeAction = function () {
var action = editor.nodes.inlineToolbar.actions;
action.innerHTML = '';
action.classList.remove('opened');
editor.toolbar.inline.actionsOpened = false;
};
/**
* Callback for keydowns in inline toolbar "Insert link..." input
*/
let inlineToolbarAnchorInputKeydown_ = function (event) {
if (event.keyCode != editor.core.keys.ENTER) {
return;
}
let editable = editor.content.currentNode,
storedSelection = editor.toolbar.inline.storedSelection;
editor.toolbar.inline.restoreSelection(editable, storedSelection);
editor.toolbar.inline.setAnchor(this.value);
/**
* Preventing events that will be able to happen
*/
event.preventDefault();
event.stopImmediatePropagation();
editor.toolbar.inline.clearRange();
};
/** Action for link creation or for setting anchor */
inline.createLinkAction = function (event) {
var isActive = this.isLinkActive();
var editable = editor.content.currentNode,
storedSelection = editor.toolbar.inline.saveSelection(editable);
/** Save globally selection */
editor.toolbar.inline.storedSelection = storedSelection;
if (isActive) {
/**
* Changing stored selection. if we want to remove anchor from word
* we should remove anchor from whole word, not only selected part.
* The solution is than we get the length of current link
* Change start position to - end of selection minus length of anchor
*/
editor.toolbar.inline.restoreSelection(editable, storedSelection);
editor.toolbar.inline.defaultToolAction('unlink');
} else {
/** Create input and close buttons */
var action = editor.draw.inputForLink();
editor.nodes.inlineToolbar.actions.appendChild(action);
editor.toolbar.inline.closeButtons();
editor.toolbar.inline.showActions();
/**
* focus to input
* Solution: https://developer.mozilla.org/ru/docs/Web/API/HTMLElement/focus
* Prevents event after showing input and when we need to focus an input which is in unexisted form
*/
action.focus();
event.preventDefault();
/** Callback to link action */
editor.listeners.add(action, 'keydown', inlineToolbarAnchorInputKeydown_, false);
}
};
inline.isLinkActive = function () {
var isActive = false;
editor.nodes.inlineToolbar.buttons.childNodes.forEach(function (tool) {
var dataType = tool.dataset.type;
if (dataType == 'link' && tool.classList.contains('hightlighted')) {
isActive = true;
}
});
return isActive;
};
/** default action behavior of tool */
inline.defaultToolAction = function (type) {
document.execCommand(type, false, null);
};
/**
* @private
*
* Sets URL
*
* @param {String} url - URL
*/
inline.setAnchor = function (url) {
document.execCommand('createLink', false, url);
/** Close after URL inserting */
editor.toolbar.inline.closeAction();
};
/**
* @private
*
* Saves selection
*/
inline.saveSelection = function (containerEl) {
var range = window.getSelection().getRangeAt(0),
preSelectionRange = range.cloneRange(),
start;
preSelectionRange.selectNodeContents(containerEl);
preSelectionRange.setEnd(range.startContainer, range.startOffset);
start = preSelectionRange.toString().length;
return {
start: start,
end: start + range.toString().length
};
};
/**
* @private
*
* Sets to previous selection (Range)
*
* @param {Element} containerEl - editable element where we restore range
* @param {Object} savedSel - range basic information to restore
*/
inline.restoreSelection = function (containerEl, savedSel) {
var range = document.createRange(),
charIndex = 0;
range.setStart(containerEl, 0);
range.collapse(true);
var nodeStack = [ containerEl ],
node,
foundStart = false,
stop = false,
nextCharIndex;
while (!stop && (node = nodeStack.pop())) {
if (node.nodeType == 3) {
nextCharIndex = charIndex + node.length;
if (!foundStart && savedSel.start >= charIndex && savedSel.start <= nextCharIndex) {
range.setStart(node, savedSel.start - charIndex);
foundStart = true;
}
if (foundStart && savedSel.end >= charIndex && savedSel.end <= nextCharIndex) {
range.setEnd(node, savedSel.end - charIndex);
stop = true;
}
charIndex = nextCharIndex;
} else {
var i = node.childNodes.length;
while (i--) {
nodeStack.push(node.childNodes[i]);
}
}
}
var sel = window.getSelection();
sel.removeAllRanges();
sel.addRange(range);
};
/**
* @private
*
* Removes all ranges from window selection
*/
inline.clearRange = function () {
var selection = window.getSelection();
selection.removeAllRanges();
};
/**
* @private
*
* sets or removes hightlight
*/
inline.hightlight = function (tool) {
var dataType = tool.dataset.type;
if (document.queryCommandState(dataType)) {
editor.toolbar.inline.setButtonHighlighted(tool);
} else {
editor.toolbar.inline.removeButtonsHighLight(tool);
}
/**
*
* hightlight for anchors
*/
var selection = window.getSelection(),
tag = selection.anchorNode.parentNode;
if (tag.tagName == 'A' && dataType == 'link') {
editor.toolbar.inline.setButtonHighlighted(tool);
}
};
/**
* @private
*
* Mark button if text is already executed
*/
inline.setButtonHighlighted = function (button) {
button.classList.add('hightlighted');
/** At link tool we also change icon */
if (button.dataset.type == 'link') {
var icon = button.childNodes[0];
icon.classList.remove('ce-icon-link');
icon.classList.add('ce-icon-unlink');
}
};
/**
* @private
*
* Removes hightlight
*/
inline.removeButtonsHighLight = function (button) {
button.classList.remove('hightlighted');
/** At link tool we also change icon */
if (button.dataset.type == 'link') {
var icon = button.childNodes[0];
icon.classList.remove('ce-icon-unlink');
icon.classList.add('ce-icon-link');
}
};
return inline;
})({});

View file

@ -1,172 +0,0 @@
/**
* Toolbar settings
*
* @version 1.0.5
*/
module.exports = (function (settings) {
let editor = codex.editor;
settings.opened = false;
settings.setting = null;
settings.actions = null;
/**
* Append and open settings
*/
settings.open = function (toolType) {
/**
* Append settings content
* It's stored in tool.settings
*/
if ( !editor.tools[toolType] || !editor.tools[toolType].makeSettings ) {
return;
}
/**
* Draw settings block
*/
var settingsBlock = editor.tools[toolType].makeSettings();
editor.nodes.pluginSettings.appendChild(settingsBlock);
/** Open settings block */
editor.nodes.blockSettings.classList.add('opened');
this.opened = true;
};
/**
* Close and clear settings
*/
settings.close = function () {
editor.nodes.blockSettings.classList.remove('opened');
editor.nodes.pluginSettings.innerHTML = '';
this.opened = false;
};
/**
* @param {string} toolType - plugin type
*/
settings.toggle = function ( toolType ) {
if ( !this.opened ) {
this.open(toolType);
} else {
this.close();
}
};
/**
* Here we will draw buttons and add listeners to components
*/
settings.makeRemoveBlockButton = function () {
var removeBlockWrapper = editor.draw.node('SPAN', 'ce-toolbar__remove-btn', {}),
settingButton = editor.draw.node('SPAN', 'ce-toolbar__remove-setting', { innerHTML : '<i class="ce-icon-trash"></i>' }),
actionWrapper = editor.draw.node('DIV', 'ce-toolbar__remove-confirmation', {}),
confirmAction = editor.draw.node('DIV', 'ce-toolbar__remove-confirm', { textContent : 'Удалить блок' }),
cancelAction = editor.draw.node('DIV', 'ce-toolbar__remove-cancel', { textContent : 'Отмена' });
editor.listeners.add(settingButton, 'click', editor.toolbar.settings.removeButtonClicked, false);
editor.listeners.add(confirmAction, 'click', editor.toolbar.settings.confirmRemovingRequest, false);
editor.listeners.add(cancelAction, 'click', editor.toolbar.settings.cancelRemovingRequest, false);
actionWrapper.appendChild(confirmAction);
actionWrapper.appendChild(cancelAction);
removeBlockWrapper.appendChild(settingButton);
removeBlockWrapper.appendChild(actionWrapper);
/** Save setting */
editor.toolbar.settings.setting = settingButton;
editor.toolbar.settings.actions = actionWrapper;
return removeBlockWrapper;
};
settings.removeButtonClicked = function () {
var action = editor.toolbar.settings.actions;
if (action.classList.contains('opened')) {
editor.toolbar.settings.hideRemoveActions();
} else {
editor.toolbar.settings.showRemoveActions();
}
editor.toolbar.toolbox.close();
editor.toolbar.settings.close();
};
settings.cancelRemovingRequest = function () {
editor.toolbar.settings.actions.classList.remove('opened');
};
settings.confirmRemovingRequest = function () {
var currentBlock = editor.content.currentNode,
firstLevelBlocksCount;
currentBlock.remove();
firstLevelBlocksCount = editor.nodes.redactor.childNodes.length;
/**
* If all blocks are removed
*/
if (firstLevelBlocksCount === 0) {
/** update currentNode variable */
editor.content.currentNode = null;
/** Inserting new empty initial block */
editor.ui.addInitialBlock();
}
editor.ui.saveInputs();
editor.toolbar.close();
};
settings.showRemoveActions = function () {
editor.toolbar.settings.actions.classList.add('opened');
};
settings.hideRemoveActions = function () {
editor.toolbar.settings.actions.classList.remove('opened');
};
return settings;
})({});

View file

@ -1,133 +0,0 @@
/**
* Codex Editor toolbar module
*
* Contains:
* - Inline toolbox
* - Toolbox within plus button
* - Settings section
*
* @author Codex Team
* @version 1.0
*/
module.exports = (function (toolbar) {
let editor = codex.editor;
toolbar.settings = require('./settings');
toolbar.inline = require('./inline');
toolbar.toolbox = require('./toolbox');
/**
* Margin between focused node and toolbar
*/
toolbar.defaultToolbarHeight = 49;
toolbar.defaultOffset = 34;
toolbar.opened = false;
toolbar.current = null;
/**
* @protected
*/
toolbar.open = function () {
if (editor.hideToolbar) {
return;
}
let toolType = editor.content.currentNode.dataset.tool;
if (!editor.tools[toolType] || !editor.tools[toolType].makeSettings ) {
editor.nodes.showSettingsButton.classList.add('hide');
} else {
editor.nodes.showSettingsButton.classList.remove('hide');
}
editor.nodes.toolbar.classList.add('opened');
this.opened = true;
};
/**
* @protected
*/
toolbar.close = function () {
editor.nodes.toolbar.classList.remove('opened');
toolbar.opened = false;
toolbar.current = null;
for (var button in editor.nodes.toolbarButtons) {
editor.nodes.toolbarButtons[button].classList.remove('selected');
}
/** Close toolbox when toolbar is not displayed */
editor.toolbar.toolbox.close();
editor.toolbar.settings.close();
};
toolbar.toggle = function () {
if ( !this.opened ) {
this.open();
} else {
this.close();
}
};
toolbar.hidePlusButton = function () {
editor.nodes.plusButton.classList.add('hide');
};
toolbar.showPlusButton = function () {
editor.nodes.plusButton.classList.remove('hide');
};
/**
* Moving toolbar to the specified node
*/
toolbar.move = function () {
/** Close Toolbox when we move toolbar */
editor.toolbar.toolbox.close();
if (!editor.content.currentNode) {
return;
}
var newYCoordinate = editor.content.currentNode.offsetTop - (editor.toolbar.defaultToolbarHeight / 2) + editor.toolbar.defaultOffset;
editor.nodes.toolbar.style.transform = `translate3D(0, ${Math.floor(newYCoordinate)}px, 0)`;
/** Close trash actions */
editor.toolbar.settings.hideRemoveActions();
};
return toolbar;
})({});

View file

@ -1,191 +0,0 @@
/**
* Codex Editor toolbox
*
* All tools be able to appended here
*
* @author Codex Team
* @version 1.0
*/
module.exports = (function (toolbox) {
let editor = codex.editor;
toolbox.opened = false;
toolbox.openedOnBlock = null;
/** Shows toolbox */
toolbox.open = function () {
/** Close setting if toolbox is opened */
if (editor.toolbar.settings.opened) {
editor.toolbar.settings.close();
}
/** Add 'toolbar-opened' class for current block **/
toolbox.openedOnBlock = editor.content.currentNode;
toolbox.openedOnBlock.classList.add('toolbar-opened');
/** display toolbox */
editor.nodes.toolbox.classList.add('opened');
/** Animate plus button */
editor.nodes.plusButton.classList.add('clicked');
/** toolbox state */
editor.toolbar.toolbox.opened = true;
};
/** Closes toolbox */
toolbox.close = function () {
/** Remove 'toolbar-opened' class from current block **/
if (toolbox.openedOnBlock) toolbox.openedOnBlock.classList.remove('toolbar-opened');
toolbox.openedOnBlock = null;
/** Makes toolbox disappear */
editor.nodes.toolbox.classList.remove('opened');
/** Rotate plus button */
editor.nodes.plusButton.classList.remove('clicked');
/** toolbox state */
editor.toolbar.toolbox.opened = false;
editor.toolbar.current = null;
};
toolbox.leaf = function () {
let currentTool = editor.toolbar.current,
tools = Object.keys(editor.tools),
barButtons = editor.nodes.toolbarButtons,
nextToolIndex = 0,
toolToSelect,
visibleTool,
tool;
if ( !currentTool ) {
/** Get first tool from object*/
for(tool in editor.tools) {
if (editor.tools[tool].displayInToolbox) {
break;
}
nextToolIndex ++;
}
} else {
nextToolIndex = (tools.indexOf(currentTool) + 1) % tools.length;
visibleTool = tools[nextToolIndex];
while (!editor.tools[visibleTool].displayInToolbox) {
nextToolIndex = (nextToolIndex + 1) % tools.length;
visibleTool = tools[nextToolIndex];
}
}
toolToSelect = tools[nextToolIndex];
for ( var button in barButtons ) {
barButtons[button].classList.remove('selected');
}
barButtons[toolToSelect].classList.add('selected');
editor.toolbar.current = toolToSelect;
};
/**
* Transforming selected node type into selected toolbar element type
* @param {event} event
*/
toolbox.toolClicked = function (event) {
/**
* UNREPLACEBLE_TOOLS this types of tools are forbidden to replace even they are empty
*/
var UNREPLACEBLE_TOOLS = ['image', 'link', 'list', 'instagram', 'twitter', 'embed'],
tool = editor.tools[editor.toolbar.current],
workingNode = editor.content.currentNode,
currentInputIndex = editor.caret.inputIndex,
newBlockContent,
appendCallback,
blockData;
/** Make block from plugin */
newBlockContent = tool.render();
/** information about block */
blockData = {
block : newBlockContent,
type : tool.type,
stretched : false
};
if (
workingNode &&
UNREPLACEBLE_TOOLS.indexOf(workingNode.dataset.tool) === -1 &&
workingNode.textContent.trim() === ''
) {
/** Replace current block */
editor.content.switchBlock(workingNode, newBlockContent, tool.type);
} else {
/** Insert new Block from plugin */
editor.content.insertBlock(blockData);
/** increase input index */
currentInputIndex++;
}
/** Fire tool append callback */
appendCallback = tool.appendCallback;
if (appendCallback && typeof appendCallback == 'function') {
appendCallback.call(event);
}
window.setTimeout(function () {
/** Set caret to current block */
editor.caret.setToBlock(currentInputIndex);
}, 10);
/**
* Changing current Node
*/
editor.content.workingNodeChanged();
/**
* Move toolbar when node is changed
*/
editor.toolbar.move();
};
return toolbox;
})({});

View file

@ -1,153 +0,0 @@
/**
* Module working with plugins
*/
module.exports = (function () {
let editor = codex.editor;
/**
* Initialize plugins before using
* Ex. Load scripts or call some internal methods
* @return Promise
*/
function prepare() {
return new Promise(function (resolve_, reject_) {
Promise.resolve()
/**
* Compose a sequence of plugins that requires preparation
*/
.then(function () {
let pluginsRequiresPreparation = [],
allPlugins = editor.tools;
for ( let pluginName in allPlugins ) {
let plugin = allPlugins[pluginName];
if (plugin.prepare && typeof plugin.prepare != 'function' || !plugin.prepare) {
continue;
}
pluginsRequiresPreparation.push(plugin);
}
/**
* If no one passed plugins requires preparation, finish prepare() and go ahead
*/
if (!pluginsRequiresPreparation.length) {
resolve_();
}
return pluginsRequiresPreparation;
})
/** Wait plugins while they prepares */
.then(waitAllPluginsPreparation_)
.then(function () {
editor.core.log('Plugins loaded', 'info');
resolve_();
}).catch(function (error) {
reject_(error);
});
});
}
/**
* @param {array} plugins - list of tools that requires preparation
* @return {Promise} resolved while all plugins will be ready or failed
*/
function waitAllPluginsPreparation_(plugins) {
/**
* @calls allPluginsProcessed__ when all plugins prepared or failed
*/
return new Promise (function (allPluginsProcessed__) {
/**
* pluck each element from queue
* First, send resolved Promise as previous value
* Each plugins "prepare" method returns a Promise, that's why
* reduce current element will not be able to continue while can't get
* a resolved Promise
*
* If last plugin is "prepared" then go to the next stage of initialization
*/
plugins.reduce(function (previousValue, plugin, iteration) {
return previousValue.then(function () {
/**
* Wait till plugins prepared
* @calls pluginIsReady__ when plugin is ready or failed
*/
return new Promise ( function (pluginIsReady__) {
callPluginsPrepareMethod_( plugin )
.then( pluginIsReady__ )
.then( function () {
plugin.available = true;
})
.catch(function (error) {
editor.core.log(`Plugin «${plugin.type}» was not loaded. Preparation failed because %o`, 'warn', error);
plugin.available = false;
plugin.loadingMessage = error;
/** Go ahead even some plugin has problems */
pluginIsReady__();
})
.then(function () {
/** If last plugin has problems then just ignore and continue */
if (iteration == plugins.length - 1) {
allPluginsProcessed__();
}
});
});
});
}, Promise.resolve() );
});
}
var callPluginsPrepareMethod_ = function (plugin) {
return plugin.prepare( plugin.config || {} );
};
return {
prepare: prepare
};
}());

View file

@ -1,136 +0,0 @@
/**
*
* Codex.Editor Transport Module
*
* @copyright 2017 Codex-Team
* @version 1.2.0
*/
module.exports = (function (transport) {
let editor = codex.editor;
/**
* @private {Object} current XmlHttpRequest instance
*/
var currentRequest = null;
/**
* @type {null} | {DOMElement} input - keeps input element in memory
*/
transport.input = null;
/**
* @property {Object} arguments - keep plugin settings and defined callbacks
*/
transport.arguments = null;
/**
* Prepares input element where will be files
*/
transport.prepare = function () {
let input = editor.draw.node( 'INPUT', '', { type : 'file' } );
editor.listeners.add(input, 'change', editor.transport.fileSelected);
editor.transport.input = input;
};
/** Clear input when files is uploaded */
transport.clearInput = function () {
/** Remove old input */
transport.input = null;
/** Prepare new one */
transport.prepare();
};
/**
* Callback for file selection
* @param {Event} event
*/
transport.fileSelected = function () {
var input = this,
i,
files = input.files,
formData = new FormData();
if (editor.transport.arguments.multiple === true) {
for ( i = 0; i < files.length; i++) {
formData.append('files[]', files[i], files[i].name);
}
} else {
formData.append('files', files[0], files[0].name);
}
currentRequest = editor.core.ajax({
type : 'POST',
data : formData,
url : editor.transport.arguments.url,
beforeSend : editor.transport.arguments.beforeSend,
success : editor.transport.arguments.success,
error : editor.transport.arguments.error,
progress : editor.transport.arguments.progress
});
/** Clear input */
transport.clearInput();
};
/**
* Use plugin callbacks
* @protected
*
* @param {Object} args - can have :
* @param {String} args.url - fetch URL
* @param {Function} args.beforeSend - function calls before sending ajax
* @param {Function} args.success - success callback
* @param {Function} args.error - on error handler
* @param {Function} args.progress - xhr onprogress handler
* @param {Boolean} args.multiple - allow select several files
* @param {String} args.accept - adds accept attribute
*/
transport.selectAndUpload = function (args) {
transport.arguments = args;
if ( args.multiple === true) {
transport.input.setAttribute('multiple', 'multiple');
}
if ( args.accept ) {
transport.input.setAttribute('accept', args.accept);
}
transport.input.click();
};
transport.abort = function () {
currentRequest.abort();
currentRequest = null;
};
return transport;
})({});

View file

@ -1,431 +0,0 @@
/**
* Codex Editor UI module
*
* @author Codex Team
* @version 1.2.0
*/
module.exports = (function (ui) {
let editor = codex.editor;
/**
* Basic editor classnames
*/
ui.className = {
/**
* @const {string} BLOCK_CLASSNAME - redactor blocks name
*/
BLOCK_CLASSNAME : 'ce-block',
/**
* @const {String} wrapper for plugins content
*/
BLOCK_CONTENT : 'ce-block__content',
/**
* @const {String} BLOCK_STRETCHED - makes block stretched
*/
BLOCK_STRETCHED : 'ce-block--stretched',
/**
* @const {String} BLOCK_HIGHLIGHTED - adds background
*/
BLOCK_HIGHLIGHTED : 'ce-block--focused',
/**
* @const {String} - for all default settings
*/
SETTINGS_ITEM : 'ce-settings__item'
};
/**
* @protected
*
* Making main interface
*/
ui.prepare = function () {
return new Promise(function (resolve) {
let wrapper = editor.draw.wrapper(),
redactor = editor.draw.redactor(),
toolbar = makeToolBar_();
wrapper.appendChild(toolbar);
wrapper.appendChild(redactor);
/** Save created ui-elements to static nodes state */
editor.nodes.wrapper = wrapper;
editor.nodes.redactor = redactor;
/** Append editor wrapper with redactor zone into holder */
editor.nodes.holder.appendChild(wrapper);
resolve();
})
/** Add toolbox tools */
.then(addTools_)
/** Make container for inline toolbar */
.then(makeInlineToolbar_)
/** Add inline toolbar tools */
.then(addInlineToolbarTools_)
/** Draw wrapper for notifications */
.then(makeNotificationHolder_)
/** Add eventlisteners to redactor elements */
.then(bindEvents_)
.catch( function () {
editor.core.log("Can't draw editor interface");
});
};
/**
* @private
* Draws inline toolbar zone
*/
var makeInlineToolbar_ = function () {
var container = editor.draw.inlineToolbar();
/** Append to redactor new inline block */
editor.nodes.inlineToolbar.wrapper = container;
/** Draw toolbar buttons */
editor.nodes.inlineToolbar.buttons = editor.draw.inlineToolbarButtons();
/** Buttons action or settings */
editor.nodes.inlineToolbar.actions = editor.draw.inlineToolbarActions();
/** Append to inline toolbar buttons as part of it */
editor.nodes.inlineToolbar.wrapper.appendChild(editor.nodes.inlineToolbar.buttons);
editor.nodes.inlineToolbar.wrapper.appendChild(editor.nodes.inlineToolbar.actions);
editor.nodes.wrapper.appendChild(editor.nodes.inlineToolbar.wrapper);
};
var makeToolBar_ = function () {
let toolbar = editor.draw.toolbar(),
blockButtons = makeToolbarSettings_(),
toolbarContent = makeToolbarContent_();
/** Appending first-level block buttons */
toolbar.appendChild(blockButtons);
/** Append toolbarContent to toolbar */
toolbar.appendChild(toolbarContent);
/** Make toolbar global */
editor.nodes.toolbar = toolbar;
return toolbar;
};
var makeToolbarContent_ = function () {
let toolbarContent = editor.draw.toolbarContent(),
toolbox = editor.draw.toolbox(),
plusButton = editor.draw.plusButton();
/** Append plus button */
toolbarContent.appendChild(plusButton);
/** Appending toolbar tools */
toolbarContent.appendChild(toolbox);
/** Make Toolbox and plusButton global */
editor.nodes.toolbox = toolbox;
editor.nodes.plusButton = plusButton;
return toolbarContent;
};
var makeToolbarSettings_ = function () {
let blockSettings = editor.draw.blockSettings(),
blockButtons = editor.draw.blockButtons(),
defaultSettings = editor.draw.defaultSettings(),
showSettingsButton = editor.draw.settingsButton(),
showTrashButton = editor.toolbar.settings.makeRemoveBlockButton(),
pluginSettings = editor.draw.pluginsSettings();
/** Add default and plugins settings */
blockSettings.appendChild(pluginSettings);
blockSettings.appendChild(defaultSettings);
/**
* Make blocks buttons
* This block contains settings button and remove block button
*/
blockButtons.appendChild(showSettingsButton);
blockButtons.appendChild(showTrashButton);
blockButtons.appendChild(blockSettings);
/** Make BlockSettings, PluginSettings, DefaultSettings global */
editor.nodes.blockSettings = blockSettings;
editor.nodes.pluginSettings = pluginSettings;
editor.nodes.defaultSettings = defaultSettings;
editor.nodes.showSettingsButton = showSettingsButton;
editor.nodes.showTrashButton = showTrashButton;
return blockButtons;
};
/** Draw notifications holder */
var makeNotificationHolder_ = function () {
/** Append block with notifications to the document */
editor.nodes.notifications = editor.notifications.createHolder();
};
/**
* @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 tools = {
bold: {
icon : 'ce-icon-bold',
command : 'bold'
},
italic: {
icon : 'ce-icon-italic',
command : 'italic'
},
link: {
icon : 'ce-icon-link',
command : 'createLink'
}
};
var toolButton,
tool;
for(var name in tools) {
tool = tools[name];
toolButton = editor.draw.toolbarButtonInline(name, tool.icon);
editor.nodes.inlineToolbar.buttons.appendChild(toolButton);
/**
* Add callbacks to this buttons
*/
editor.ui.setInlineToolbarButtonBehaviour(toolButton, tool.command);
}
};
/**
* @private
* Bind editor UI events
*/
var bindEvents_ = function () {
editor.core.log('ui.bindEvents fired', 'info');
// window.addEventListener('error', function (errorMsg, url, lineNumber) {
// editor.notifications.errorThrown(errorMsg, event);
// }, false );
/** All keydowns on Document */
editor.listeners.add(document, 'keydown', editor.callback.globalKeydown, false);
/** All keydowns on Redactor zone */
editor.listeners.add(editor.nodes.redactor, 'keydown', editor.callback.redactorKeyDown, false);
/** All keydowns on Document */
editor.listeners.add(document, 'keyup', editor.callback.globalKeyup, false );
/**
* Mouse click to radactor
*/
editor.listeners.add(editor.nodes.redactor, 'click', editor.callback.redactorClicked, false );
/**
* Clicks to the Plus button
*/
editor.listeners.add(editor.nodes.plusButton, 'click', editor.callback.plusButtonClicked, false);
/**
* Clicks to SETTINGS button in toolbar
*/
editor.listeners.add(editor.nodes.showSettingsButton, 'click', editor.callback.showSettingsButtonClicked, false );
/** Bind click listeners on toolbar buttons */
for (var button in editor.nodes.toolbarButtons) {
editor.listeners.add(editor.nodes.toolbarButtons[button], 'click', editor.callback.toolbarButtonClicked, false);
}
};
ui.addBlockHandlers = function (block) {
if (!block) return;
/**
* Block keydowns
*/
editor.listeners.add(block, 'keydown', editor.callback.blockKeydown, false);
/**
* Pasting content from another source
* We have two type of sanitization
* First - uses deep-first search algorithm to get sub nodes,
* sanitizes whole Block_content and replaces cleared nodes
* This method is deprecated
* Method is used in editor.callback.blockPaste(event)
*
* Secont - uses Mutation observer.
* Observer "observe" DOM changes and send changings to callback.
* Callback gets changed node, not whole Block_content.
* Inserted or changed node, which we've gotten have been cleared and replaced with diry node
*
* Method is used in editor.callback.blockPasteViaSanitize(event)
*
* @uses html-janitor
* @example editor.callback.blockPasteViaSanitize(event), the second method.
*
*/
editor.listeners.add(block, 'paste', editor.paste.blockPasteCallback, false);
/**
* Show inline toolbar for selected text
*/
editor.listeners.add(block, 'mouseup', editor.toolbar.inline.show, false);
editor.listeners.add(block, 'keyup', editor.toolbar.inline.show, false);
};
/** getting all contenteditable elements */
ui.saveInputs = function () {
var redactor = editor.nodes.redactor;
editor.state.inputs = [];
/** Save all inputs in global variable state */
var inputs = redactor.querySelectorAll('[contenteditable], input, textarea');
Array.prototype.map.call(inputs, function (current) {
if (!current.type || current.type == 'text' || current.type == 'textarea') {
editor.state.inputs.push(current);
}
});
};
/**
* Adds first initial block on empty redactor
*/
ui.addInitialBlock = function () {
var initialBlockType = editor.settings.initialBlockPlugin,
initialBlock;
if ( !editor.tools[initialBlockType] ) {
editor.core.log('Plugin %o was not implemented and can\'t be used as initial block', 'warn', initialBlockType);
return;
}
initialBlock = editor.tools[initialBlockType].render();
initialBlock.setAttribute('data-placeholder', editor.settings.placeholder);
editor.content.insertBlock({
type : initialBlockType,
block : initialBlock
});
editor.content.workingNodeChanged(initialBlock);
};
ui.setInlineToolbarButtonBehaviour = function (button, type) {
editor.listeners.add(button, 'mousedown', function (event) {
editor.toolbar.inline.toolClicked(event, type);
}, false);
};
return ui;
})({});

10245
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,25 +1,59 @@
{
"name": "codex.editor",
"version": "1.7.8",
"version": "2.0.0",
"description": "Codex Editor. Native JS, based on API and Open Source",
"main": "index.js",
"scripts": {
"build": "webpack"
"build": "rimraf dist && npm run svg && npm run build:dev",
"svg": "svg-sprite-generate -d src/assets/ -o build/sprite.svg",
"build:dev": "webpack --mode development --progress --display-error-details --display-entrypoints"
},
"author": "Codex Team",
"license": "ISC",
"repository": {
"type": "git",
"url": "git+https://github.com/codex-team/codex.editor.git"
},
"devDependencies": {
"babel-core": "^6.21.0",
"babel-loader": "^6.2.10",
"babel-polyfill": "^6.20.0",
"babel-preset-es2015": "^6.22.0",
"babel-runtime": "^6.20.0",
"css-loader": "^0.26.1",
"eslint": "^3.12.2",
"eslint-loader": "^1.6.1",
"extract-text-webpack-plugin": "^1.0.1",
"babel-core": "^6.26.3",
"babel-loader": "^7.1.4",
"babel-plugin-add-module-exports": "^0.2.1",
"babel-plugin-class-display-name": "^2.1.0",
"babel-polyfill": "^6.26.0",
"babel-preset-env": "^1.7.0",
"babel-preset-stage-0": "^6.24.1",
"babel-runtime": "^6.26.0",
"css-loader": "^0.28.11",
"eslint": "^4.19.1",
"eslint-loader": "^2.0.0",
"extract-text-webpack-plugin": "^3.0.2",
"html-janitor": "^2.0.2",
"path": "^0.12.7",
"webpack": "^1.14.0"
}
"postcss-apply": "^0.10.0",
"postcss-color-hex-alpha": "^3.0.0",
"postcss-color-mod-function": "^2.4.2",
"postcss-cssnext": "^3.1.0",
"postcss-custom-media": "^6.0.0",
"postcss-custom-properties": "^7.0.0",
"postcss-custom-selectors": "^4.0.1",
"postcss-font-family-system-ui": "^3.0.0",
"postcss-font-variant": "^3.0.0",
"postcss-loader": "^2.1.5",
"postcss-media-minmax": "^3.0.0",
"postcss-nested": "^3.0.0",
"postcss-nested-ancestors": "^2.0.0",
"postcss-nesting": "^6.0.0",
"postcss-smart-import": "^0.7.6",
"raw-loader": "^0.5.1",
"rimraf": "^2.6.2",
"stylelint": "^9.3.0",
"svg-sprite-generator": "0.0.7",
"ts-loader": "^4.4.1",
"tslint": "^5.10.0",
"tslint-loader": "^3.6.0",
"typescript": "^2.9.1",
"webpack": "^4.12.0",
"webpack-cli": "^3.0.3"
},
"dependencies": {}
}

View file

@ -1,130 +0,0 @@
.cdx-attaches__default-wrapper {
margin: 15px 0;
padding: 15px;
background: #fff;
border: 1px solid #ebecec;
box-shadow: 0 1px 2px 0 rgba(34, 36, 44, 0.03);
border-radius: 3px;
text-align: center;
}
.cdx-attaches__default-button {
color: #8990aa;
cursor: pointer;
}
.cdx-attaches__default-button:hover {
color: #393f52;
}
.cdx-attaches__wrapper {
display: -ms-flexbox;
display: flex;
-ms-flex-flow: row nowrap;
flex-flow: row nowrap;
-ms-flex-pack: start;
justify-content: flex-start;
-ms-flex-align: center;
align-items: center;
margin: 10px 0;
padding: 15px 20px;
background: #fff;
border: 1px solid #ebecec;
box-shadow: 0 1px 2px 0 rgba(34, 36, 44, 0.03);
border-radius: 3px;
font-size: 15px;
}
.cdx-attaches__file-name {
-ms-flex-positive: 8;
flex-grow: 8;
width: 100%;
outline: none;
border: 0;
font-size: inherit;
}
.cdx-attaches__file-name--collapsed {
width: 30%;
overflow: hidden;
text-overflow: ellipsis;
}
.cdx-attaches__extension,
.cdx-attaches__size {
color: #8f9298;
white-space: nowrap;
}
.cdx-attaches__extension::after {
content: ',';
margin-right: 0.2em;
}
.cdx-attaches__size::after {
content: 'KB';
margin-left: 0.2em;
}
.cdx-attaches__icon {
display: inline-block;
width: 16px;
height: 32px;
background: url(file-icon-black.svg) no-repeat center center;
background-size: contain;
}
li:hover .cdx-attaches__icon,
.selected .cdx-attaches__icon {
background: url(file-icon-white.svg) no-repeat center center;
background-size: contain;
}
.cdx-attaches__icon--inline {
height: 16px;
vertical-align: text-bottom;
}
.cdx-attaches__loader {
background-color: transparent;
background-image: repeating-linear-gradient(-45deg, transparent, transparent 4px, #f5f9ff 4px, #eaedef 8px) !important;
background-size: 56px 56px;
animation: loading-bar 5s infinite linear;
}
@keyframes loading-bar {
100% { background-position: -56% 0; }
}
.cdx-attaches__progress-bar {
width: 100%;
height: 3px;
margin: 0 15px;
background: #e1e3eb;
border: 0;
border-radius: 5px;
}
.cdx-attaches__progress-bar::-webkit-progress-bar {
background: #e0e1e3;
border-radius: 5px;
}
.cdx-attaches__progress-bar::-webkit-progress-value {
background: #414957;
border-radius: 5px;
transition: all 100ms ease-in;
}
progress::-moz-progress-bar {
background: #414957;
border-radius: 5px;
}
.cdx-attaches__cross-button {
width: 18px;
height: 18px;
background: url(cross.svg) no-repeat center center;
background-size: contain;
cursor: pointer;
}

View file

@ -1,430 +0,0 @@
/**
* Attache-file Plugin for CodeX Editor
*
* @param {String} config.fetchUrl - Route for file uploding
* @param {Nubmer} config.maxSize - Maximum allowed file size in KB
* @param {String} config.accept - Accepted MIME-types. By default, accepts all
*
* Backend should return response with
* 'url' - Full path to the uploaded file
* 'title' - File title,
* 'name' - File name without extension,
* 'extension' - File extension,
* 'size' - File size
*
* @author @gohabereg
* @version 1.0.0
*/
var cdxAttaches = function () {
/**
* Private methods and props
*/
var KBYTE = 1024,
fileWrapper = null;
/**
* Default config
* Can be redefined with prepare method
*
* @var sting config.fetchUrl -- url to your fetch script
* @var int config.maxSize -- max size of file in kilobytes
* @var accept config.accept -- valid MIME-types. By default, accepts all
*
*/
var config = {
fetchUrl: '',
maxSize: 2,
accept: ''
};
var elementsClasses = {
defaultFormWrapper : 'cdx-attaches__default-wrapper',
defaultFormButton : 'cdx-attaches__default-button',
progressBar : 'cdx-attaches__progress-bar',
wrapper : 'cdx-attaches__wrapper',
loader : 'cdx-attaches__loader',
crossButton : 'cdx-attaches__cross-button',
file: {
title : 'cdx-attaches__file-name',
collapsedName : 'cdx-attaches__file-name--collapsed',
extension : 'cdx-attaches__extension',
size : 'cdx-attaches__size'
}
};
var ui = {
defaultForm: function () {
var wrapper = codex.editor.draw.node('div', elementsClasses.defaultFormWrapper),
button = codex.editor.draw.node('div', elementsClasses.defaultFormButton);
button.addEventListener('click', upload.fire);
button.innerHTML = '<i class="cdx-attaches__icon cdx-attaches__icon--inline"></i> Загрузить файл';
wrapper.appendChild(button);
return wrapper;
},
uploadedFile: function (data) {
var wrapper = codex.editor.draw.node('div', elementsClasses.wrapper),
name = codex.editor.draw.node('input', elementsClasses.file.title),
extension = codex.editor.draw.node('span', elementsClasses.file.extension),
size = codex.editor.draw.node('span', elementsClasses.file.size);
wrapper.dataset.url = data.url;
wrapper.dataset.name = data.name;
name.value = data.title || '';
extension.textContent = data.extension.toUpperCase();
size.textContent = data.size;
wrapper.appendChild(name);
wrapper.appendChild(extension);
wrapper.appendChild(size);
return wrapper;
},
progressBar: {
bar: null,
draw: function () {
var wrapper = codex.editor.draw.node('div', elementsClasses.wrapper),
progress = codex.editor.draw.node('progress', elementsClasses.progressBar),
name = codex.editor.draw.node('span', elementsClasses.file.title),
crossButton = codex.editor.draw.node('span', elementsClasses.crossButton);
progress.max = 100;
progress.value = 0;
name.textContent = codex.editor.transport.input.files[0].name;
name.classList.add(elementsClasses.file.collapsedName);
crossButton.addEventListener('click', upload.abort);
ui.progressBar.bar = progress;
wrapper.appendChild(name);
wrapper.appendChild(progress);
wrapper.appendChild(crossButton);
return wrapper;
},
change: function (value) {
console.assert( !isNaN(value), 'CodeX Editor Attaches: passed value is not a Number');
ui.progressBar.bar.value = value;
}
}
};
/**
* Notify about upload errors via codex.editor.notifications
*
* @param Object error can have `message` property with error message
*/
var notifyError = function (error) {
error = error || {};
codex.editor.notifications.notification({
type: 'error',
message: 'Ошибка во время загрузки файла' + ( error.message ? ': ' + error.message : '' )
});
};
/**
* Contains validation methods
*
* TODO: MIME-type validation
*
*/
var validation = {
size: function () {
var file = codex.editor.transport.input.files[0];
return Math.ceil(file.size / KBYTE) <= config.maxSize;
},
};
var upload = {
current: null,
aborted: false,
/**
* Fired codex.editor.transport selectAndUpload methods
*/
fire: function () {
codex.editor.transport.selectAndUpload({
url: config.fetchUrl,
success: upload.success,
beforeSend: upload.start,
progress: upload.progress,
error: upload.error,
accept: config.accept
});
},
/**
* Will be called before upload
* Draws load animation and progress bar
*/
start: function () {
if (!validation.size()) {
notifyError({message: 'Файл слишком большой'});
return false;
}
if (upload.current) {
notifyError({message: 'Дождитесь окончания предыдущей загрузки'});
return;
}
var progress = ui.progressBar.draw();
upload.current = progress;
codex.editor.content.switchBlock(fileWrapper, progress, 'attaches');
},
/**
* Handler for XmlHttpRequest.upload.onprogress event
* Changes progress bar status
*
* @param event
*/
progress: function (event) {
/** Prevents isNaN value assignment */
if (!event.total) {
return;
}
var value = parseInt(event.loaded / event.total * 100);
ui.progressBar.change(value);
},
/**
* Will be called after success upload
* Try to decode JSON response and draws ui or fires error handler
*
* @param response
*/
success: function (response) {
var data,
uploadedFile;
try {
response = JSON.parse(response);
if (response.success) {
data = response.data;
data.size = Math.ceil(data.size / KBYTE) || 1;
uploadedFile = ui.uploadedFile(data);
codex.editor.content.switchBlock(upload.current, uploadedFile, 'attaches');
uploadedFile.querySelector('input').focus();
} else {
upload.error(response);
}
} catch (e) {
upload.error();
}
upload.current = null;
},
/**
* Upload errors handler
*
* @param error
*/
error: function (error) {
var defaultFrom = ui.defaultForm();
codex.editor.content.switchBlock(upload.current, defaultFrom, 'attaches');
if (!upload.aborted) {
notifyError(error);
}
upload.aborted = false;
upload.current = null;
},
abort: function () {
codex.editor.transport.abort();
upload.aborted = true;
upload.current = null;
}
};
/*
* Public methods
* @param {String} _config.fetchUrl Required
*/
var prepare = function (_config) {
return new Promise(function(resolve, reject){
if ( !_config.fetchUrl ){
reject(Error('fetchUrl is missed'));
return;
}
config.fetchUrl = _config.fetchUrl;
config.accept = _config.accept || config.accept;
if ( !isNaN(_config.maxSize)){
config.maxSize = _config.maxSize;
}
resolve();
});
};
var render = function (data) {
if (!data) {
fileWrapper = ui.defaultForm();
return fileWrapper;
}
return ui.uploadedFile(data);
};
var save = function (block) {
var data = {
url: block.dataset.url,
name: block.dataset.name,
title: block.querySelector('.' + elementsClasses.file.title).value,
extension: block.querySelector('.' + elementsClasses.file.extension).textContent,
size: block.querySelector('.' + elementsClasses.file.size).textContent,
};
return data;
};
var validate = function (data) {
if (!data.url || !data.url.trim()) {
return false;
}
if (!data.title || !data.title.trim()) {
return false;
}
if (!data.extension || !data.extension.trim()) {
return false;
}
if (!data.size || !data.size.trim()) {
return false;
}
return true;
};
var destroy = function () {
cdxAttaches = null;
};
var appendCallback = function () {
upload.fire();
};
return {
prepare: prepare,
render: render,
save: save,
validate: validate,
destroy: destroy,
appendCallback: appendCallback
};
}();

View file

@ -1,7 +0,0 @@
<svg width="13px" height="13px" viewBox="0 0 13 13" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<g id="Symbols" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="icon/cross" transform="translate(-5.000000, -5.000000)" fill="#575D67">
<path d="M10.4350288,10.7279221 L4.4359005,10.7279221 C3.8796597,10.7279221 3.43502884,11.1756373 3.43502884,11.7279221 C3.43502884,12.2840572 3.88313435,12.7279221 4.4359005,12.7279221 L10.4350288,12.7279221 L10.4350288,18.7270504 C10.4350288,19.2832912 10.8827441,19.7279221 11.4350288,19.7279221 C11.991164,19.7279221 12.4350288,19.2798166 12.4350288,18.7270504 L12.4350288,12.7279221 L18.4341572,12.7279221 C18.990398,12.7279221 19.4350288,12.2802068 19.4350288,11.7279221 C19.4350288,11.1717869 18.9869233,10.7279221 18.4341572,10.7279221 L12.4350288,10.7279221 L12.4350288,4.72879372 C12.4350288,4.17255292 11.9873136,3.72792206 11.4350288,3.72792206 C10.8788937,3.72792206 10.4350288,4.17602757 10.4350288,4.72879372 L10.4350288,10.7279221 Z" transform="translate(11.435029, 11.727922) rotate(45.000000) translate(-11.435029, -11.727922) "></path>
</g>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View file

@ -1 +0,0 @@
<svg enable-background="new 0 0 48 48" height="48px" id="Layer_1" version="1.1" viewBox="0 0 48 48" width="48px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path clip-rule="evenodd" d="M37,47H11c-2.209,0-4-1.791-4-4V5c0-2.209,1.791-4,4-4h18.973 c0.002,0,0.005,0,0.007,0h0.02H30c0.32,0,0.593,0.161,0.776,0.395l9.829,9.829C40.84,11.407,41,11.68,41,12l0,0v0.021 c0,0.002,0,0.003,0,0.005V43C41,45.209,39.209,47,37,47z M31,4.381V11h6.619L31,4.381z M39,13h-9c-0.553,0-1-0.448-1-1V3H11 C9.896,3,9,3.896,9,5v38c0,1.104,0.896,2,2,2h26c1.104,0,2-0.896,2-2V13z M33,39H15c-0.553,0-1-0.447-1-1c0-0.552,0.447-1,1-1h18 c0.553,0,1,0.448,1,1C34,38.553,33.553,39,33,39z M33,31H15c-0.553,0-1-0.447-1-1c0-0.552,0.447-1,1-1h18c0.553,0,1,0.448,1,1 C34,30.553,33.553,31,33,31z M33,23H15c-0.553,0-1-0.447-1-1c0-0.552,0.447-1,1-1h18c0.553,0,1,0.448,1,1C34,22.553,33.553,23,33,23 z" fill-rule="evenodd" fill="#000"/></svg>

Before

Width:  |  Height:  |  Size: 958 B

View file

@ -1 +0,0 @@
<svg enable-background="new 0 0 48 48" height="48px" id="Layer_1" version="1.1" viewBox="0 0 48 48" width="48px" xml:space="preserve" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><path clip-rule="evenodd" d="M37,47H11c-2.209,0-4-1.791-4-4V5c0-2.209,1.791-4,4-4h18.973 c0.002,0,0.005,0,0.007,0h0.02H30c0.32,0,0.593,0.161,0.776,0.395l9.829,9.829C40.84,11.407,41,11.68,41,12l0,0v0.021 c0,0.002,0,0.003,0,0.005V43C41,45.209,39.209,47,37,47z M31,4.381V11h6.619L31,4.381z M39,13h-9c-0.553,0-1-0.448-1-1V3H11 C9.896,3,9,3.896,9,5v38c0,1.104,0.896,2,2,2h26c1.104,0,2-0.896,2-2V13z M33,39H15c-0.553,0-1-0.447-1-1c0-0.552,0.447-1,1-1h18 c0.553,0,1,0.448,1,1C34,38.553,33.553,39,33,39z M33,31H15c-0.553,0-1-0.447-1-1c0-0.552,0.447-1,1-1h18c0.553,0,1,0.448,1,1 C34,30.553,33.553,31,33,31z M33,23H15c-0.553,0-1-0.447-1-1c0-0.552,0.447-1,1-1h18c0.553,0,1,0.448,1,1C34,22.553,33.553,23,33,23 z" fill-rule="evenodd" fill="#fff"/></svg>

Before

Width:  |  Height:  |  Size: 958 B

View file

@ -1,82 +0,0 @@
/**
* Code Plugin\
* Creates code tag and adds content to this tag
*/
var code = (function(code_plugin) {
var baseClass = "ce-code";
/**
* Make initial header block
* @param {object} JSON with block data
* @return {Element} element to append
*/
var make_ = function (data) {
var tag = codex.editor.draw.node('TEXTAREA', [baseClass], {});
if (data && data.text) {
tag.value = data.text;
}
return tag;
};
/**
* Escapes HTML chars
*
* @param {string} input
* @return {string} escaped string
*/
var escapeHTML_ = function (input) {
var div = document.createElement('DIV'),
text = document.createTextNode(input);
div.appendChild(text);
return div.innerHTML;
};
/**
* Method to render HTML block from JSON
*/
code_plugin.render = function (data) {
return make_(data);
};
/**
* Method to extract JSON data from HTML block
*/
code_plugin.save = function (blockContent) {
var escaped = escapeHTML_(blockContent.value),
data = {
text : escaped
};
return data;
};
code_plugin.validate = function (data) {
if (data.text.trim() == '')
return;
return true;
};
code_plugin.destroy = function () {
code = null;
};
return code_plugin;
})({});

View file

@ -1,211 +0,0 @@
/**
* Embed plugin by gohabereg
* @version 1.0.0
*/
var embed = function(embed_plugin){
var methods = {
addInternal: function (content) {
codex.editor.content.switchBlock(codex.editor.content.currentNode, content);
var blockContent = codex.editor.content.currentNode.childNodes[0];
blockContent.classList.add('embed__loader');
setTimeout(function(){
blockContent.classList.remove('embed__loader');
}, 1000);
},
getHtmlWithEmbedId: function (type, id) {
return services[type].html.replace(/<\%\= remote\_id \%\>/g, id);
},
makeElementFromHtml: function(html) {
var wrapper = document.createElement('DIV');
wrapper.innerHTML = html;
return wrapper;
},
getRemoteId: function(source, execArray) {
switch(source) {
case 'yandex-music-track':
id = execArray[2]+'/'+execArray[1];
break;
case 'yandex-music-playlist':
id = execArray[1]+'/'+execArray[2];
break;
default:
id = execArray[1];
}
return id;
}
};
var services = {
youtube: {
regex: /^.*(?:(?:youtu\.be\/)|(?:youtube\.com)\/|v\/|u\/\w\/|embed\/|watch\?v=|\&v=)([^#\&\?]*)(?:[\?\&]t\=(\d*)|)/,
html: "<iframe src=\"https://www.youtube.com/embed/<%= remote_id %>\" style=\"width:100%;\" height=\"320\" frameborder=\"0\" allowfullscreen></iframe>",
height: 320,
width: 580
}
};
embed_plugin.make = function(data, isInternal) {
if (!data.remote_id)
return;
var html = methods.getHtmlWithEmbedId(data.source, data.remote_id),
block = methods.makeElementFromHtml(html);
block.dataset.remoteId = data.remote_id;
block.dataset.source = data.source;
block.dataset.thumbnailUrl = data.thumbnailUrl;
block.classList.add('embed');
// var sidePadding = (600 - services[data.source].width) / 2 + 'px';
// block.style.padding = '30px ' + sidePadding;
if (isInternal) {
methods.addInternal(block);
}
return block;
};
/**
* Saving JSON output.
* Upload data via ajax
*/
embed_plugin.save = function(blockContent) {
if (!blockContent)
return;
var data,
source = blockContent.dataset.source;
data = {
source: source,
remote_id: blockContent.dataset.remoteId,
thumbnailUrl: blockContent.dataset.thumbnailUrl,
height: services[source].height,
width: services[source].width
};
return data;
};
/**
* Render data
*/
embed_plugin.render = function(data) {
return embed_plugin.make(data);
};
embed_plugin.urlPastedCallback = function(url, pattern) {
var execArray = pattern.regex.exec(url),
id = methods.getRemoteId(pattern.type, execArray);
var data = {
source: pattern.type,
remote_id: id,
thumbnailUrl: url
};
embed_plugin.make(data, true);
};
embed_plugin.validate = function(savedData) {
var source = savedData.source,
execArray = services[source].regex.exec(savedData.thumbnailUrl),
remoteId = methods.getRemoteId(source, execArray);
return remoteId == savedData.remote_id;
};
embed_plugin.pastePatterns = [
{
type: 'vk',
regex: /https?:\/\/vk\.com\/.*(?:video)([-0-9]+_[0-9]+)/, ///https?.+vk?.com\/feed\?w=wall\d+_\d+/,
callback: embed_plugin.urlPastedCallback
},
{
type: 'youtube',
regex: /(?:https?:\/{2})?(?:w{3}\.)?youtu(?:be)?\.(?:com|be)(?:\/watch\?v=|\/)([^\s&]+)/,
callback: embed_plugin.urlPastedCallback
},
{
type: 'vimeo',
regex: /(?:http[s]?:\/\/)?(?:www.)?vimeo\.co(?:.+\/([^\/]\d+)(?:#t=[\d]+)?s?$)/,
callback: embed_plugin.urlPastedCallback
},
{
type: 'coub',
regex: /https?:\/\/coub\.com\/view\/([^\/\?\&]+)/,
callback: embed_plugin.urlPastedCallback
},
{
type: 'vine',
regex: /https?:\/\/vine\.co\/v\/([^\/\?\&]+)/,
callback: embed_plugin.urlPastedCallback
},
{
type: 'imgur',
regex: /https?:\/\/(?:i\.)?imgur\.com.*\/([a-zA-Z0-9]+)(?:\.gifv)?/,
callback: embed_plugin.urlPastedCallback
},
{
type: 'gfycat',
regex: /https?:\/\/gfycat\.com(?:\/detail)?\/([a-zA-Z]+)/,
callback: embed_plugin.urlPastedCallback
},
{
type: 'twitch-channel',
regex: /https?:\/\/www.twitch.tv\/([^\/\?\&]*)/,
callback: embed_plugin.urlPastedCallback
},
{
type: 'twitch-video',
regex: /https?:\/\/www.twitch.tv\/(?:[^\/\?\&]*\/v|videos)\/([0-9]*)/,
callback: embed_plugin.urlPastedCallback
},
{
type: 'yandex-music-album',
regex: /https?:\/\/music.yandex.ru\/album\/([0-9]*)/,
callback: embed_plugin.urlPastedCallback
},
{
type: 'yandex-music-track',
regex: /https?:\/\/music.yandex.ru\/album\/([0-9]*)\/track\/([0-9]*)/,
callback: embed_plugin.urlPastedCallback
},
{
type: 'yandex-music-playlist',
regex: /https?:\/\/music.yandex.ru\/users\/([^\/\?\&]*)\/playlists\/([0-9]*)/,
callback: embed_plugin.urlPastedCallback
} ];
embed_plugin.destroy = function () {
embed = null;
};
return embed_plugin;
}({});

View file

@ -1,41 +0,0 @@
/**
* Plugin styles
*/
.ce-header {
padding: .7em 0;
margin: 0;
line-height: 1.4em;
}
.ce-header p,
.ce-header div{
padding: 0 !important;
margin: 0 !important;
}
/** H e a d e r - settings */
.ce_plugin_header--select_button{
display: block;
color: #306ac7;
cursor: pointer;
line-height: 1.3em;
}
.ce_plugin_header--select_button:not(:last-of-type){
margin-bottom: 1.5em;
}
.ce_plugin_header--select_button:hover{
color: #a1b4ec;
}
/**
* Empty header placeholder
*/
.ce-header:empty::before{
content : attr(data-placeholder);
color: #818BA1;
opacity: .7;
transition: opacity 200ms ease;
}
.ce-header:focus::before{
opacity: .1;
}

View file

@ -1,169 +0,0 @@
/**
* Example of making plugin
* H e a d e r
*/
var header = (function(header_plugin) {
/**
* @private
*/
var methods_ = {
/**
* Binds click event to passed button
*/
addSelectTypeClickListener : function (el, type) {
el.addEventListener('click', function () {
methods_.selectTypeClicked(type);
}, false);
},
/**
* Replaces old header with new type
* @params {string} type - new header tagName: H1H6
*/
selectTypeClicked : function (type) {
var old_header, new_header;
/** Now current header stored as a currentNode */
old_header = codex.editor.content.currentNode.querySelector('[contentEditable]');
/** Making new header */
new_header = codex.editor.draw.node(type, ['ce-header'], { innerHTML : old_header.innerHTML });
new_header.contentEditable = true;
new_header.setAttribute('data-placeholder', 'Заголовок');
new_header.dataset.headerData = type;
codex.editor.content.switchBlock(old_header, new_header, 'header');
/** Close settings after replacing */
codex.editor.toolbar.settings.close();
}
};
/**
* @private
*
* Make initial header block
* @param {object} JSON with block data
* @return {Element} element to append
*/
var make_ = function (data) {
var availableTypes = ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'],
tag,
headerType = 'h2';
if ( data && data['heading-styles'] && availableTypes.includes(data['heading-styles']) ) {
headerType = data['heading-styles'];
}
tag = document.createElement(headerType);
/**
* Save header type in data-attr.
* We need it in save method to extract type from HTML to JSON
*/
tag.dataset.headerData = headerType;
if (data && data.text) {
tag.textContent = data.text;
}
if (!tag.dataset.headerData) {
tag.dataset.headerData = 'h2';
}
tag.classList.add('ce-header');
tag.setAttribute('data-placeholder', 'Заголовок');
tag.contentEditable = true;
return tag;
};
header_plugin.prepareDataForSave = function(data) {
};
/**
* Method to render HTML block from JSON
*/
header_plugin.render = function (data) {
return make_(data);
};
/**
* Method to extract JSON data from HTML block
*/
header_plugin.save = function (blockContent) {
var data = {
"heading-styles": blockContent.dataset.headerData,
"format": "html",
"text": blockContent.textContent || ''
};
return data;
};
/**
* Settings panel content
* - - - - - - - - - - - - -
* | настройки H1 H2 H3 |
* - - - - - - - - - - - - -
* @return {Element} element contains all settings
*/
header_plugin.makeSettings = function () {
var holder = codex.editor.draw.node('DIV', ['cdx-plugin-settings--horisontal'], {} ),
types = {
h2: 'H2',
h3: 'H3',
h4: 'H4'
},
selectTypeButton;
/** Now add type selectors */
for (var type in types){
selectTypeButton = codex.editor.draw.node('SPAN', ['cdx-plugin-settings__item'], { textContent : types[type] });
methods_.addSelectTypeClickListener(selectTypeButton, type);
holder.appendChild(selectTypeButton);
}
return holder;
};
header_plugin.validate = function(data) {
if (data.text.trim() === '' || data['heading-styles'].trim() === ''){
return false;
}
return true;
};
header_plugin.destroy = function () {
header = null;
}
return header_plugin;
})({});

View file

@ -1,701 +0,0 @@
/**
* Image plugin for codex-editor
* @author CodeX Team <team@ifmo.su>
*
* @version 1.3.0
*/
var image = (function(image_plugin) {
/**
* @private
*
* CSS classNames
*/
var elementClasses_ = {
ce_image : 'ce-image',
loading : 'ce-plugin-image__loader',
blockStretched: 'ce-block--stretched',
uploadedImage : {
centered : 'ce-plugin-image__uploaded--centered',
stretched : 'ce-plugin-image__uploaded--stretched'
},
imageCaption : 'ce-plugin-image__caption',
imageWrapper : 'ce-plugin-image__wrapper',
formHolder : 'ce-plugin-image__holder',
uploadButton : 'ce-plugin-image__button',
imagePreview : 'ce-image__preview',
selectorHolder: 'ce-settings-checkbox',
selectorButton: 'ce-settings-checkbox__toggler',
settingsItem: 'ce-image-settings__item',
imageWrapperBordered : 'ce-image__wrapper--bordered',
toggled : 'ce-image-settings__item--toggled'
};
/**
*
* @private
*
* UI methods
*/
var ui_ = {
holder : function(){
var element = document.createElement('DIV');
element.classList.add(elementClasses_.formHolder);
element.classList.add(elementClasses_.ce_image);
return element;
},
uploadButton : function(){
var button = document.createElement('SPAN');
button.classList.add(elementClasses_.uploadButton);
button.innerHTML = '<i class="ce-icon-picture"> </i>';
button.innerHTML += 'Загрузить фотографию';
return button;
},
/**
* @param {object} file - file path
* @param {string} style - css class
* @return {object} image - document IMG tag
*/
image : function(file, styles) {
var image = document.createElement('IMG');
styles.map(function(item) {
image.classList.add(item);
});
image.src = file.url;
image.dataset.bigUrl = file.bigUrl;
return image;
},
wrapper : function() {
var div = document.createElement('div');
div.classList.add(elementClasses_.imageWrapper);
return div;
},
caption : function() {
var div = document.createElement('div');
div.classList.add(elementClasses_.imageCaption);
div.contentEditable = true;
return div;
},
/**
* Draws form for image upload
*/
makeForm : function() {
var holder = ui_.holder(),
uploadButton = ui_.uploadButton();
holder.appendChild(uploadButton);
uploadButton.addEventListener('click', uploadButtonClicked_, false );
image.holder = holder;
return holder;
},
/**
* wraps image and caption
* @param {object} data - image information
* @param {string} imageTypeClass - plugin's style
* @param {boolean} stretched - stretched or not
* @return wrapped block with image and caption
*/
makeImage : function(data, imageTypeClasses, stretched, bordered) {
var file = data,
text = data.caption,
type = data.type,
image = ui_.image(file, imageTypeClasses),
caption = ui_.caption(),
wrapper = ui_.wrapper();
caption.innerHTML = text || '';
wrapper.dataset.stretched = stretched;
wrapper.dataset.bordered = bordered;
/** Appeding to the wrapper */
wrapper.appendChild(image);
wrapper.appendChild(caption);
return wrapper;
},
/**
* @param {HTML} data - Rendered block with image
*/
getImage : function(data) {
var image = data.querySelector('.' + elementClasses_.uploadedImage.centered) ||
data.querySelector('.' + elementClasses_.uploadedImage.stretched);
return image;
},
/**
* wraps image and caption
* @deprecated
* @param {object} data - image information
* @return wrapped block with image and caption
*/
centeredImage : function(data) {
var file = data.file,
text = data.caption,
type = data.type,
image = ui_.image(file, elementClasses_.uploadedImage.centered),
caption = ui_.caption(),
wrapper = ui_.wrapper();
caption.textContent = text;
wrapper.dataset.stretched = 'false';
/** Appeding to the wrapper */
wrapper.appendChild(image);
wrapper.appendChild(caption);
return wrapper;
},
/**
* wraps image and caption
* @deprecated
* @param {object} data - image information
* @return stretched image
*/
stretchedImage : function(data) {
var file = data.file,
text = data.caption,
type = data.type,
image = ui_.image(file, elementClasses_.uploadedImage.stretched),
caption = ui_.caption(),
wrapper = ui_.wrapper();
caption.textContent = text;
wrapper.dataset.stretched = 'true';
/** Appeding to the wrapper */
wrapper.appendChild(image);
wrapper.appendChild(caption);
return wrapper;
}
};
/**
* @private
*
* After render callback
*/
var uploadButtonClicked_ = function(event) {
var url = image_plugin.config.uploadImage,
beforeSend = uploadingCallbacks_.ByClick.beforeSend,
success = uploadingCallbacks_.ByClick.success,
error = uploadingCallbacks_.ByClick.error;
/** Define callbacks */
codex.editor.transport.selectAndUpload({
url : url,
multiple : false,
accept : 'image/*',
beforeSend : beforeSend,
success : success,
error : error
});
};
var methods_ = {
addSelectTypeClickListener : function(el, type) {
el.addEventListener('click', function() {
// el - settings element
switch (type) {
case 'bordered':
methods_.toggleBordered(type, this); break;
case 'stretched':
methods_.toggleStretched(type, this); break;
}
}, false);
},
toggleBordered : function(type, clickedSettingsItem ) {
var current = codex.editor.content.currentNode,
blockContent = current.childNodes[0],
img = ui_.getImage(current),
wrapper = current.querySelector('.' + elementClasses_.imageWrapper);
if (!img) {
return;
}
/**
* Add classes to the IMG tag and to the Settings element
*/
img.classList.toggle(elementClasses_.imageWrapperBordered);
clickedSettingsItem.classList.toggle(elementClasses_.toggled);
/**
* Save settings in dataset
*/
wrapper.dataset.bordered = img.classList.contains(elementClasses_.imageWrapperBordered);
setTimeout(function() {
codex.editor.toolbar.settings.close();
}, 200);
},
toggleStretched : function( type, clickedSettingsItem ) {
var current = codex.editor.content.currentNode,
blockContent = current.childNodes[0],
img = ui_.getImage(current),
wrapper = current.querySelector('.' + elementClasses_.imageWrapper);
if (!img) {
return;
}
/** Clear classList */
blockContent.classList.add(elementClasses_.blockStretched);
img.classList.toggle(elementClasses_.uploadedImage.stretched);
img.classList.toggle(elementClasses_.uploadedImage.centered);
clickedSettingsItem.classList.toggle(elementClasses_.toggled);
wrapper.dataset.stretched = img.classList.contains(elementClasses_.uploadedImage.stretched);
setTimeout(function() {
codex.editor.toolbar.settings.close();
}, 1000);
}
};
/**
* @private
* Callbacks
*/
var uploadingCallbacks_ = {
ByClick : {
/**
* Before sending ajax request
*/
beforeSend : function() {
var input = codex.editor.transport.input,
files = input.files;
var validFileExtensions = ["jpg", "jpeg", "bmp", "gif", "png"];
var type = files[0].type.split('/');
var result = validFileExtensions.some(function(ext) {
return ext == type[1];
});
if (!result) {
return;
}
var reader = new FileReader();
reader.readAsDataURL(files[0]);
reader.onload = function(e) {
var data = {
background : false,
border : false,
isstretch : false,
url : e.target.result,
bigUrl : null,
width : null,
height : null,
additionalData : null,
caption : '',
cover : null
};
var newImage = make_(data);
codex.editor.content.switchBlock(image.holder, newImage, 'image');
newImage.classList.add(elementClasses_.imagePreview);
/**
* Change holder to image
*/
image.holder = newImage;
};
},
/** Photo was uploaded successfully */
success : function(result) {
var parsed = JSON.parse(result),
data,
currentBlock = codex.editor.content.currentNode;
/**
* Preparing {Object} data to draw an image
* @uses ceImage.make method
*/
data = parsed.data;
image.holder.classList.remove(elementClasses_.imagePreview);
/**
* Change src of image
*/
var newImage = image.holder.getElementsByTagName('IMG')[0];
newImage.src = parsed.data.url;
newImage.dataset.bigUrl = parsed.data.bigUrl;
newImage.dataset.width = parsed.data.width;
newImage.dataset.height = parsed.data.height;
newImage.dataset.additionalData = parsed.data.additionalData;
},
/** Error callback. Sends notification to user that something happend or plugin doesn't supports method */
error : function(result) {
var oldHolder = image.holder;
var form = ui_.makeForm();
codex.editor.content.switchBlock(oldHolder, form, 'image');
}
},
ByPaste : {
/**
* Direct upload
* Any URL that contains image extension
* @param url
*/
uploadImageFromUrl : function(path) {
var image,
current = codex.editor.content.currentNode,
beforeSend,
success_callback;
/** When image is uploaded to redactors folder */
success_callback = function(data) {
var imageInfo = JSON.parse(data);
var newImage = image.getElementsByTagName('IMG')[0];
newImage.dataset.stretched = false;
newImage.dataset.src = imageInfo.url;
newImage.dataset.bigUrl = imageInfo.bigUrl;
newImage.dataset.width = imageInfo.width;
newImage.dataset.height = imageInfo.height;
newImage.dataset.additionalData = imageInfo.additionalData;
image.classList.remove(elementClasses_.imagePreview);
};
/** Before sending XMLHTTP request */
beforeSend = function() {
var content = current.querySelector('.ce-block__content');
var data = {
background: false,
border: false,
isStretch: false,
file: {
url: path,
bigUrl: null,
width: null,
height: null,
additionalData: null
},
caption: '',
cover: null
};
image = codex.editor.tools.image_extended.render(data);
image.classList.add(elementClasses_.imagePreview);
var img = image.querySelector('img');
codex.editor.content.switchBlock(codex.editor.content.currentNode, image, 'image');
};
/** Preparing data for XMLHTTP */
var data = {
url: image_plugin.config.uploadFromUrl,
type: "POST",
data : {
url: path
},
beforeSend : beforeSend,
success : success_callback
};
codex.editor.core.ajax(data);
}
}
};
/**
* Image path
* @type {null}
*/
image_plugin.path = null;
/**
* Plugin configuration
*/
image_plugin.config = null;
/**
*
* @private
*
* @param data
* @return {*}
*
*/
var make_ = function ( data ) {
var holder,
classes = [];
if (data) {
if (data.border) {
classes.push(elementClasses_.imageWrapperBordered);
}
if ( data.isstretch || data.isstretch === 'true') {
classes.push(elementClasses_.uploadedImage.stretched);
holder = ui_.makeImage(data, classes, 'true', data.border);
} else {
classes.push(elementClasses_.uploadedImage.centered);
holder = ui_.makeImage(data, classes, 'false', data.border);
}
return holder;
} else {
holder = ui_.makeForm();
return holder;
}
};
/**
* @private
*
* Prepare clear data before save
*
* @param data
*/
var prepareDataForSave_ = function(data) {
};
/**
* @public
* @param config
*/
image_plugin.prepare = function(config) {
image_plugin.config = config;
return Promise.resolve();
};
/**
* @public
*
* this tool works when tool is clicked in toolbox
*/
image_plugin.appendCallback = function(event) {
/** Upload image and call success callback*/
uploadButtonClicked_(event);
};
/**
* @public
*
* @param data
* @return {*}
*/
image_plugin.render = function( data ) {
return make_(data);
};
/**
* @public
*
* @param block
* @return {{background: boolean, border: boolean, isstretch: boolean, file: {url: (*|string|Object), bigUrl: (null|*), width: *, height: *, additionalData: null}, caption: (string|*|string), cover: null}}
*/
image_plugin.save = function ( block ) {
var content = block,
image = ui_.getImage(content),
caption = content.querySelector('.' + elementClasses_.imageCaption);
var data = {
background : false,
border : content.dataset.bordered === 'true' ? true : false,
isstretch : content.dataset.stretched === 'true' ? true : false,
// file : {
url : image.dataset.src || image.src,
bigUrl : image.dataset.bigUrl,
width : image.width,
height : image.height,
additionalData :null,
// },
caption : caption.innerHTML || '',
cover : null
};
return data;
};
/**
* @public
*
* Settings panel content
* @return {Element} element contains all settings
*/
image_plugin.makeSettings = function () {
var currentNode = codex.editor.content.currentNode,
wrapper = currentNode.querySelector('.' + elementClasses_.imageWrapper),
holder = document.createElement('DIV'),
types = {
stretched : "На всю ширину",
bordered : "Добавить рамку"
},
currentImageWrapper = currentNode.querySelector('.' + elementClasses_.imageWrapper ),
currentImageSettings = currentImageWrapper.dataset;
/** Add holder classname */
holder.className = 'ce-image-settings';
/** Now add type selectors */
for (var type in types){
/**
* Settings template
*/
var settingsItem = document.createElement('DIV'),
selectorsHolder = document.createElement('SPAN'),
selectorsButton = document.createElement('SPAN');
settingsItem.classList.add(elementClasses_.settingsItem);
selectorsHolder.classList.add(elementClasses_.selectorHolder);
selectorsButton.classList.add(elementClasses_.selectorButton);
selectorsHolder.appendChild(selectorsButton);
settingsItem.appendChild(selectorsHolder);
selectTypeButton = document.createTextNode(types[type]);
settingsItem.appendChild(selectTypeButton);
/**
* Activate previously selected settings
*/
if ( currentImageSettings[type] == 'true' ){
settingsItem.classList.add(elementClasses_.toggled);
}
methods_.addSelectTypeClickListener(settingsItem, type);
holder.appendChild(settingsItem);
}
return holder;
};
/**
* Share as API
*/
image_plugin.uploadImageFromUri = uploadingCallbacks_.ByPaste.uploadImageFromUrl;
image_plugin.pastePatterns = [
{
type: 'image',
regex: /(?:([^:\/?#]+):)?(?:\/\/([^\/?#]*))?([^?#]*\.(?:jpe?g|gif|png))(?:\?([^#]*))?(?:#(.*))?/i,
callback: image_plugin.uploadImageFromUri
},
{
type: 'uploadCare',
regex: /^https:\/\/(uploadcare\.cmtt\.ru|ucarecdn\.com|static[0-9]+\.siliconrus\.cmtt\.ru|static[0-9]+\.cmtt\.ru)/i,
callback: image_plugin.uploadImageFromUri
} ];
image_plugin.destroy = function () {
image = null;
};
return image_plugin;
})({});

View file

@ -1,162 +0,0 @@
/**
* Instagram plugin
* @version 1.0.0
*/
var instagram = (function(instagram_plugin) {
var methods = {
render : function(content) {
codex.editor.content.switchBlock(codex.editor.content.currentNode, content);
setTimeout(function() {
window.instgrm.Embeds.process();
}, 200);
},
/**
* Drawing html content.
*
* @param url
* @returns {Element} blockquote - HTML template for Instagram Embed JS
*/
instagramBlock : function(url) {
var blockquote = codex.editor.draw.node('BLOCKQUOTE', 'instagram-media instagram', {}),
div = codex.editor.draw.node('DIV', '', {}),
paragraph = codex.editor.draw.node('P', 'ce-paste__instagram--p', {}),
anchor = codex.editor.draw.node('A', '', { href : url });
blockquote.dataset.instgrmVersion = 4;
paragraph.appendChild(anchor);
div.appendChild(paragraph);
blockquote.appendChild(div);
return blockquote;
}
};
/**
* Prepare before usage
* Load important scripts to render embed
*/
instagram_plugin.prepare = function() {
return new Promise(function(resolve, reject){
codex.editor.core.importScript("https://platform.instagram.com/en_US/embeds.js", 'instagram-api').then(function(){
resolve();
}).catch(function(){
reject(Error('Instagram API was not loaded'));
});
});
};
/**
* @private
*
* Make instagram embed via Widgets method
*/
var make_ = function(data, isInternal) {
if (!data.instagram_url)
return;
var block = methods.instagramBlock(data.instagram_url);
if (isInternal) {
setTimeout(function() {
/** Render block */
methods.render(block);
}, 200);
}
if (!isInternal) {
methods.render(block);
}
return block;
};
instagram_plugin.validate = function(data) {
return true;
};
/**
* Saving JSON output.
* Upload data via ajax
*/
instagram_plugin.save = function(blockContent) {
var data;
if (!blockContent)
return;
/** Example */
data = {
instagram_url: blockContent.src
};
return data;
};
instagram_plugin.validate = function(data) {
var checkUrl = new RegExp("http?.+instagram.com\/p?.");
if (!data.instagram_url || checkUrl.exec(data.instagram_url).length == 0)
return;
return true;
};
/**
* Render data
*/
instagram_plugin.render = function(data) {
return make_(data);
};
/**
* callback for instagram url's coming from pasteTool
* Using instagram Embed Widgete to render
* @param url
*/
instagram_plugin.urlPastedCallback = function(url) {
var data = {
instagram_url: url
};
make_(data, true);
};
instagram_plugin.pastePatterns = [
{
type: 'instagram',
regex: /http?.+instagram.com\/p\/([a-zA-Z0-9]*)\S*/,
callback: instagram_plugin.urlPastedCallback
}
];
instagram_plugin.destroy = function () {
instagram = null;
delete window.instgrm
};
return instagram_plugin;
})({});

View file

@ -1,350 +0,0 @@
/**
* Created by nostr on 29.06.16.
*/
/**
* Link tool plugin
*/
var link = (function(link_plugin) {
var settings = {
defaultText : 'Вставьте ссылку ...',
ENTER_KEY : 13,
currentBlock : null,
currentInput : null,
elementClasses : {
link: "tool-link-link",
image: "tool-link-image",
title: "tool-link-title",
description: "tool-link-description",
loader: "tool-link-loader",
error: "tool-link-error"
}
};
var ui = {
make : function (json) {
var wrapper = ui.wrapper(),
siteImage = ui.image(json.image, settings.elementClasses.image),
siteTitle = ui.title(json.title),
siteDescription = ui.description(json.description),
siteLink = ui.link(json.url, json.url);
wrapper.appendChild(siteImage);
wrapper.appendChild(siteTitle);
wrapper.appendChild(siteLink);
wrapper.appendChild(siteDescription);
siteTitle.contentEditable = true;
siteDescription.contentEditable = true;
return wrapper;
},
mainBlock : function () {
var wrapper = document.createElement('div');
wrapper.classList.add("ceditor-tool-link");
return wrapper;
},
input : function () {
var inputTag = document.createElement('input');
inputTag.classList.add("ceditor-tool-link-input");
inputTag.placeholder = settings.defaultText;
inputTag.contentEditable = false;
return inputTag;
},
wrapper : function () {
var wrapper = document.createElement('div');
wrapper.classList.add('tool-link-panel', 'clearfix');
return wrapper;
},
image : function (imageSrc, imageClass) {
var imageTag = document.createElement('img');
imageTag.classList.add(imageClass);
imageTag.setAttribute('src', imageSrc);
return imageTag;
},
link : function (linkUrl, linkText) {
var linkTag = document.createElement('a');
linkTag.classList.add(settings.elementClasses.link);
linkTag.href = linkUrl;
linkTag.target = "_blank";
linkTag.innerText = linkText;
return linkTag;
},
title : function (titleText) {
var titleTag = document.createElement('div');
titleTag.classList.add("tool-link-content", settings.elementClasses.title);
titleTag.innerHTML = titleText;
return titleTag;
},
description : function (descriptionText) {
var descriptionTag = document.createElement('div');
descriptionTag.classList.add("tool-link-content", settings.elementClasses.description);
descriptionTag.innerHTML = descriptionText;
return descriptionTag;
}
};
var methods = {
blockPasteCallback : function (event) {
var clipboardData = event.clipboardData || window.clipboardData,
pastedData = clipboardData.getData('Text'),
block = event.target.parentNode;
methods.renderLink(pastedData, block);
event.stopPropagation();
},
blockKeyDownCallback : function (event) {
var inputTag = event.target,
block = inputTag.parentNode,
url;
if ( block.classList.contains(settings.elementClasses.error) ) {
block.classList.remove(settings.elementClasses.error);
}
if (event.keyCode == settings.ENTER_KEY) {
url = inputTag.value;
methods.renderLink(url, block);
event.preventDefault();
}
},
renderLink : function (url, block) {
Promise.resolve()
.then(function () {
return methods.urlify(url);
})
.then(function (url) {
/* Show loader gif **/
block.classList.add(settings.elementClasses.loader);
return fetch( link_plugin.config.fetchUrl + '?url=' + encodeURI(url) );
})
.then(function (response) {
if (response.status == "200"){
return response.json();
} else {
return Error("Invalid response status: %o", response);
}
})
.then(function (json) {
methods.composeLinkPreview(json, block);
})
.catch(function(error) {
/* Hide loader gif **/
block.classList.remove(settings.elementClasses.loader);
block.classList.add(settings.elementClasses.error);
codex.editor.core.log('Error while doing things with link paste: %o', 'error', error);
});
},
urlify : function (text) {
var urlRegex = /(https?:\/\/\S+)/g;
var links = text.match(urlRegex);
if (links) {
return links[0];
}
return Promise.reject(Error("Url is not matched"));
},
composeLinkPreview : function (json, currentBlock) {
if (json == {}) {
return;
}
var previewBlock = ui.make(json);
settings.currentInput.remove();
currentBlock.appendChild(previewBlock);
currentBlock.classList.remove(settings.elementClasses.loader);
}
};
link_plugin.prepare = function (config) {
link_plugin.config = config;
return Promise.resolve();
};
/**
* Make initial header block
* @param {object} JSON with block data
* @return {Element} element to append
*/
link_plugin.makeNewBlock = function (data) {
var wrapper = ui.mainBlock(),
tag = ui.input();
settings.currentInput = tag;
wrapper.appendChild(tag);
wrapper.classList.add('ce-link');
/**
* Bind callbacks
**/
tag.addEventListener('paste', methods.blockPasteCallback, false);
tag.addEventListener('keydown', methods.blockKeyDownCallback, false);
return wrapper;
};
/**
* Method to render HTML block from JSON
*/
link_plugin.render = function (json) {
if ( json ) {
var block = ui.mainBlock(),
tag = ui.make(json);
block.classList.add('ce-link');
block.appendChild(tag);
return block;
} else {
var wrapper = ui.mainBlock(),
tag = ui.input();
settings.currentInput = tag;
wrapper.appendChild(tag);
wrapper.classList.add('ce-link');
/**
* Bind callbacks
**/
tag.addEventListener('paste', methods.blockPasteCallback, false);
tag.addEventListener('keydown', methods.blockKeyDownCallback, false);
return wrapper;
}
};
link_plugin.validate = function (data) {
if (data.url.trim() == '' || data.title.trim() == '' || data.description.trim() == '')
return;
return true;
};
/**
* Method to extract JSON data from HTML block
*/
link_plugin.save = function (blockContent){
var linkElement = settings.elementClasses.link;
var data = {
url : blockContent.querySelector("." + linkElement).href,
shortLink : blockContent.querySelector("." + linkElement).textContent || '',
image : blockContent.querySelector("." + settings.elementClasses.image).src || '',
title : blockContent.querySelector("." + settings.elementClasses.title).textContent || '',
description : blockContent.querySelector("." + settings.elementClasses.description).textContent || ''
};
return data;
};
link_plugin.destroy = function () {
link = null;
};
return link_plugin;
})({});

View file

@ -1,241 +0,0 @@
/**
* Code Plugin\
* Creates code tag and adds content to this tag
*/
var list = (function(list_plugin) {
/**
* CSS class names
*/
var elementClasses_ = {
pluginWrapper: 'cdx-plugin-list',
li: 'cdx-plugin-list__li',
settings: 'cdx-plugin-list__settings',
settingsItem: 'cdx-plugin-settings__item'
};
var LIST_ITEM_TAG = 'LI';
var ui = {
make: function (blockType) {
var wrapper = this.block(blockType || 'UL', elementClasses_.pluginWrapper);
wrapper.dataset.type = blockType;
wrapper.contentEditable = true;
wrapper.addEventListener('keydown', methods_.keyDown);
return wrapper;
},
block: function (blockType, blockClass) {
var block = document.createElement(blockType);
if (blockClass) block.classList.add(blockClass);
return block;
},
button: function (buttonType) {
var types = {
unordered: '<i class="ce-icon-list-bullet"></i>Обычный',
ordered: '<i class="ce-icon-list-numbered"></i>Нумерованный'
},
button = document.createElement('DIV');
button.innerHTML = types[buttonType];
button.classList.add(elementClasses_.settingsItem);
return button;
}
};
var methods_ = {
/**
* Changes block type => OL or UL
* @param event
* @param blockType
*/
changeBlockStyle : function (event, blockType) {
var currentBlock = codex.editor.content.currentNode,
newEditable = ui.make(blockType),
oldEditable = currentBlock.querySelector("[contenteditable]");
newEditable.dataset.type = blockType;
newEditable.innerHTML = oldEditable.innerHTML;
newEditable.classList.add(elementClasses_.pluginWrapper);
codex.editor.content.switchBlock(currentBlock, newEditable, 'list');
},
keyDown: function (e) {
var controlKeyPressed = e.ctrlKey || e.metaKey,
keyCodeForA = 65;
/**
* If CTRL+A (CMD+A) was pressed, we should select only one list item,
* not all <OL> or <UI>
*/
if (controlKeyPressed && e.keyCode == keyCodeForA) {
e.preventDefault();
/**
* Select <LI> content
*/
methods_.selectListItem();
}
},
/**
* Select all content of <LI> with caret
*/
selectListItem : function () {
var selection = window.getSelection(),
currentSelectedNode = selection.anchorNode.parentNode,
range = new Range();
/**
* Search for <LI> element
*/
while ( currentSelectedNode && currentSelectedNode.tagName != LIST_ITEM_TAG ) {
currentSelectedNode = currentSelectedNode.parentNode;
}
range.selectNodeContents(currentSelectedNode);
selection.removeAllRanges();
selection.addRange(range);
}
};
/**
* Method to render HTML block from JSON
*/
list_plugin.render = function (data) {
var type = data && (data.type == 'ordered' || data.type == 'OL') ? 'OL' : 'UL',
tag = ui.make(type),
newLi;
if (data && data.items) {
data.items.forEach(function (element, index, array) {
newLi = ui.block('li', elementClasses_.li);
newLi.innerHTML = element || '';
tag.appendChild(newLi);
});
} else {
newLi = ui.block('li', elementClasses_.li);
tag.appendChild(newLi);
}
return tag;
};
list_plugin.validate = function(data) {
var isEmpty = data.items.every(function(item){
return item.trim() === '';
});
if (isEmpty){
return;
}
if (data.type != 'UL' && data.type != 'OL'){
console.warn('CodeX Editor List-tool: wrong list type passed %o', data.type);
return;
}
return true;
};
/**
* Method to extract JSON data from HTML block
*/
list_plugin.save = function (blockContent){
var data = {
type : null,
items : []
},
litsItemContent = '',
isEmptyItem = false;
for (var index = 0; index < blockContent.childNodes.length; index++){
litsItemContent = blockContent.childNodes[index].innerHTML;
isEmptyItem = !blockContent.childNodes[index].textContent.trim();
if (!isEmptyItem) {
data.items.push(litsItemContent);
}
}
data.type = blockContent.dataset.type;
return data;
};
list_plugin.makeSettings = function () {
var holder = document.createElement('DIV');
/** Add holder classname */
holder.className = elementClasses_.settings;
var orderedButton = ui.button("ordered"),
unorderedButton = ui.button("unordered");
orderedButton.addEventListener('click', function (event) {
methods_.changeBlockStyle(event, 'OL');
codex.editor.toolbar.settings.close();
});
unorderedButton.addEventListener('click', function (event) {
methods_.changeBlockStyle(event, 'UL');
codex.editor.toolbar.settings.close();
});
holder.appendChild(orderedButton);
holder.appendChild(unorderedButton);
return holder;
};
list_plugin.destroy = function () {
list = null;
};
return list_plugin;
})({});

View file

@ -1,115 +0,0 @@
/**
* Paragraph Plugin
* Creates DIV tag and adds content to this tag
*/
var paragraph = (function(paragraph_plugin) {
/**
* @private
*
* Make initial paragraph block
* @param {object} JSON with block data
* @return {Element} element to append
*/
var make_ = function (data) {
/** Create Empty DIV */
var tag = codex.editor.draw.node('DIV', ['ce-paragraph'], {});
if (data && data.text) {
tag.innerHTML = data.text;
}
tag.contentEditable = true;
return tag;
};
/**
* @private
*
* Handles input data for save
* @param data
*/
var prepareDataForSave_ = function(data) {
};
/**
* @public
*
* Plugins should have prepare method
* @param config
*/
paragraph_plugin.prepare = function(config) {
};
/*
* @public
*
* Method to render HTML block from JSON
*/
paragraph_plugin.render = function (data) {
return make_(data);
};
/**
* @public
*
* Check output data for validity.
* Should be defined by developer
*/
paragraph_plugin.validate = function(output) {
if (output.text === '')
return;
return output;
};
/**
* @public
*
* Method to extract JSON data from HTML block
*/
paragraph_plugin.save = function (blockContent){
var wrappedText = codex.editor.content.wrapTextWithParagraphs(blockContent.innerHTML),
sanitizerConfig = {
tags : {
p : {},
a: {
href: true,
target: '_blank',
rel: 'nofollow'
},
i: {},
b: {},
}
};
var data = {
"text": codex.editor.sanitizer.clean(wrappedText, sanitizerConfig),
"format": "html",
"introText": '<<same>>'
};
return data;
};
paragraph_plugin.destroy = function () {
paragraph = null;
};
return paragraph_plugin;
})({});

Some files were not shown because too many files have changed in this diff Show more