new redactor version (#94)

* new redactor version

* update

* update

* bundler config updated

* clear bundler without plugins
This commit is contained in:
khaydarov 2016-12-07 21:25:31 +03:00 committed by Peter Savchenko
parent 339e15e349
commit 458c834dc9
46 changed files with 9181 additions and 4752 deletions

5
.gitignore vendored
View file

@ -3,4 +3,7 @@
Thumbs.db
/.idea/
/*.sublime-project
/*.sublime-workspace
/*.sublime-workspace
node_modules/*
plugins/*

414
codex-editor.css Normal file
View file

@ -0,0 +1,414 @@
/**
* CodeX Editor stylesheets
* @author CodeX Team https://ifmo.su
*
* https://github.com/codex-team/codex.editor
*/
@import url('icons.css');
/**
* Editor wrapper
*/
.codex-editor{
position: relative;
}
/**
* Working zone - redactor
*/
.ce-redactor{
position: relative;
padding-bottom: 120px;
min-height: 350px;
}
/*.ce-redactor * {
box-sizing: border-box;
}*/
/**
* Remove outlines from inputs
*/
.ce-redactor [contenteditable]{
outline: none !important;
}
/**
* Toolbar
*/
.ce-toolbar{
position: absolute;
z-index: 2;
width: 100%;
/* hidden by default */
display: none;
}
.ce-toolbar.opened{
display: block;
}
.ce-toolbar__content {
position: relative;
max-width: 600px;
margin: 0 auto;
}
/**
* Plus button
*/
.ce-toolbar__plus{
background-image: url('fonts/codex_editor/icon-plus.svg');
background-position: center center;
background-repeat: no-repeat;
text-align: center;
transition: transform 100ms ease;
will-change: transform;
margin-left: -50px;
}
.ce-toolbar__plus.clicked{
transform: rotate(45deg);
}
/**
* Tools list
*/
.ce-toolbar__tools{
position: absolute;
top: 0;
left: 0;
/* hidden by default */
opacity: 0;
visibility: hidden;
transform: translateX(-100px);
transition: all 150ms cubic-bezier(0.600, -0.280, 0.735, 0.045);
}
.ce-toolbar__tools.opened{
opacity: 1;
visibility: visible;
transform: none;
}
.ce-toolbar__plus,
.ce-toolbar__tools li {
display: inline-block;
width: 32px;
height: 32px;
background-color: #eff2f5;
/*box-shadow: 0 0 0 1px #6d748c;*/
margin-right: 17px;
border-radius: 16px;
text-align: center;
cursor: pointer;
font-size: 14px;
will-change: transform, margin-right;
transition: transform 200ms cubic-bezier(0.600, -0.280, 0.735, 0.045), margin 200ms ease-out;
}
.ce-toolbar__tools li i{
line-height: 32px;
}
.ce-toolbar__tools li:hover,
.ce-toolbar__tools .selected{
background: #383b5d;
box-shadow: none;
color: #fff;
}
/* animation for tools opening */
.ce-toolbar__tools li{
transform: rotate(-180deg) scale(.7);
margin-right: -15px;
}
.ce-toolbar__tools.opened li{
transform: none;
margin-right: 17px;
}
/**
* Toolbar right zone with SETTINGS and DELETE
*/
.ce-toolbar__actions{
position: absolute;
right: 10px;
border-radius: 2px;
padding: 2px 4px;
/*background: #f9f9fb;*/
}
/**
* Settings button
*/
.ce-toolbar__settings-btn{
margin-right: .3em;
cursor: pointer;
}
.ce-toolbar__settings-btn,
.ce-toolbar__remove-btn{
color: #5e6475;
}
.ce-toolbar__settings-btn:hover,
.ce-toolbar__remove-btn:hover{
color: #272b35
}
/**
* Settigns pane
*/
.ce-settings,
.ce-toolbar__remove-confirmation{
position: absolute;
right: 0;
margin-top: 10px;
min-width: 200px;
background: #FFFFFF;
border: 1px solid #e7e9f1;
box-shadow: 0px 2px 5px 0px rgba(16, 23, 49, 0.05);
border-radius: 3px;
white-space: nowrap;
color: #707684;
font-size: 13.4px;
/* hidden by default */
display: none;
}
/**
* Settings and remove-confirmation corner
*/
.ce-settings:before,
.ce-toolbar__remove-confirmation:before,
.ce-settings:after,
.ce-toolbar__remove-confirmation:after{
content: "";
position: absolute;
top: -14px;
right: 10px;
border-style: solid;
}
.ce-settings:before,
.ce-toolbar__remove-confirmation:before {
margin: -2px -1px 0;
border-width: 8px;
border-color: transparent transparent #e7e9f1 transparent;
}
.ce-settings:after,
.ce-toolbar__remove-confirmation:after {
border-width: 7px;
border-color: transparent transparent #fff transparent;
}
.ce-settings:before,
.ce-settings:after{
right: 31px;
}
.ce-toolbar__remove-confirmation:before,
.ce-toolbar__remove-confirmation:after{
right: 10px;
}
.ce-toolbar__remove-confirmation{
right: -3px;
}
.ce-settings__item,
.ce-toolbar__remove-confirm
.ce-toolbar__remove-cancel {
cursor: pointer;
}
.ce-settings.opened,
.ce-toolbar__remove-confirmation.opened{
display: block;
}
.ce-settings_plugin{
padding: 20px;
border-bottom: 1px solid #E8EAEE;
}
.ce-settings_plugin:empty{
display: none;
}
.ce-settings_default{
padding: 20px;
}
.ce-settings__item i {
margin-right: 1.3em;
}
/**
* Trash button
*/
.ce-toolbar__remove-btn {
cursor: pointer;
}
.ce-toolbar__remove-confirmation{
padding: 5px 0;
}
.ce-toolbar__remove-confirm,
.ce-toolbar__remove-cancel{
padding: 10px 20px;
}
.ce-toolbar__remove-confirm{
color: #ea5c5c;
}
.ce-toolbar__remove-confirm:hover{
background: #e23d3d;
color: #fff;
}
.ce-toolbar__remove-cancel:hover{
background: #edf0f5;
}
/**
* Overlayed inline toolbar
*/
.ce-toolbar-inline{
position: absolute;
left: 0;
top: 0;
z-index: 3;
background: #242533;
border-radius: 3px;
padding: 0 5px;
margin-top: -.5em;
will-change: transform;
transition: transform .2s cubic-bezier(0.600, -0.280, 0.735, 0.045);
color: #fff;
/* hidden by default */
display: none;
}
.ce-toolbar-inline.opened {
display: block;
}
.ce-toolbar-inline__buttons{
}
.ce-toolbar-inline__buttons button{
background: none;
border: 0;
height: auto !important;
padding: 12px 8px;
color: inherit;
font-size: 12px;
cursor: pointer;
}
.ce-toolbar-inline__buttons button:hover{
background: #171827;
color: #428bff;
}
.ce-toolbar-inline__actions{
position: absolute;
left: 0;
top: 0;
bottom: 0;
right: 0;
border-radius: 3px;
background: #242533;
display: none;
}
.ce-toolbar-inline__actions.opened{
display: block;
}
.ce-toolbar-inline__actions input{
background: transparent !important;
border : 0 !important;
box-sizing: border-box !important;
padding: 10px;
width: 100%;
color: #fff;
outline: none;
}
.ce-toolbar-inline__actions input::-moz-placeholder{ color: #afb4c3 !important;}
.ce-toolbar-inline__actions input::-webkit-input-placeholder{ color: #afb4c3 !important;}
/**
* Base blocks
*/
.ce-block{
margin: 0 5px;
border-radius: 3px;
}
.ce-block--focused{
background: #f9f9fb;
}
.ce-block--feed-mode{
position: relative;
}
.ce-block--feed-mode:before {
content: '\e81b';
font-family: "codex_editor";
display: inline-block;
position: absolute;
left: 17px;
top: 13px;
font-size: 16px;
color: #ef4a4a;
}
/**
* Block content holder
*/
.ce-block__content{
max-width: 600px;
margin: 0 auto;
padding: 1px;
}
.ce-block--stretched{
max-width: none;
padding: 0;
}
/**
* Typographycs
*/
.ce-redactor p{
margin: 0;
}
/**
* Loading bar class
*/
.ce-redactor__loader{
background-image: repeating-linear-gradient(-45deg, transparent, transparent 4px, #f5f9ff 4px, #eaedef 8px) !important;
background-size: 56px 56px;
animation: loading-bar 1.3s infinite linear;
}
@keyframes loading-bar {
100% { background-position: -56% 0 }
}
/**
* Mobile viewport styles
* =================================
*/
@media all and (max-width: 800px){
.ce-block{
margin: 0;
padding-left: 5px;
padding-right: 5px;
}
.ce-block__content{
margin: 0 15px;
}
}

File diff suppressed because it is too large Load diff

1
codex-editor.js.map Normal file

File diff suppressed because one or more lines are too long

View file

@ -1,267 +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'; } /* '' */
/* EDITOR */
.ce_wrapper {
position: relative;
}
.ce_redactor {
position: relative;
outline: none;
padding: 1px 0;
}
.ce_toolbar{
position: absolute;
z-index: 2;
margin-left: -1px;
background: #414758;
/*background: #fff;*/
/*border: 1px solid #e3e7ee;*/
/*box-shadow: 0 2px 11px rgba(27,39,54,.11);*/
/*color: #2e394b;*/
border-radius: 3px;
color: #bab9d8;
display: none;
}
.ce_toolbar.opened{
display: block;
/*animation-name: bounceIn; animation-duration: 200ms; animation-iteration-count: 1;*/
}
.ce_toolbar .toggler{
/*background: #f8f9fd;*/
background: #34384a;
/*color: #6485d0;*/
color: #05ff9b;
cursor: pointer;
border-radius: 3px 0 0 3px
}
.ce_toolbar .toggler,
.ce_toolbar li
{
display: inline-block;
margin: 0 !important;
padding: 12px;
cursor: pointer;
font-size: 14px;
}
.ce_toolbar .selected,
.ce_toolbar .toggler:hover,
.ce_toolbar li:hover
{
background: #36374e;
color: #85aeff;
}
.ce_toolbar .settings_btn{
font-size: 1.1em;
}
/** Block settings panel */
.ce_block_settings{
position: absolute;
z-index: 2;
padding: 25px 30px;
background: #32384c !important;
color: #81839e;
overflow: hidden;
background: #fff;
border-radius: 0 0 3px 3px;
font-size: 14px;
display: none;
}
.ce_block_settings.opened{
display: block;
}
/** Typography styles */
.ce_redactor p{
padding: 5px 0;
font-size: 1em;
line-height: 1.7em;
margin: 0;
}
.ce_redactor ul,
.ce_redactor ol{
list-style-position: inside;
}
.ce_redactor li{
margin: 5px 0;
}
.ce_redactor .ce_block{
padding: 10px;
/*margin: -1px;*/
/*border: 1px dotted #ccc;*/
background: #fff;
outline: none;
}
.ce_redactor .ce_block:focus{
background: #fbfbfb;
border-radius: 3px;
}
/*
}
@-moz-keyframes bounceIn {
0% {opacity: 0;-moz-transform: scale(.3);}
50% {opacity: 1;-moz-transform: scale(1.05);}
70% {-moz-transform: scale(.9);}
100% {-moz-transform: scale(1);}
}
@-o-keyframes bounceIn {
0% {opacity: 0;-o-transform: scale(.3);}
50% {opacity: 1;-o-transform: scale(1.05);}
70% {-o-transform: scale(.9);}
100% {-o-transform: scale(1);}
}
@keyframes bounceIn {
0% {opacity: 0;transform: scale(.3);}
50% {opacity: 1;transform: scale(1.07);}
70% {transform: scale(.9);}
100% {transform: scale(1);}
}
.bounceIn {
-moz-animation-name: bounceIn; -moz-animation-duration: 600ms; -moz-animation-iteration-count: 1;
-o-animation-name: bounceIn; -o-animation-duration: 600ms; -o-animation-iteration-count: 1;
animation-name: bounceIn; animation-duration: 600ms; animation-iteration-count: 1;
}
*/
/** Alerts */
.ce_notifications-block {
position: fixed;
top: 0;
right: 0;
left: 0;
}
.ce_notification-item {
padding: 15px 25px;
font-size: 14px;
text-align: center;
animation-duration: 1s;
animation-iteration-count: 1;
animation-fill-mode: both;
}
.ce_notification-error {
background: #e5f3ed;
color: #55818c;
}
@keyframes flipInX {
from {
transform: perspective(400px) rotate3d(1, 0, 0, 90deg);
animation-timing-function: ease-in;
opacity: 0;
}
40% {
transform: perspective(400px) rotate3d(1, 0, 0, -20deg);
animation-timing-function: ease-in;
}
60% {
transform: perspective(400px) rotate3d(1, 0, 0, 10deg);
opacity: 1;
}
80% {
transform: perspective(400px) rotate3d(1, 0, 0, -5deg);
}
to {
transform: perspective(400px);
}
}
.flipInX {
backface-visibility: visible !important;
animation-name: flipInX;
}

129
editor.js Normal file
View file

@ -0,0 +1,129 @@
var codex = (function(codex){
var init = function() {
require('./modules/core');
require('./modules/ui');
require('./modules/transport');
require('./modules/renderer');
require('./modules/saver');
require('./modules/content');
require('./modules/toolbar/toolbar');
require('./modules/tools');
require('./modules/callbacks');
require('./modules/draw');
require('./modules/caret');
require('./modules/notifications');
require('./modules/parser');
};
/**
* @public
*
* holds initial settings
*/
codex.settings = {
tools : ['paragraph', 'header', 'picture', 'list', 'quote', 'code', 'twitter', 'instagram', 'smile'],
textareaId: 'codex-editor',
uploadImagesUrl: '/editor/transport/',
// Type of block showing on empty editor
initialBlockPlugin: "paragraph"
};
/**
* public
*
* Static nodes
*/
codex.nodes = {
textarea : 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
*/
codex.state = {
jsonOutput: [],
blocks : [],
inputs : []
};
/**
* Initialization
* @uses Promise cEditor.core.prepare
* @param {} userSettings are :
* - tools [],
* - textareaId String
* ...
*
* 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
*/
codex.start = function (userSettings) {
init();
this.core.prepare(userSettings)
// If all ok, make UI, bind events and parse initial-content
.then(this.ui.make)
.then(this.ui.addTools)
.then(this.ui.bindEvents)
.then(this.ui.preparePlugins)
.then(this.transport.prepare)
.then(this.renderer.makeBlocksFromData)
.then(this.ui.saveInputs)
.catch(function (error) {
codex.core.log('Initialization failed with error: %o', 'warn', error);
});
};
return codex;
})({});
module.exports = codex;

File diff suppressed because one or more lines are too long

View file

@ -0,0 +1,16 @@
<?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>

After

Width:  |  Height:  |  Size: 1.4 KiB

63
icons.css Normal file
View file

@ -0,0 +1,63 @@
@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'; } /* '' */

8
index.js Normal file
View file

@ -0,0 +1,8 @@
/**
*
*/
'use strict';
var editor = require('./editor');
module.exports = editor;

758
modules/callbacks.js Normal file
View file

@ -0,0 +1,758 @@
var codex = require('../editor');
var callbacks = (function(callbacks) {
callbacks.redactorSyncTimeout = null;
callbacks.globalKeydown = function(event){
switch (event.keyCode){
case codex.core.keys.TAB : codex.callback.tabKeyPressed(event); break;
case codex.core.keys.ENTER : codex.callback.enterKeyPressed(event); break;
case codex.core.keys.ESC : codex.callback.escapeKeyPressed(event); break;
default : codex.callback.defaultKeyPressed(event); break;
}
};
callbacks.globalKeyup = function(event){
switch (event.keyCode){
case codex.core.keys.UP :
case codex.core.keys.LEFT :
case codex.core.keys.RIGHT :
case codex.core.keys.DOWN : codex.callback.arrowKeyPressed(event); break;
}
};
callbacks.tabKeyPressed = function(event){
if ( !codex.toolbar.opened ) {
codex.toolbar.open();
}
if (codex.toolbar.opened && !codex.toolbar.toolbox.opened) {
codex.toolbar.toolbox.open();
} else {
codex.toolbar.toolbox.leaf();
}
event.preventDefault();
};
/**
* ENTER key handler
* Makes new paragraph block
*/
callbacks.enterKeyPressed = function(event){
/** Set current node */
var firstLevelBlocksArea = codex.callback.clickedOnFirstLevelBlockArea();
if (firstLevelBlocksArea) {
event.preventDefault();
/**
* it means that we lose input index, saved index before is not correct
* therefore we need to set caret when we insert new block
*/
codex.caret.inputIndex = -1;
codex.callback.enterPressedOnBlock();
return;
}
if (event.target.contentEditable == 'true') {
/** Update input index */
codex.caret.saveCurrentInputIndex();
}
if (!codex.content.currentNode) {
/**
* Enter key pressed in first-level block area
*/
codex.callback.enterPressedOnBlock(event);
return;
}
var currentInputIndex = codex.caret.getCurrentInputIndex() || 0,
workingNode = codex.content.currentNode,
tool = workingNode.dataset.tool,
isEnterPressedOnToolbar = codex.toolbar.opened &&
codex.toolbar.current &&
event.target == codex.state.inputs[currentInputIndex];
/** The list of tools which needs the default browser behaviour */
var enableLineBreaks = codex.tools[tool].enableLineBreaks;
/** This type of block creates when enter is pressed */
var NEW_BLOCK_TYPE = 'paragraph';
/**
* When toolbar is opened, select tool instead of making new paragraph
*/
if ( isEnterPressedOnToolbar ) {
event.preventDefault();
codex.toolbar.toolbox.toolClicked(event);
codex.toolbar.close();
return;
}
/**
* Allow making new <p> in same block by SHIFT+ENTER and forbids to prevent default browser behaviour
*/
if ( event.shiftKey && !enableLineBreaks) {
codex.callback.enterPressedOnBlock(codex.content.currentBlock, event);
event.preventDefault();
} else if ( (event.shiftKey && !enableLineBreaks) || (!event.shiftKey && enableLineBreaks) ){
/** XOR */
return;
}
var isLastTextNode = false,
currentSelection = window.getSelection(),
currentSelectedNode = currentSelection.anchorNode,
caretAtTheEndOfText = codex.caret.position.atTheEnd(),
isTextNodeHasParentBetweenContenteditable = false;
/**
* 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 == codex.core.nodeTypes.TEXT &&
!isTextNodeHasParentBetweenContenteditable &&
!caretAtTheEndOfText
){
event.preventDefault();
codex.core.log('Splitting Text node...');
codex.content.splitBlock(currentInputIndex);
/** Show plus button when next input after split is empty*/
if (!codex.state.inputs[currentInputIndex + 1].textContent.trim()) {
codex.toolbar.showPlusButton();
}
} else {
if ( currentSelectedNode && currentSelectedNode.parentNode) {
isLastTextNode = !currentSelectedNode.parentNode.nextSibling;
}
if ( isLastTextNode && caretAtTheEndOfText ) {
event.preventDefault();
codex.core.log('ENTER clicked in last textNode. Create new BLOCK');
codex.content.insertBlock({
type : NEW_BLOCK_TYPE,
block : codex.tools[NEW_BLOCK_TYPE].render()
}, true );
codex.toolbar.move();
codex.toolbar.open();
/** Show plus button with empty block */
codex.toolbar.showPlusButton();
} else {
codex.core.log('Default ENTER behavior.');
}
}
/** get all inputs after new appending block */
codex.ui.saveInputs();
};
callbacks.escapeKeyPressed = function(event){
/** Close all toolbar */
codex.toolbar.close();
/** Close toolbox */
codex.toolbar.toolbox.close();
event.preventDefault();
};
callbacks.arrowKeyPressed = function(event){
codex.content.workingNodeChanged();
/* Closing toolbar */
codex.toolbar.close();
codex.toolbar.move();
};
callbacks.defaultKeyPressed = function(event) {
codex.toolbar.close();
if (!codex.toolbar.inline.actionsOpened) {
codex.toolbar.inline.close();
codex.content.clearMark();
}
};
callbacks.redactorClicked = function (event) {
codex.content.workingNodeChanged(event.target);
codex.ui.saveInputs();
var selectedText = codex.toolbar.inline.getSelectionText();
/**
* If selection range took off, then we hide inline toolbar
*/
if (selectedText.length === 0) {
codex.toolbar.inline.close();
}
/** Update current input index in memory when caret focused into existed input */
if (event.target.contentEditable == 'true') {
codex.caret.saveCurrentInputIndex();
}
if (codex.content.currentNode === null) {
/**
* If inputs in redactor does not exits, then we put input index 0 not -1
*/
var indexOfLastInput = codex.state.inputs.length > 0 ? codex.state.inputs.length - 1 : 0;
/** If we have any inputs */
if (codex.state.inputs.length) {
/** getting firstlevel parent of input */
var firstLevelBlock = codex.content.getFirstLevelBlock(codex.state.inputs[indexOfLastInput]);
}
/** If input is empty, then we set caret to the last input */
if (codex.state.inputs.length && codex.state.inputs[indexOfLastInput].textContent === '' && firstLevelBlock.dataset.tool == 'paragraph') {
codex.caret.setToBlock(indexOfLastInput);
} else {
/** Create new input when caret clicked in redactors area */
var NEW_BLOCK_TYPE = 'paragraph';
codex.content.insertBlock({
type : NEW_BLOCK_TYPE,
block : codex.tools[NEW_BLOCK_TYPE].render()
});
/** If there is no inputs except inserted */
if (codex.state.inputs.length === 1) {
codex.caret.setToBlock(indexOfLastInput);
} else {
/** Set caret to this appended input */
codex.caret.setToNextBlock(indexOfLastInput);
}
}
/**
* Move toolbar to the right position and open
*/
codex.toolbar.move();
codex.toolbar.open();
} else {
/**
* Move toolbar to the new position and open
*/
codex.toolbar.move();
codex.toolbar.open();
/** Close all panels */
codex.toolbar.settings.close();
codex.toolbar.toolbox.close();
}
var inputIsEmpty = !codex.content.currentNode.textContent.trim();
if (inputIsEmpty) {
/** Show plus button */
codex.toolbar.showPlusButton();
} else {
/** Hide plus buttons */
codex.toolbar.hidePlusButton();
}
var currentNodeType = codex.content.currentNode.dataset.tool;
/** Mark current block*/
if (currentNodeType != 'paragraph' || !inputIsEmpty) {
codex.content.markBlock();
}
};
/**
* This method allows to define, is caret in contenteditable element or not.
* 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.
*/
callbacks.clickedOnFirstLevelBlockArea = function() {
var selection = window.getSelection(),
anchorNode = selection.anchorNode,
flag = false;
if (selection.rangeCount == 0) {
return true;
} else {
if (!codex.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" */
return flag ? false : true;
}
};
/**
* Toolbar button click handler
* @param this - cursor to the button
*/
callbacks.toolbarButtonClicked = function (event) {
var button = this;
codex.toolbar.current = button.dataset.type;
codex.toolbar.toolbox.toolClicked(event);
codex.toolbar.close();
};
callbacks.redactorInputEvent = function (event) {
/**
* Clear previous sync-timeout
*/
if (this.redactorSyncTimeout){
clearTimeout(this.redactorSyncTimeout);
}
/**
* Start waiting to input finish and sync redactor
*/
this.redactorSyncTimeout = setTimeout(function() {
codex.content.sync();
}, 500);
};
/** Show or Hide toolbox when plus button is clicked */
callbacks.plusButtonClicked = function() {
if (!codex.nodes.toolbox.classList.contains('opened')) {
codex.toolbar.toolbox.open();
} else {
codex.toolbar.toolbox.close();
}
};
/**
* Block handlers for KeyDown events
*/
callbacks.blockKeydown = function(event, block) {
switch (event.keyCode){
case codex.core.keys.DOWN:
case codex.core.keys.RIGHT:
codex.callback.blockRightOrDownArrowPressed(block);
break;
case codex.core.keys.BACKSPACE:
codex.callback.backspacePressed(block);
break;
case codex.core.keys.UP:
case codex.core.keys.LEFT:
codex.callback.blockLeftOrUpArrowPressed(block);
break;
}
};
/**
* RIGHT or DOWN keydowns on block
*/
callbacks.blockRightOrDownArrowPressed = function (block) {
var selection = window.getSelection(),
inputs = codex.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)
{
codex.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 (codex.core.isDomNode(lastChild)) {
deepestTextnode = codex.content.getDeepestTextNodeFromPosition(lastChild, lastChild.childNodes.length);
} else {
deepestTextnode = lastChild;
}
caretInLastChild = selection.anchorNode == deepestTextnode;
caretAtTheEndOfText = deepestTextnode.length == selection.anchorOffset;
if ( !caretInLastChild || !caretAtTheEndOfText ) {
codex.core.log('arrow [down|right] : caret does not reached the end');
return false;
}
codex.caret.setToNextBlock(editableElementIndex);
};
/**
* LEFT or UP keydowns on block
*/
callbacks.blockLeftOrUpArrowPressed = function (block) {
var selection = window.getSelection(),
inputs = codex.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) {
codex.caret.setToPreviousBlock(editableElementIndex);
return;
}
firstChild = focusedNode.childNodes[0];
if (codex.core.isDomNode(firstChild)) {
deepestTextnode = codex.content.getDeepestTextNodeFromPosition(firstChild, 0);
} else {
deepestTextnode = firstChild;
}
caretInFirstChild = selection.anchorNode == deepestTextnode;
caretAtTheBeginning = selection.anchorOffset === 0;
if ( caretInFirstChild && caretAtTheBeginning ) {
codex.caret.setToPreviousBlock(editableElementIndex);
}
};
/**
* Callback for enter key pressing in first-level block area
*/
callbacks.enterPressedOnBlock = function (event) {
var NEW_BLOCK_TYPE = 'paragraph';
codex.content.insertBlock({
type : NEW_BLOCK_TYPE,
block : codex.tools[NEW_BLOCK_TYPE].render()
}, true );
codex.toolbar.move();
codex.toolbar.open();
};
callbacks.backspacePressed = function (block) {
var currentInputIndex = codex.caret.getCurrentInputIndex(),
range,
selectionLength,
firstLevelBlocksCount;
if (block.textContent.trim()) {
range = codex.content.getRange();
selectionLength = range.endOffset - range.startOffset;
if (codex.caret.position.atStart() && !selectionLength) {
codex.content.mergeBlocks(currentInputIndex);
} else {
return;
}
}
if (!selectionLength) {
block.remove();
}
firstLevelBlocksCount = codex.nodes.redactor.childNodes.length;
/**
* If all blocks are removed
*/
if (firstLevelBlocksCount === 0) {
/** update currentNode variable */
codex.content.currentNode = null;
/** Inserting new empty initial block */
codex.ui.addInitialBlock();
/** Updating inputs state after deleting last block */
codex.ui.saveInputs();
/** Set to current appended block */
setTimeout(function () {
codex.caret.setToPreviousBlock(1);
}, 10);
} else {
if (codex.caret.inputIndex !== 0) {
/** Target block is not first */
codex.caret.setToPreviousBlock(codex.caret.inputIndex);
} else {
/** If we try to delete first block */
codex.caret.setToNextBlock(codex.caret.inputIndex);
}
}
codex.toolbar.move();
if (!codex.toolbar.opened) {
codex.toolbar.open();
}
/** Updating inputs state */
codex.ui.saveInputs();
/** Prevent default browser behaviour */
event.preventDefault();
};
callbacks.blockPaste = function(event) {
var currentInputIndex = codex.caret.getCurrentInputIndex(),
node = codex.state.inputs[currentInputIndex];
setTimeout(function() {
codex.content.sanitize(node);
}, 10);
};
callbacks._blockPaste = function(event) {
var currentInputIndex = codex.caret.getCurrentInputIndex();
/**
* create an observer instance
*/
var observer = new MutationObserver(codex.callback.handlePasteEvents);
/**
* configuration of the observer:
*/
var config = { attributes: true, childList: true, characterData: false };
// pass in the target node, as well as the observer options
observer.observe(codex.state.inputs[currentInputIndex], config);
};
/**
* Sends all mutations to paste handler
*/
callbacks.handlePasteEvents = function(mutations) {
mutations.forEach(codex.content.paste);
};
/**
* Clicks on block settings button
*/
callbacks.showSettingsButtonClicked = function(){
/**
* 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 = codex.content.currentNode.dataset.tool;
codex.toolbar.settings.toggle(currentToolType);
/** Close toolbox when settings button is active */
codex.toolbar.toolbox.close();
codex.toolbar.settings.hideRemoveActions();
};
return callbacks;
})({});
codex.callback = callbacks;
module.exports = callbacks;

241
modules/caret.js Normal file
View file

@ -0,0 +1,241 @@
var codex = require('../editor');
var caret = (function(caret) {
/**
* @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 || this.offset || 0;
index = index || this.focusedNodeIndex || 0;
var childs = el.childNodes,
nodeToSet;
if ( childs.length === 0 ) {
nodeToSet = el;
} else {
nodeToSet = childs[index];
}
/** If Element is INPUT */
if (el.tagName == 'INPUT') {
el.focus();
return;
}
if (codex.core.isDomNode(nodeToSet)) {
nodeToSet = codex.content.getDeepestTextNodeFromPosition(nodeToSet, nodeToSet.childNodes.length);
}
var range = document.createRange(),
selection = window.getSelection();
setTimeout(function() {
range.setStart(nodeToSet, offset);
range.setEnd(nodeToSet, offset);
selection.removeAllRanges();
selection.addRange(range);
codex.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 = codex.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 ++;
}
this.inputIndex = editableElementIndex;
};
/**
* Returns current input index (caret object)
*/
caret.getCurrentInputIndex = function() {
return this.inputIndex;
};
/**
* @param {int} index - index of first-level block after that we set caret into next input
*/
caret.setToNextBlock = function(index) {
var inputs = codex.state.inputs,
nextInput = inputs[index + 1];
if (!nextInput) {
codex.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);
}
codex.caret.inputIndex = index + 1;
codex.caret.set(nextInput, 0, 0);
codex.content.workingNodeChanged(nextInput);
};
/**
* @param {int} index - index of target input.
* Sets caret to input with this index
*/
caret.setToBlock = function(index) {
var inputs = codex.state.inputs,
targetInput = inputs[index];
console.assert( targetInput , 'caret.setToBlock: target input does not exists');
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);
}
codex.caret.inputIndex = index;
codex.caret.set(targetInput, 0, 0);
codex.content.workingNodeChanged(targetInput);
};
/**
* @param {int} index - index of input
*/
caret.setToPreviousBlock = function(index) {
index = index || 0;
var inputs = codex.state.inputs,
previousInput = inputs[index - 1],
lastChildNode,
lengthOfLastChildNode,
emptyTextElement;
if (!previousInput) {
codex.core.log('We are reached first node');
return;
}
lastChildNode = codex.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);
}
codex.caret.inputIndex = index - 1;
codex.caret.set(previousInput, previousInput.childNodes.length - 1, lengthOfLastChildNode);
codex.content.workingNodeChanged(inputs[index - 1]);
};
caret.position = {
atStart : function() {
var selection = window.getSelection(),
anchorOffset = selection.anchorOffset,
anchorNode = selection.anchorNode,
firstLevelBlock = codex.content.getFirstLevelBlock(anchorNode),
pluginsRender = firstLevelBlock.childNodes[0];
if (!codex.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;
}
};
return caret;
})({});
codex.caret = caret;
module.exports = caret;

631
modules/content.js Normal file
View file

@ -0,0 +1,631 @@
var codex = require('../editor');
var content = (function(content) {
content.currentNode = null;
/**
* Synchronizes redactor with original textarea
*/
content.sync = function () {
codex.core.log('syncing...');
/**
* Save redactor content to codex.state
*/
codex.state.html = codex.nodes.redactor.innerHTML;
};
/**
* @deprecated
*/
content.getNodeFocused = function() {
var selection = window.getSelection(),
focused;
if (selection.anchorNode === null) {
return null;
}
if ( selection.anchorNode.nodeType == codex.core.nodeTypes.TAG ) {
focused = selection.anchorNode;
} else {
focused = selection.focusNode.parentElement;
}
if ( !codex.parser.isFirstLevelBlock(focused) ) {
/** Iterate with parent nodes to find first-level*/
var parent = focused.parentNode;
while (parent && !codex.parser.isFirstLevelBlock(parent)){
parent = parent.parentNode;
}
focused = parent;
}
if (focused != codex.nodes.redactor){
return focused;
}
return null;
};
/**
* Appends background to the block
*/
content.markBlock = function() {
codex.content.currentNode.classList.add(codex.ui.className.BLOCK_HIGHLIGHTED);
};
/**
* Clear background
*/
content.clearMark = function() {
if (codex.content.currentNode) {
codex.content.currentNode.classList.remove(codex.ui.className.BLOCK_HIGHLIGHTED);
}
};
/**
* @private
*
* Finds first-level block
* @param {Element} node - selected or clicked in redactors area node
*/
content.getFirstLevelBlock = function(node) {
if (!codex.core.isDomNode(node)) {
node = node.parentNode;
}
if (node === codex.nodes.redactor || node === document.body) {
return null;
} else {
while(!node.classList.contains(codex.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
* 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 */
codex.content.clearMark();
if (!targetNode) {
return;
}
this.currentNode = this.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 function_name(targetBlock, newBlock) {
if (!targetBlock || !newBlock){
codex.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(codex.ui.className.BLOCK_CLASSNAME)) {
targetBlock = targetBlock.parentNode;
}
/** Replacing */
codex.nodes.redactor.replaceChild(newBlock, targetBlock);
/**
* Set new node as current
*/
codex.content.workingNodeChanged(newBlock);
/**
* Add block handlers
*/
codex.ui.addBlockHandlers(newBlock);
/**
* Save changes
*/
codex.ui.saveInputs();
};
/**
* @private
*
* 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 = codex.content.currentNode,
newBlockContent = blockData.block,
blockType = blockData.type,
isStretched = blockData.stretched;
var newBlock = codex.content.composeNewBlock(newBlockContent, blockType, isStretched);
if (workingBlock) {
codex.core.insertAfter(workingBlock, newBlock);
} else {
/**
* If redactor is empty, append as first child
*/
codex.nodes.redactor.appendChild(newBlock);
}
/**
* Block handler
*/
codex.ui.addBlockHandlers(newBlock);
/**
* Set new node as current
*/
codex.content.workingNodeChanged(newBlock);
/**
* Save changes
*/
codex.ui.saveInputs();
if ( needPlaceCaret ) {
/**
* If we don't know input index then we set default value -1
*/
var currentInputIndex = codex.caret.getCurrentInputIndex() || -1;
if (currentInputIndex == -1) {
var editableElement = newBlock.querySelector('[contenteditable]'),
emptyText = document.createTextNode('');
editableElement.appendChild(emptyText);
codex.caret.set(editableElement, 0, 0);
codex.toolbar.move();
codex.toolbar.showPlusButton();
} else {
/** Timeout for browsers execution */
setTimeout(function () {
/** Setting to the new input */
codex.caret.setToNextBlock(currentInputIndex);
codex.toolbar.move();
codex.toolbar.open();
}, 10);
}
}
};
/**
* Replaces blocks with saving content
* @protected
* @param {Element} noteToReplace
* @param {Element} newNode
* @param {Element} blockType
*/
content.switchBlock = function(blockToReplace, newBlock, tool){
var newBlockComposed = codex.content.composeNewBlock(newBlock, tool);
/** Replacing */
codex.content.replaceBlock(blockToReplace, newBlockComposed);
/** Save new Inputs when block is changed */
codex.ui.saveInputs();
};
/**
* Iterates between child noted and looking for #text node on deepest level
* @private
* @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 == codex.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 looking_from_start = false;
/** For looking from START */
if (position === 0) {
looking_from_start = true;
position = 1;
}
while ( position ) {
/** initial verticle of node. */
if ( looking_from_start ) {
block = block.childNodes[0];
} else {
block = block.childNodes[position - 1];
}
if ( block.nodeType == codex.core.nodeTypes.TAG ){
position = block.childNodes.length;
} else if (block.nodeType == codex.core.nodeTypes.TEXT ){
position = 0;
}
}
return block;
};
/**
* @private
*/
content.composeNewBlock = function (block, tool, isStretched) {
var newBlock = codex.draw.node('DIV', codex.ui.className.BLOCK_CLASSNAME, {}),
blockContent = codex.draw.node('DIV', codex.ui.className.BLOCK_CONTENT, {});
blockContent.appendChild(block);
newBlock.appendChild(blockContent);
if (isStretched) {
blockContent.classList.add(codex.ui.className.BLOCK_STRETCHED);
}
newBlock.dataset.tool = tool;
return newBlock;
};
/**
* Returns Range object of current selection
*/
content.getRange = function() {
var selection = window.getSelection().getRangeAt(0);
return selection;
};
/**
* Divides block in two blocks (after and before caret)
* @private
* @param {Int} inputIndex - target input index
*/
content.splitBlock = function(inputIndex) {
var selection = window.getSelection(),
anchorNode = selection.anchorNode,
anchorNodeText = anchorNode.textContent,
caretOffset = selection.anchorOffset,
textBeforeCaret,
textNodeBeforeCaret,
textAfterCaret,
textNodeAfterCaret;
var currentBlock = codex.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 */
codex.state.inputs[inputIndex].innerHTML = '';
/**
* Append all childs founded before anchorNode
*/
var previousChildsLength = previousChilds.length;
for(i = 0; i < previousChildsLength; i++) {
codex.state.inputs[inputIndex].appendChild(previousChilds[i]);
}
codex.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 = 'paragraph';
/**
* Make new paragraph with text after caret
*/
codex.content.insertBlock({
type : NEW_BLOCK_TYPE,
block : codex.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
*/
content.mergeBlocks = function(currentInputIndex, targetInputIndex) {
/** If current input index is zero, then prevent method execution */
if (currentInputIndex === 0) {
return;
}
var targetInput,
currentInputContent = codex.state.inputs[currentInputIndex].innerHTML;
if (!targetInputIndex) {
targetInput = codex.state.inputs[currentInputIndex - 1];
} else {
targetInput = codex.state.inputs[targetInputIndex];
}
targetInput.innerHTML += currentInputContent;
};
/**
* @private
*
* Callback for HTML Mutations
* @param {Array} mutation - Mutation Record
*/
content.paste = function(mutation) {
var workingNode = codex.content.currentNode,
tool = workingNode.dataset.tool;
if (codex.tools[tool].allowedToPaste) {
codex.content.sanitize(mutation.addedNodes);
} else {
codex.content.pasteTextContent(mutation.addedNodes);
}
};
/**
* @private
*
* gets only text/plain content of node
* @param {Element} target - HTML node
*/
content.pasteTextContent = function(nodes) {
var node = nodes[0],
textNode = document.createTextNode(node.textContent);
if (codex.core.isDomNode(node)) {
node.parentNode.replaceChild(textNode, node);
}
};
/**
* @private
*
* Sanitizes HTML content
* @param {Element} target - inserted element
* @uses DFS function for deep searching
*/
content.sanitize = function(target) {
if (!target) {
return;
}
for (var i = 0; i < target.childNodes.length; i++) {
this.dfs(target.childNodes[i]);
}
};
/**
* Clears styles
* @param {Element|Text}
*/
content.clearStyles = function(target) {
var href,
newNode = null,
blockTags = ['P', 'BLOCKQUOTE', 'UL', 'CODE', 'OL', 'LI', 'H1', 'H2', 'H3', 'H4', 'H5', 'H6', 'DIV', 'PRE', 'HEADER', 'SECTION'],
allowedTags = ['P', 'B', 'I', 'A', 'U', 'BR'],
needReplace = !allowedTags.includes(target.tagName),
isDisplayedAsBlock = blockTags.includes(target.tagName);
if (!codex.core.isDomNode(target)){
return target;
}
if (!target.parentNode){
return target;
}
if (needReplace) {
if (isDisplayedAsBlock) {
newNode = document.createElement('P');
newNode.innerHTML = target.innerHTML;
target.parentNode.replaceChild(newNode, target);
target = newNode;
} else {
newNode = document.createTextNode(` ${target.textContent} `);
newNode.textContent = newNode.textContent.replace(/\s{2,}/g, ' ');
target.parentNode.replaceChild(newNode, target);
}
}
/** keep href attributes of tag A */
if (target.tagName == 'A') {
href = target.getAttribute('href');
}
/** Remove all tags */
while(target.attributes.length > 0) {
target.removeAttribute(target.attributes[0].name);
}
/** return href */
if (href) {
target.setAttribute('href', href);
}
return target;
};
/**
* Depth-first search Algorithm
* returns all childs
* @param {Element}
*/
content.dfs = function(el) {
if (!codex.core.isDomNode(el))
return;
var sanitized = this.clearStyles(el);
for(var i = 0; i < sanitized.childNodes.length; i++) {
this.dfs(sanitized.childNodes[i]);
}
};
return content;
})({});
codex.content = content;
module.exports = content;

181
modules/core.js Normal file
View file

@ -0,0 +1,181 @@
var codex = require('./../editor');
var core = (function(core) {
/**
* @public
*
* Editor preparing method
* @return Promise
*/
core.prepare = function (userSettings) {
return new Promise(function(resolve, reject) {
if ( userSettings ){
codex.settings.tools = userSettings.tools || codex.settings.tools;
}
if (userSettings.data) {
codex.state.blocks = userSettings.data;
}
codex.nodes.textarea = document.getElementById(userSettings.textareaId || codex.settings.textareaId);
if (typeof codex.nodes.textarea === undefined || codex.nodes.textarea === null) {
reject(Error("Textarea wasn't found by ID: #" + userSettings.textareaId));
} 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 && console[ type ] ){
if ( arg ) console[ type ]( msg , arg );
else 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
};
/**
* @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;
};
/**
* Native Ajax
*/
core.ajax = function (data) {
if (!data || !data.url){
return;
}
var XMLHTTP = window.XMLHttpRequest ? new XMLHttpRequest() : new ActiveXObject("Microsoft.XMLHTTP"),
success_function = function(){},
params = '',
obj;
data.async = true;
data.type = data.type || 'GET';
data.data = data.data || '';
data['content-type'] = data['content-type'] || 'application/json; charset=utf-8';
success_function = data.success || success_function ;
if (data.type == 'GET' && data.data) {
data.url = /\?/.test(data.url) ? data.url + '&' + data.data : data.url + '?' + data.data;
} else {
for(obj in data.data) {
params += (obj + '=' + encodeURIComponent(data.data[obj]) + '&');
}
}
if (data.withCredentials) {
XMLHTTP.withCredentials = true;
}
if (data.beforeSend && typeof data.beforeSend == 'function') {
data.beforeSend.call();
}
XMLHTTP.open( data.type, data.url, data.async );
XMLHTTP.setRequestHeader("X-Requested-With", "XMLHttpRequest");
XMLHTTP.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
XMLHTTP.onreadystatechange = function() {
if (XMLHTTP.readyState == 4 && XMLHTTP.status == 200) {
success_function(XMLHTTP.responseText);
}
};
XMLHTTP.send(params);
};
/** Appends script to head of document */
core.importScript = function(scriptPath, instanceName) {
/** Script is already loaded */
if ( !instanceName || (instanceName && document.getElementById('ce-script-' + instanceName)) ) {
codex.core.log("Instance name of script is missed or script is already loaded", "warn");
return;
}
var script = document.createElement('SCRIPT');
script.type = "text/javascript";
script.src = scriptPath;
script.async = true;
script.defer = true;
if (instanceName) {
script.id = "ce-script-" + instanceName;
}
document.head.appendChild(script);
return script;
};
return core;
})({});
codex.core = core;
module.exports = core;

311
modules/draw.js Normal file
View file

@ -0,0 +1,311 @@
var codex = require('../editor');
var draw = (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 = 'Type URL ...';
input.setAttribute('form', 'defaultForm');
input.setAttribute('autofocus', 'autofocus');
return input;
};
/**
* Block with notifications
*/
draw.alertsHolder = function() {
var block = document.createElement('div');
block.classList.add('ce_notifications-block');
return block;
};
/**
* @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"),
tool_icon = document.createElement("i"),
tool_title = document.createElement("span");
button.dataset.type = type;
button.setAttribute('title', type);
tool_icon.classList.add(classname);
tool_title.classList.add('ce_toolbar_tools--title');
button.appendChild(tool_icon);
button.appendChild(tool_title);
return button;
};
/**
* @protected
*
* Draws tools for inline toolbar
*
* @param {String} type
* @param {String} classname
*/
draw.toolbarButtonInline = function(type, classname) {
var button = document.createElement("BUTTON"),
tool_icon = document.createElement("I");
button.type = "button";
button.dataset.type = type;
tool_icon.classList.add(classname);
button.appendChild(tool_icon);
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;
};
draw.pluginsRender = function(type, content) {
return {
type : type,
block : cEditor.tools[type].render({
text : content
})
};
};
return draw;
})({});
codex.draw = draw;
module.exports = draw;

45
modules/notifications.js Normal file
View file

@ -0,0 +1,45 @@
var codex = require('../editor');
var notifications = (function(notifications) {
/**
* Error notificator. Shows block with message
* @protected
*/
notifications.errorThrown = function(errorMsg, event) {
codex.notifications.send('This action is not available currently', event.type, false);
},
/**
* Appends notification with different types
* @param message {string} - Error or alert message
* @param type {string} - Type of message notification. Ex: Error, Warning, Danger ...
* @param append {boolean} - can be True or False when notification should be inserted after
*/
notifications.send = function(message, type, append) {
var notification = codex.draw.block('div');
notification.textContent = message;
notification.classList.add('ce_notification-item', 'ce_notification-' + type, 'flipInX');
if (!append) {
codex.nodes.notifications.innerHTML = '';
}
codex.nodes.notifications.appendChild(notification);
setTimeout(function () {
notification.remove();
}, 3000);
};
return notifications;
})({});
codex.notifications = notifications;
module.exports = notifications;

261
modules/parser.js Normal file
View file

@ -0,0 +1,261 @@
var codex = require('../editor');
var parser = (function(parser) {
parser.init = function() {
};
/**
* Splits content by `\n` and returns blocks
*/
parser.getSeparatedTexttSeparatedTextFromContent = function(content) {
return content.split('\n');
};
/** inserting text */
parser.insertPastedContent = function(content) {
var blocks = this.getSeparatedTextFromContent(content),
i,
inputIndex = cEditor.caret.getCurrentInputIndex(),
textNode,
parsedTextContent;
for(i = 0; i < blocks.length; i++) {
blocks[i].trim();
if (blocks[i]) {
var data = cEditor.draw.pluginsRender('paragraph', blocks[i]);
cEditor.content.insertBlock(data);
}
}
};
/**
* Asynchronously parses textarea input string to HTML editor blocks
*/
parser.parseTextareaContent = function () {
var initialContent = cEditor.nodes.textarea.value;
if ( initialContent.trim().length === 0 ) return true;
cEditor.parser
/** Get child nodes async-aware */
.getNodesFromString(initialContent)
/** Then append nodes to the redactor */
.then(cEditor.parser.appendNodesToRedactor)
/** Write log if something goes wrong */
.catch(function(error) {
cEditor.core.log('Error while parsing content: %o', 'warn', error);
});
};
/**
* Parses string to nodeList
* @param string inputString
* @return Primise -> nodeList
*/
parser.getNodesFromString = function (inputString) {
return Promise.resolve().then(function() {
var contentHolder = document.createElement('div');
contentHolder.innerHTML = inputString;
/**
* Returning childNodes will include:
* - Elements (html-tags),
* - Texts (empty-spaces or non-wrapped strings )
* - Comments and other
*/
return contentHolder.childNodes;
});
};
/**
* Appends nodes to the redactor
* @param nodeList nodes - list for nodes to append
*/
parser.appendNodesToRedactor = function(nodes) {
/**
* Sequence of one-by-one nodes appending
* Uses to save blocks order after async-handler
*/
var nodeSequence = Promise.resolve();
for (var index = 0; index < nodes.length ; index++ ) {
/** Add node to sequence at specified index */
cEditor.parser.appendNodeAtIndex(nodeSequence, nodes, index);
}
};
/**
* Append node at specified index
*/
parser.appendNodeAtIndex = function (nodeSequence, nodes, index) {
/** We need to append node to sequence */
nodeSequence
/** first, get node async-aware */
.then(function() {
return cEditor.parser.getNodeAsync(nodes , index);
})
/**
* second, compose editor-block from node
* and append it to redactor
*/
.then(function(node){
var block = cEditor.parser.createBlockByDomNode(node);
if ( cEditor.core.isDomNode(block) ) {
block.contentEditable = "true";
/** Mark node as redactor block*/
block.classList.add('ce-block');
/** Append block to the redactor */
cEditor.nodes.redactor.appendChild(block);
/** Save block to the cEditor.state array */
cEditor.state.blocks.push(block);
return block;
}
return null;
})
.then(cEditor.ui.addBlockHandlers)
/** Log if something wrong with node */
.catch(function(error) {
cEditor.core.log('Node skipped while parsing because %o', 'warn', error);
});
};
/**
* Asynchronously returns node from nodeList by index
* @return Promise to node
*/
parser.getNodeAsync = function (nodeList, index) {
return Promise.resolve().then(function() {
return nodeList.item(index);
});
};
/**
* Creates editor block by DOM node
*
* First-level blocks (see cEditor.settings.blockTags) saves as-is,
* other wrapps with <p>-tag
*
* @param DOMnode node
* @return First-level node (paragraph)
*/
parser.createBlockByDomNode = function (node) {
/** First level nodes already appears as blocks */
if ( cEditor.parser.isFirstLevelBlock(node) ){
/** Save plugin type in data-type */
node = this.storeBlockType(node);
return node;
}
/** Other nodes wraps into parent block (paragraph-tag) */
var parentBlock,
nodeContent = node.textContent.trim(),
isPlainTextNode = node.nodeType != cEditor.core.nodeTypes.TAG;
/** Skip empty textNodes with space-symbols */
if (isPlainTextNode && !nodeContent.length) return null;
/** Make <p> tag */
parentBlock = cEditor.draw.block('P');
if (isPlainTextNode){
parentBlock.textContent = nodeContent.replace(/(\s){2,}/, '$1'); // remove double spaces
} else {
parentBlock.appendChild(node);
}
/** Save plugin type in data-type */
parentBlock = this.storeBlockType(parentBlock);
return parentBlock;
};
/**
* It's a crutch
* - - - - - - -
* We need block type stored as data-attr
* Now supports only simple blocks : P, HEADER, QUOTE, CODE
* Remove it after updating parser module for the block-oriented structure:
* - each block must have stored type
* @param {Element} node
*/
parser.storeBlockType = function (node) {
switch (node.tagName) {
case 'P' : node.dataset.tool = 'paragraph'; break;
case 'H1':
case 'H2':
case 'H3':
case 'H4':
case 'H5':
case 'H6': node.dataset.tool = 'header'; break;
case 'BLOCKQUOTE': node.dataset.tool = 'quote'; break;
case 'CODE': node.dataset.tool = 'code'; break;
}
return node;
};
/**
* Check DOM node for display style: separated block or child-view
*/
parser.isFirstLevelBlock = function (node) {
return node.nodeType == cEditor.core.nodeTypes.TAG &&
node.classList.contains(cEditor.ui.className.BLOCK_CLASSNAME);
};
return parser;
})({});
parser.init();
codex.parser = parser;
module.exports = parser;

171
modules/renderer.js Normal file
View file

@ -0,0 +1,171 @@
var codex = require('../editor');
var renderer = (function(renderer) {
/**
* Asyncronously parses input JSON to redactor blocks
*/
renderer.makeBlocksFromData = function () {
/**
* If redactor is empty, add first paragraph to start writing
*/
if (!codex.state.blocks.items.length) {
codex.ui.addInitialBlock();
return;
}
Promise.resolve()
/** First, get JSON from state */
.then(function() {
return codex.state.blocks;
})
/** Then, start to iterate they */
.then(codex.renderer.appendBlocks)
/** Write log if something goes wrong */
.catch(function(error) {
codex.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 */
codex.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 codex.renderer.getNodeAsync(blocks , index);
})
/**
* second, compose editor-block from JSON object
*/
.then(codex.renderer.createBlockFromData)
/**
* now insert block to redactor
*/
.then(function(blockData){
/**
* blockData has 'block', 'type' and 'stretched' information
*/
codex.content.insertBlock(blockData);
/** Pass created block to next step */
return blockData.block;
})
/** Log if something wrong with node */
.catch(function(error) {
codex.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 blocksList[index];
});
};
/**
* Creates editor block by JSON-data
*
* @uses render method of each plugin
*
* @param {object} blockData looks like
* { header : {
* text: '',
* type: 'H3', ...
* }
* }
* @return {object} with type and Element
*/
renderer.createBlockFromData = function (blockData) {
/** New parser */
var pluginName = blockData.type;
/** Get first key of object that stores plugin name */
// for (var pluginName in blockData) break;
/** Check for plugin existance */
if (!codex.tools[pluginName]) {
throw Error(`Plugin «${pluginName}» not found`);
}
/** Check for plugin having render method */
if (typeof codex.tools[pluginName].render != 'function') {
throw Error(`Plugin «${pluginName}» must have «render» method`);
}
/** New Parser */
var block = codex.tools[pluginName].render(blockData.data);
/** Fire the render method with data */
// var block = codex.tools[pluginName].render(blockData[pluginName]);
/** is first-level block stretched */
var stretched = codex.tools[pluginName].isStretched || false;
/** Retrun type and block */
return {
type : pluginName,
block : block,
stretched : stretched
};
};
return renderer;
})({});
codex.renderer = renderer;
module.exports = renderer;

111
modules/saver.js Normal file
View file

@ -0,0 +1,111 @@
var codex = require('../editor');
var saver = (function(saver) {
/**
* Saves blocks
* @private
*/
saver.saveBlocks = function () {
/** Save html content of redactor to memory */
codex.state.html = codex.nodes.redactor.innerHTML;
/** Empty jsonOutput state */
codex.state.jsonOutput = [];
Promise.resolve()
.then(function() {
return codex.nodes.redactor.childNodes;
})
/** Making a sequence from separate blocks */
.then(codex.saver.makeQueue)
.then(function() {
// codex.nodes.textarea.innerHTML = codex.state.html;
})
.catch( function(error) {
console.log('Something happend');
});
};
saver.makeQueue = function(blocks) {
var queue = Promise.resolve();
for(var index = 0; index < blocks.length; index++) {
/** Add node to sequence at specified index */
codex.saver.getBlockData(queue, blocks, index);
}
};
/** Gets every block and makes From Data */
saver.getBlockData = function(queue, blocks, index) {
queue.then(function() {
return codex.saver.getNodeAsync(blocks, index);
})
.then(codex.saver.makeFormDataFromBlocks);
};
/**
* Asynchronously returns block data from blocksList by index
* @return Promise to node
*/
saver.getNodeAsync = function (blocksList, index) {
return Promise.resolve().then(function() {
return blocksList[index];
});
};
saver.makeFormDataFromBlocks = function(block) {
var pluginName = block.dataset.tool;
/** Check for plugin existance */
if (!codex.tools[pluginName]) {
throw Error(`Plugin «${pluginName}» not found`);
}
/** Check for plugin having render method */
if (typeof codex.tools[pluginName].save != 'function') {
throw Error(`Plugin «${pluginName}» must have save method`);
}
/** Result saver */
var blockContent = block.childNodes[0],
pluginsContent = blockContent.childNodes[0],
savedData = codex.tools[pluginName].save(pluginsContent),
output;
output = {
type: pluginName,
data: savedData
};
/** Marks Blocks that will be in main page */
output.cover = block.classList.contains(codex.ui.className.BLOCK_IN_FEED_MODE);
codex.state.jsonOutput.push(output);
};
return saver;
})({});
codex.saver = saver;
module.exports = saver;

485
modules/toolbar/inline.js Normal file
View file

@ -0,0 +1,485 @@
var codex = require('../../editor');
var inline = (function(inline) {
inline.init = function() {
};
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 selectedText = this.getSelectionText(),
toolbar = codex.nodes.inlineToolbar.wrapper,
buttons = codex.nodes.inlineToolbar.buttons;
if (selectedText.length > 0) {
/** Move toolbar and open */
codex.toolbar.inline.move();
/** Open inline toolbar */
toolbar.classList.add('opened');
/** show buttons of inline toolbar */
codex.toolbar.inline.showButtons();
}
};
/**
* @protected
*
* Closes inline toolbar
*/
inline.close = function() {
var toolbar = codex.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 = codex.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 */
codex.toolbar.inline.closeButtons();
codex.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' : codex.toolbar.inline.createLinkAction(event, type); break;
default : codex.toolbar.inline.defaultToolAction(type); break;
}
/**
* highlight buttons
* after making some action
*/
codex.nodes.inlineToolbar.buttons.childNodes.forEach(codex.toolbar.inline.hightlight);
};
/**
* @private
*
* Saving wrappers offset in DOM
*/
inline.getWrappersOffset = function() {
var wrapper = codex.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];
x = rect.left;
y = rect.top;
}
}
}
return { x: x, y: y };
};
/**
* @private
*
* Returns selected text as String
* @returns {string}
*/
inline.getSelectionText = function getSelectionText(){
var selectedText = "";
if (window.getSelection){ // all modern browsers and IE9+
selectedText = window.getSelection().toString();
}
return selectedText;
};
/** Opens buttons block */
inline.showButtons = function() {
var buttons = codex.nodes.inlineToolbar.buttons;
buttons.classList.add('opened');
codex.toolbar.inline.buttonsOpened = true;
/** highlight buttons */
codex.nodes.inlineToolbar.buttons.childNodes.forEach(codex.toolbar.inline.hightlight);
};
/** Makes buttons disappear */
inline.closeButtons = function() {
var buttons = codex.nodes.inlineToolbar.buttons;
buttons.classList.remove('opened');
codex.toolbar.inline.buttonsOpened = false;
};
/** Open buttons defined action if exist */
inline.showActions = function() {
var action = codex.nodes.inlineToolbar.actions;
action.classList.add('opened');
codex.toolbar.inline.actionsOpened = true;
};
/** Close actions block */
inline.closeAction = function() {
var action = codex.nodes.inlineToolbar.actions;
action.innerHTML = '';
action.classList.remove('opened');
codex.toolbar.inline.actionsOpened = false;
};
/** Action for link creation or for setting anchor */
inline.createLinkAction = function(event, type) {
var isActive = this.isLinkActive();
var editable = codex.content.currentNode,
storedSelection = codex.toolbar.inline.storedSelection;
if (isActive) {
var selection = window.getSelection(),
anchorNode = selection.anchorNode;
storedSelection = codex.toolbar.inline.saveSelection(editable);
/**
* 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
*/
codex.toolbar.inline.restoreSelection(editable, storedSelection);
codex.toolbar.inline.defaultToolAction('unlink');
} else {
/** Create input and close buttons */
var action = codex.draw.inputForLink();
codex.nodes.inlineToolbar.actions.appendChild(action);
codex.toolbar.inline.closeButtons();
codex.toolbar.inline.showActions();
storedSelection = codex.toolbar.inline.saveSelection(editable);
/**
* 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 */
action.addEventListener('keydown', function(event){
if (event.keyCode == codex.core.keys.ENTER) {
codex.toolbar.inline.restoreSelection(editable, storedSelection);
codex.toolbar.inline.setAnchor(action.value);
/**
* Preventing events that will be able to happen
*/
event.preventDefault();
event.stopImmediatePropagation();
codex.toolbar.inline.clearRange();
}
}, false);
}
};
inline.isLinkActive = function() {
var isActive = false;
codex.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 */
codex.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)) {
codex.toolbar.inline.setButtonHighlighted(tool);
} else {
codex.toolbar.inline.removeButtonsHighLight(tool);
}
/**
*
* hightlight for anchors
*/
var selection = window.getSelection(),
tag = selection.anchorNode.parentNode;
if (tag.tagName == 'A' && dataType == 'link') {
codex.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;
})({});
inline.init();
module.exports = inline;

249
modules/toolbar/settings.js Normal file
View file

@ -0,0 +1,249 @@
var codex = require('../../editor');
var settings = (function(settings) {
settings.init = function() {
require('../content');
};
settings.opened = false;
settings.setting = null;
settings.actions = null;
settings.cover = null;
/**
* Append and open settings
*/
settings.open = function(toolType){
/**
* Append settings content
* It's stored in tool.settings
*/
if (!codex.tools[toolType] || !codex.core.isDomNode(codex.tools[toolType].settings) ) {
codex.core.log(`Plugin «${toolType}» has no settings`, 'warn');
// codex.nodes.pluginSettings.innerHTML = `Плагин «${toolType}» не имеет настроек`;
} else {
codex.nodes.pluginSettings.appendChild(codex.tools[toolType].settings);
}
var currentBlock = codex.content.currentNode;
/** Open settings block */
codex.nodes.blockSettings.classList.add('opened');
codex.toolbar.settings.addDefaultSettings();
this.opened = true;
};
/**
* Close and clear settings
*/
settings.close = function(){
codex.nodes.blockSettings.classList.remove('opened');
codex.nodes.pluginSettings.innerHTML = '';
this.opened = false;
};
/**
* @param {string} toolType - plugin type
*/
settings.toggle = function( toolType ){
if ( !this.opened ){
this.open(toolType);
} else {
this.close();
}
};
/**
* This function adds default core settings
*/
settings.addDefaultSettings = function() {
/** list of default settings */
var feedModeToggler;
/** Clear block and append initialized settings */
codex.nodes.defaultSettings.innerHTML = '';
/** Init all default setting buttons */
feedModeToggler = codex.toolbar.settings.makeFeedModeToggler();
/**
* Fill defaultSettings
*/
/**
* Button that enables/disables Feed-mode
* Feed-mode means that block will be showed in articles-feed like cover
*/
codex.nodes.defaultSettings.appendChild(feedModeToggler);
};
/**
* Cover setting.
* This tune highlights block, so that it may be used for showing target block on main page
* Draw different setting when block is marked for main page
* If TRUE, then we show button that removes this selection
* Also defined setting "Click" events will be listened and have separate callbacks
*
* @return {Element} node/button that we place in default settings block
*/
settings.makeFeedModeToggler = function() {
var isFeedModeActivated = codex.toolbar.settings.isFeedModeActivated(),
setting,
data;
if (!isFeedModeActivated) {
data = {
innerHTML : '<i class="ce-icon-newspaper"></i>Вывести в ленте'
};
} else {
data = {
innerHTML : '<i class="ce-icon-newspaper"></i>Не выводить в ленте'
};
}
setting = codex.draw.node('DIV', codex.ui.className.SETTINGS_ITEM, data);
setting.addEventListener('click', codex.toolbar.settings.updateFeedMode, false);
return setting;
};
/**
* Updates Feed-mode
*/
settings.updateFeedMode = function() {
var currentNode = codex.content.currentNode;
currentNode.classList.toggle(codex.ui.className.BLOCK_IN_FEED_MODE);
codex.toolbar.settings.close();
};
settings.isFeedModeActivated = function() {
var currentBlock = codex.content.currentNode;
if (currentBlock) {
return currentBlock.classList.contains(codex.ui.className.BLOCK_IN_FEED_MODE);
} else {
return false;
}
};
/**
* Here we will draw buttons and add listeners to components
*/
settings.makeRemoveBlockButton = function() {
var removeBlockWrapper = codex.draw.node('SPAN', 'ce-toolbar__remove-btn', {}),
settingButton = codex.draw.node('SPAN', 'ce-toolbar__remove-setting', { innerHTML : '<i class="ce-icon-trash"></i>' }),
actionWrapper = codex.draw.node('DIV', 'ce-toolbar__remove-confirmation', {}),
confirmAction = codex.draw.node('DIV', 'ce-toolbar__remove-confirm', { textContent : 'Удалить блок' }),
cancelAction = codex.draw.node('DIV', 'ce-toolbar__remove-cancel', { textContent : 'Отменить удаление' });
settingButton.addEventListener('click', codex.toolbar.settings.removeButtonClicked, false);
confirmAction.addEventListener('click', codex.toolbar.settings.confirmRemovingRequest, false);
cancelAction.addEventListener('click', codex.toolbar.settings.cancelRemovingRequest, false);
actionWrapper.appendChild(confirmAction);
actionWrapper.appendChild(cancelAction);
removeBlockWrapper.appendChild(settingButton);
removeBlockWrapper.appendChild(actionWrapper);
/** Save setting */
codex.toolbar.settings.setting = settingButton;
codex.toolbar.settings.actions = actionWrapper;
return removeBlockWrapper;
};
settings.removeButtonClicked = function() {
var action = codex.toolbar.settings.actions;
if (action.classList.contains('opened')) {
codex.toolbar.settings.hideRemoveActions();
} else {
codex.toolbar.settings.showRemoveActions();
}
codex.toolbar.toolbox.close();
codex.toolbar.settings.close();
};
settings.cancelRemovingRequest = function() {
codex.toolbar.settings.actions.classList.remove('opened');
};
settings.confirmRemovingRequest = function() {
var currentBlock = codex.content.currentNode,
firstLevelBlocksCount;
currentBlock.remove();
firstLevelBlocksCount = codex.nodes.redactor.childNodes.length;
/**
* If all blocks are removed
*/
if (firstLevelBlocksCount === 0) {
/** update currentNode variable */
codex.content.currentNode = null;
/** Inserting new empty initial block */
codex.ui.addInitialBlock();
}
codex.ui.saveInputs();
codex.toolbar.close();
};
settings.showRemoveActions = function() {
codex.toolbar.settings.actions.classList.add('opened');
};
settings.hideRemoveActions = function() {
codex.toolbar.settings.actions.classList.remove('opened');
};
return settings;
})({});
settings.init();
module.exports = settings;

104
modules/toolbar/toolbar.js Normal file
View file

@ -0,0 +1,104 @@
var codex = require('../../editor');
var toolbar = (function(toolbar) {
toolbar.init = function() {
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 (){
codex.nodes.toolbar.classList.add('opened');
this.opened = true;
};
/**
* @protected
*/
toolbar.close = function(){
codex.nodes.toolbar.classList.remove('opened');
this.opened = false;
this.current = null;
for (var button in codex.nodes.toolbarButtons){
codex.nodes.toolbarButtons[button].classList.remove('selected');
}
/** Close toolbox when toolbar is not displayed */
codex.toolbar.toolbox.close();
codex.toolbar.settings.close();
};
toolbar.toggle = function(){
if ( !this.opened ){
this.open();
} else {
this.close();
}
};
toolbar.hidePlusButton = function() {
codex.nodes.plusButton.classList.add('hide');
};
toolbar.showPlusButton = function() {
codex.nodes.plusButton.classList.remove('hide');
};
/**
* Moving toolbar to the specified node
*/
toolbar.move = function() {
/** Close Toolbox when we move toolbar */
codex.toolbar.toolbox.close();
if (!codex.content.currentNode) {
return;
}
var toolbarHeight = codex.nodes.toolbar.clientHeight || codex.toolbar.defaultToolbarHeight,
newYCoordinate = codex.content.currentNode.offsetTop - (codex.toolbar.defaultToolbarHeight / 2) + codex.toolbar.defaultOffset;
codex.nodes.toolbar.style.transform = `translate3D(0, ${Math.floor(newYCoordinate)}px, 0)`;
/** Close trash actions */
codex.toolbar.settings.hideRemoveActions();
};
return toolbar;
})({});
toolbar.init();
codex.toolbar = toolbar;
module.exports = toolbar;

152
modules/toolbar/toolbox.js Normal file
View file

@ -0,0 +1,152 @@
var codex = require('../../editor');
var toolbox = (function(toolbox) {
toolbox.init = function() {
require('./toolbar');
};
toolbox.opened = false;
/** Shows toolbox */
toolbox.open = function() {
/** Close setting if toolbox is opened */
if (codex.toolbar.settings.opened) {
codex.toolbar.settings.close();
}
/** display toolbox */
codex.nodes.toolbox.classList.add('opened');
/** Animate plus button */
codex.nodes.plusButton.classList.add('clicked');
/** toolbox state */
codex.toolbar.toolbox.opened = true;
};
/** Closes toolbox */
toolbox.close = function() {
/** Makes toolbox disapear */
codex.nodes.toolbox.classList.remove('opened');
/** Rotate plus button */
codex.nodes.plusButton.classList.remove('clicked');
/** toolbox state */
codex.toolbar.toolbox.opened = false;
};
toolbox.leaf = function(){
var currentTool = codex.toolbar.current,
tools = Object.keys(codex.tools),
barButtons = codex.nodes.toolbarButtons,
nextToolIndex,
toolToSelect;
if ( !currentTool ) {
/** Get first tool from object*/
for (toolToSelect in barButtons) break;
} else {
nextToolIndex = tools.indexOf(currentTool) + 1;
if ( nextToolIndex == tools.length) nextToolIndex = 0;
toolToSelect = tools[nextToolIndex];
}
for (var button in barButtons) barButtons[button].classList.remove('selected');
barButtons[toolToSelect].classList.add('selected');
codex.toolbar.current = toolToSelect;
};
/**
* Transforming selected node type into selected toolbar element type
* @param {event} event
*/
toolbox.toolClicked = function() {
/**
* UNREPLACEBLE_TOOLS this types of tools are forbidden to replace even they are empty
*/
var UNREPLACEBLE_TOOLS = ['image', 'link', 'list', 'instagram', 'twitter'],
tool = codex.tools[codex.toolbar.current],
workingNode = codex.content.currentNode,
currentInputIndex = codex.caret.inputIndex,
newBlockContent,
appendCallback,
blockData;
/** Make block from plugin */
newBlockContent = tool.make();
/** 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 */
codex.content.switchBlock(workingNode, newBlockContent, tool.type);
} else {
/** Insert new Block from plugin */
codex.content.insertBlock(blockData);
/** increase input index */
currentInputIndex++;
}
/** Fire tool append callback */
appendCallback = tool.appendCallback;
if (appendCallback && typeof appendCallback == 'function') {
appendCallback.call(event);
}
setTimeout(function() {
/** Set caret to current block */
codex.caret.setToBlock(currentInputIndex);
}, 10);
/**
* Changing current Node
*/
codex.content.workingNodeChanged();
/**
* Move toolbar when node is changed
*/
codex.toolbar.move();
};
return toolbox;
})({});
toolbox.init();
module.exports = toolbox;

10
modules/tools.js Normal file
View file

@ -0,0 +1,10 @@
var codex = require('../editor');
var tools = (function(tools) {
return tools;
})({});
codex.tools = tools;
module.exports = tools;

101
modules/transport.js Normal file
View file

@ -0,0 +1,101 @@
var codex = require('../editor');
var transport = (function(transport){
transport.input = null;
/**
* @property {Object} arguments - keep plugin settings and defined callbacks
*/
transport.arguments = null;
transport.prepare = function(){
var input = document.createElement('INPUT');
input.type = 'file';
input.addEventListener('change', codex.transport.fileSelected);
codex.transport.input = input;
};
/** Clear input when files is uploaded */
transport.clearInput = function() {
/** Remove old input */
this.input = null;
/** Prepare new one */
this.prepare();
};
/**
* Callback for file selection
*/
transport.fileSelected = function(event){
var input = this,
files = input.files,
filesLength = files.length,
formdData = new FormData(),
file,
i;
formdData.append('files', files[0], files[0].name);
codex.transport.ajax({
data : formdData,
beforeSend : codex.transport.arguments.beforeSend,
success : codex.transport.arguments.success,
error : codex.transport.arguments.error
});
};
/**
* Use plugin callbacks
* @protected
*/
transport.selectAndUpload = function (args) {
this.arguments = args;
this.input.click();
};
/**
* Ajax requests module
*/
transport.ajax = function(params){
var xhr = new XMLHttpRequest(),
beforeSend = typeof params.beforeSend == 'function' ? params.beforeSend : function(){},
success = typeof params.success == 'function' ? params.success : function(){},
error = typeof params.error == 'function' ? params.error : function(){};
beforeSend();
xhr.open('POST', codex.settings.uploadImagesUrl, true);
xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
xhr.onload = function () {
if (xhr.status === 200) {
success(xhr.responseText);
} else {
console.log("request error: %o", xhr);
error();
}
};
xhr.send(params.data);
this.clearInput();
};
return transport;
})({});
codex.transport = transport;
module.exports = transport;

383
modules/ui.js Normal file
View file

@ -0,0 +1,383 @@
var codex = require('../editor');
var ui = (function(ui){
/**
* 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} - highlights covered blocks
*/
BLOCK_IN_FEED_MODE : 'ce-block--feed-mode',
/**
* @const {String} - for all default settings
*/
SETTINGS_ITEM : 'ce-settings__item'
};
/**
* @protected
*
* Making main interface
*/
ui.make = function () {
var wrapper,
toolbar,
toolbarContent,
inlineToolbar,
redactor,
ceBlock,
notifications,
blockButtons,
blockSettings,
showSettingsButton,
showTrashButton,
toolbox,
plusButton;
/** Make editor wrapper */
wrapper = codex.draw.wrapper();
/** Append editor wrapper after initial textarea */
codex.core.insertAfter(codex.nodes.textarea, wrapper);
/** Append block with notifications to the document */
notifications = codex.draw.alertsHolder();
codex.nodes.notifications = document.body.appendChild(notifications);
/** Make toolbar and content-editable redactor */
toolbar = codex.draw.toolbar();
toolbarContent = codex.draw.toolbarContent();
inlineToolbar = codex.draw.inlineToolbar();
plusButton = codex.draw.plusButton();
showSettingsButton = codex.draw.settingsButton();
showTrashButton = codex.toolbar.settings.makeRemoveBlockButton();
blockSettings = codex.draw.blockSettings();
blockButtons = codex.draw.blockButtons();
toolbox = codex.draw.toolbox();
redactor = codex.draw.redactor();
/** settings */
var defaultSettings = codex.draw.defaultSettings(),
pluginSettings = codex.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);
/** Append plus button */
toolbarContent.appendChild(plusButton);
/** Appending toolbar tools */
toolbarContent.appendChild(toolbox);
/** Appending first-level block buttons */
toolbar.appendChild(blockButtons);
/** Append toolbarContent to toolbar */
toolbar.appendChild(toolbarContent);
wrapper.appendChild(toolbar);
wrapper.appendChild(redactor);
/** Save created ui-elements to static nodes state */
codex.nodes.wrapper = wrapper;
codex.nodes.toolbar = toolbar;
codex.nodes.plusButton = plusButton;
codex.nodes.toolbox = toolbox;
codex.nodes.blockSettings = blockSettings;
codex.nodes.pluginSettings = pluginSettings;
codex.nodes.defaultSettings = defaultSettings;
codex.nodes.showSettingsButton = showSettingsButton;
codex.nodes.showTrashButton = showTrashButton;
codex.nodes.redactor = redactor;
codex.ui.makeInlineToolbar(inlineToolbar);
/** fill in default settings */
codex.toolbar.settings.addDefaultSettings();
};
ui.makeInlineToolbar = function(container) {
/** Append to redactor new inline block */
codex.nodes.inlineToolbar.wrapper = container;
/** Draw toolbar buttons */
codex.nodes.inlineToolbar.buttons = codex.draw.inlineToolbarButtons();
/** Buttons action or settings */
codex.nodes.inlineToolbar.actions = codex.draw.inlineToolbarActions();
/** Append to inline toolbar buttons as part of it */
codex.nodes.inlineToolbar.wrapper.appendChild(codex.nodes.inlineToolbar.buttons);
codex.nodes.inlineToolbar.wrapper.appendChild(codex.nodes.inlineToolbar.actions);
codex.nodes.wrapper.appendChild(codex.nodes.inlineToolbar.wrapper);
};
/**
* @private
* Append tools passed in codex.tools
*/
ui.addTools = function () {
var tool,
tool_button;
for(var name in codex.settings.tools) {
tool = codex.settings.tools[name];
codex.tools[name] = tool;;
}
/** Make toolbar buttons */
for (var name in codex.tools){
tool = codex.tools[name];
if (tool.displayInToolbox == false) {
continue;
}
if (!tool.iconClassname) {
codex.core.log('Toolbar icon classname missed. Tool %o skipped', 'warn', name);
continue;
}
if (typeof tool.make != 'function') {
codex.core.log('make method missed. Tool %o skipped', 'warn', name);
continue;
}
/**
* if tools is for toolbox
*/
tool_button = codex.draw.toolbarButton(name, tool.iconClassname);
codex.nodes.toolbox.appendChild(tool_button);
/** Save tools to static nodes */
codex.nodes.toolbarButtons[name] = tool_button;
}
/**
* Add inline toolbar tools
*/
codex.ui.addInlineToolbarTools();
};
ui.addInlineToolbarTools = function() {
var tools = {
bold: {
icon : 'ce-icon-bold',
command : 'bold'
},
italic: {
icon : 'ce-icon-italic',
command : 'italic'
},
underline: {
icon : 'ce-icon-underline',
command : 'underline'
},
link: {
icon : 'ce-icon-link',
command : 'createLink',
}
};
var toolButton,
tool;
for(var name in tools) {
tool = tools[name];
toolButton = codex.draw.toolbarButtonInline(name, tool.icon);
codex.nodes.inlineToolbar.buttons.appendChild(toolButton);
/**
* Add callbacks to this buttons
*/
codex.ui.setInlineToolbarButtonBehaviour(toolButton, tool.command);
}
};
/**
* @private
* Bind editor UI events
*/
ui.bindEvents = function () {
codex.core.log('ui.bindEvents fired', 'info');
window.addEventListener('error', function (errorMsg, url, lineNumber) {
codex.notifications.errorThrown(errorMsg, event);
}, false );
/** All keydowns on Document */
codex.nodes.redactor.addEventListener('keydown', codex.callback.globalKeydown, false );
/** All keydowns on Document */
document.addEventListener('keyup', codex.callback.globalKeyup, false );
/**
* Mouse click to radactor
*/
codex.nodes.redactor.addEventListener('click', codex.callback.redactorClicked, false );
/**
* Clicks to the Plus button
*/
codex.nodes.plusButton.addEventListener('click', codex.callback.plusButtonClicked, false);
/**
* Clicks to SETTINGS button in toolbar
*/
codex.nodes.showSettingsButton.addEventListener('click', codex.callback.showSettingsButtonClicked, false );
/**
* @deprecated ( but now in use for syncronization );
* Any redactor changes: keyboard input, mouse cut/paste, drag-n-drop text
*/
codex.nodes.redactor.addEventListener('input', codex.callback.redactorInputEvent, false );
/** Bind click listeners on toolbar buttons */
for (var button in codex.nodes.toolbarButtons){
codex.nodes.toolbarButtons[button].addEventListener('click', codex.callback.toolbarButtonClicked, false);
}
};
/**
* Initialize plugins before using
* Ex. Load scripts or call some internal methods
*/
ui.preparePlugins = function() {
for(var tool in codex.tools) {
if (typeof codex.tools[tool].prepare != 'function')
continue;
codex.tools[tool].prepare();
}
},
ui.addBlockHandlers = function(block) {
if (!block) return;
/**
* Block keydowns
*/
block.addEventListener('keydown', function(event) {
codex.callback.blockKeydown(event, block);
}, false);
/**
* Pasting content from another source
*/
block.addEventListener('paste', function (event) {
codex.callback.blockPaste(event);
}, false);
block.addEventListener('mouseup', function(){
codex.toolbar.inline.show();
}, false);
};
/** getting all contenteditable elements */
ui.saveInputs = function() {
var redactor = codex.nodes.redactor,
elements = [];
/** Save all inputs in global variable state */
codex.state.inputs = redactor.querySelectorAll('[contenteditable], input');
};
/**
* Adds first initial block on empty redactor
*/
ui.addInitialBlock = function(){
var initialBlockType = codex.settings.initialBlockPlugin,
initialBlock;
if ( !codex.tools[initialBlockType] ){
codex.core.log('Plugin %o was not implemented and can\'t be used as initial block', 'warn', initialBlockType);
return;
}
initialBlock = codex.tools[initialBlockType].render();
initialBlock.setAttribute('data-placeholder', 'Write your story...');
codex.content.insertBlock({
type : initialBlockType,
block : initialBlock
});
codex.content.workingNodeChanged(initialBlock);
};
ui.setInlineToolbarButtonBehaviour = function(button, type) {
button.addEventListener('mousedown', function(event) {
codex.toolbar.inline.toolClicked(event, type);
}, false);
};
return ui;
})({});
codex.ui = ui;
module.exports = codex;

16
package.json Normal file
View file

@ -0,0 +1,16 @@
{
"name": "webpack",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"css-loader": "^0.26.1",
"lodash": "^4.17.2",
"style-loader": "^0.13.1"
}
}

View file

@ -1,7 +0,0 @@
.tool-code {
display: block;
font-family: 'monospace', 'monaco', 'consolas', 'courier';
line-height: 1.5em;
background: #f8f8fd !important;
color: #304f77;
}

View file

@ -1,74 +0,0 @@
/**
* Code Plugin\
* Creates code tag and adds content to this tag
*/
var codeTool = {
baseClass : "tool-code",
/**
* Make initial header block
* @param {object} JSON with block data
* @return {Element} element to append
*/
make : function (data) {
var tag = document.createElement('code');
tag.classList += codeTool.baseClass;
if (data && data.text) {
tag.innerHTML = data.text;
}
tag.contentEditable = true;
return tag;
},
/**
* Method to render HTML block from JSON
*/
render : function (data) {
return codeTool.make(data);
},
/**
* Method to extract JSON data from HTML block
*/
save : function (blockContent){
var block = blockContent[0];
json = {
type : 'code',
data : {
text : null,
}
};
json.data.text = block.innerHTML;
return json;
},
};
/**
* Now plugin is ready.
* Add it to redactor tools
*/
cEditor.tools.code = {
type : 'code',
iconClassname : 'ce-icon-code',
make : codeTool.make,
appendCallback : null,
settings : null,
render : codeTool.render,
save : codeTool.save
};

View file

@ -1,19 +0,0 @@
/**
* Plugin styles
*/
/** H e a d e r - settings */
.ce_plugin_header--settings{
white-space: nowrap;
/*padding-right: 10px; */
}
.ce_plugin_header--select_button{
display: inline-block;
margin-left: 40px;
border-bottom: 1px solid #475588;
color: #6881e6;
cursor: pointer;
}
.ce_plugin_header--select_button:hover{
border-bottom-color: #687da5;
color: #8da8dc;
}

View file

@ -1,182 +0,0 @@
/**
* Example of making plugin
* H e a d e r
*/
var headerTool = {
/**
* Make initial header block
* @param {object} JSON with block data
* @return {Element} element to append
*/
make : function (data) {
var availableTypes = ['H1', 'H2', 'H3', 'H4', 'H5', 'H6'],
tag;
if (data && data.type && availableTypes.includes(data.type)) {
tag = document.createElement( data.type );
/**
* Save header type in data-attr.
* We need it in save method to extract type from HTML to JSON
*/
tag.dataset.headerData = data.type;
} else {
tag = document.createElement( 'H2' );
}
if (data && data.text) {
tag.textContent = data.text;
}
tag.contentEditable = true;
return tag;
},
/**
* Method to render HTML block from JSON
*/
render : function (data) {
return headerTool.make(data);
},
/**
* Method to extract JSON data from HTML block
*/
save : function (blockContent) {
var block = blockContent[0],
json = {
type : 'header',
data : {
type : null,
text : null,
}
};
json.data.type = block.dataset.headerData;
json.data.text = block.textContent;
return json;
},
/**
* Block appending callback
*/
appendCallback : function (argument) {
console.log('header appended...');
},
/**
* Settings panel content
* - - - - - - - - - - - - -
* | настройки H1 H2 H3 |
* - - - - - - - - - - - - -
* @return {Element} element contains all settings
*/
makeSettings : function () {
var holder = document.createElement('DIV'),
caption = document.createElement('SPAN'),
types = {
H2: 'Заголовок раздела',
H3: 'Подзаголовок',
H4: 'Заголовок 3-его уровня'
},
selectTypeButton;
/** Add holder classname */
holder.className = 'ce_plugin_header--settings';
/** Add settings helper caption */
caption.textContent = 'Настройки заголовка';
caption.className = 'ce_plugin_header--caption';
holder.appendChild(caption);
/** Now add type selectors */
for (var type in types){
selectTypeButton = document.createElement('SPAN');
selectTypeButton.textContent = types[type];
selectTypeButton.className = 'ce_plugin_header--select_button';
this.addSelectTypeClickListener(selectTypeButton, type);
holder.appendChild(selectTypeButton);
}
return holder;
},
/**
* Binds click event to passed button
*/
addSelectTypeClickListener : function (el, type) {
el.addEventListener('click', function () {
headerTool.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 = cEditor.content.currentNode;
/** Making new header */
new_header = document.createElement(type);
new_header.innerHTML = old_header.innerHTML;
new_header.contentEditable = true;
cEditor.content.switchBlock(old_header, new_header, 'header');
/** Add listeners for Arrow keys*/
cEditor.ui.addBlockHandlers(new_header);
/** Close settings after replacing */
cEditor.toolbar.settings.close();
},
};
/**
* Now plugin is ready.
* Add it to redactor tools
*/
cEditor.tools.header = {
type : 'header',
iconClassname : 'ce-icon-header',
make : headerTool.make,
appendCallback : headerTool.appendCallback,
settings : headerTool.makeSettings(),
render : headerTool.render,
save : headerTool.save
};

View file

@ -1,99 +0,0 @@
/**
* Image plugin for codex-editor
* @author CodeX Team <team@ifmo.su>
*
* @version 0.0.1
*/
.ce-plugin-image__holder{
position: relative;
background: #FEFEFE;
border: 2px dashed #E8EBF5;
border-radius: 55px;
margin: 30px 0;
padding: 30px 110px 30px 40px;
}
.ce-plugin-image__holder input{
border: 0;
background: transparent;
outline: none;
-webkit-appearance: none;
font-size: 1.2em;
color: #A5ABBC;
}
/* Placeholder color for Chrome */
.ce-plugin-image__holder input::-webkit-input-placeholder {
color: #A5ABBC;
}
/* Placeholder color for IE 10+ */
.ce-plugin-image__holder input:-ms-input-placeholder {
color: #A5ABBC;
}
/* Placeholder color for Firefox 19+ */
.ce-plugin-image__holder input::-moz-placeholder {
color: #A5ABBC;
opacity: 1;
}
.ce-plugin-image__button{
position: absolute;
z-index: 2;
right: 40px;
cursor: pointer;
font-family: "codex_editor";
font-size: 1.5em;
color: #8990AA;
}
.ce-plugin-image__button:hover{
color: #393F52;
}
.ce-plugin-image__wrapper {
margin : 3em 0;
}
.ce-plugin-image__uploaded--centered {
max-width: 700px;
display:block;
margin: 0 auto;
}
.ce-plugin-image__uploaded--stretched {
/*width: 939px;*/
}
.ce-plugin-image--stretch {
margin: 0 !important;
max-width: 100% !important;
}
.ce-plugin-image__caption {
margin-top: 1em;
text-align: center;
color: #898a8c;
}
.ce-plugin-image--caption:empty::before {
content : 'Подпись';
font-weight: normal;
color: #a1a5b3;;
opacity: 1;
transition: opacity 200ms ease;
}
.ce-plugin-image--caption:focus::before {
opacity: .1;
}
/** Settings */
.ce_plugin_image--settings{
white-space: nowrap;
/*padding-right: 10px; */
}
.ce_plugin_image--select_button{
display: inline-block;
margin-left: 40px;
border-bottom: 1px solid #475588;
color: #6881e6;
cursor: pointer;
}
.ce_plugin_image--select_button:hover{
border-bottom-color: #687da5;
color: #8da8dc;
}

View file

@ -1,353 +0,0 @@
/**
* Image plugin for codex-editor
* @author CodeX Team <team@ifmo.su>
*
* @version 0.0.2
*/
var ceImage = {
elementClasses : {
uploadedImage : {
centered : 'ce-plugin-image__uploaded--centered',
stretched : 'ce-plugin-image__uploaded--stretched',
},
stretch : 'ce-plugin-image--stretch',
imageCaption : 'ce-plugin-image__caption',
imageWrapper : 'ce-plugin-image__wrapper',
formHolder : 'ce-plugin-image__holder',
uploadButton : 'ce-plugin-image__button',
},
/** Default path to redactors images */
path : '/upload/redactor_images/',
make : function ( data ) {
/**
* If we can't find image or we've got some problems with image path, we show plugin uploader
*/
if (!data || !data.file.url) {
holder = ceImage.ui.formView();
} else {
if ( !data.isStretch) {
holder = ceImage.ui.centeredImage(data);
} else {
holder = ceImage.ui.stretchedImage(data);
}
}
return holder;
},
/**
* Settings panel content
* @return {Element} element contains all settings
*/
makeSettings : function () {
var holder = document.createElement('DIV'),
caption = document.createElement('SPAN'),
types = {
centered : 'По центру',
stretched : 'На всю ширину',
},
selectTypeButton;
/** Add holder classname */
holder.className = 'ce_plugin_image--settings';
/** Add settings helper caption */
caption.textContent = 'Настройки плагина';
caption.className = 'ce_plugin_image--caption';
holder.appendChild(caption);
/** Now add type selectors */
for (var type in types){
selectTypeButton = document.createElement('SPAN');
selectTypeButton.textContent = types[type];
selectTypeButton.className = 'ce_plugin_image--select_button';
this.addSelectTypeClickListener(selectTypeButton, type);
holder.appendChild(selectTypeButton);
}
return holder;
},
addSelectTypeClickListener : function(el, type) {
el.addEventListener('click', function() {
ceImage.selectTypeClicked(type);
}, false);
},
selectTypeClicked : function(type) {
var current = cEditor.content.currentNode;
if (type == 'stretched') {
current.classList.add(ceImage.elementClasses.stretch);
} else if (type == 'centered') {
current.classList.remove(ceImage.elementClasses.stretch);
}
},
render : function( data ) {
return this.make(data);
},
save : function ( block ) {
var data = block[0],
image = data.querySelector('.' + ceImage.elementClasses.uploadedImage.centered) ||
data.querySelector('.' + ceImage.elementClasses.uploadedImage.stretched),
caption = data.querySelector('.' + ceImage.elementClasses.imageCaption);
var json = {
type : 'image',
data : {
background : false,
border : false,
isStrech : data.dataset.stretched,
file : {
url : image.src,
bigUrl : null,
width : image.width,
height : image.height,
additionalData :null,
},
caption : caption.textContent,
cover : null,
}
};
return json;
},
uploadButtonClicked : function(event) {
var success = ceImage.photoUploadingCallbacks.success,
error = ceImage.photoUploadingCallbacks.error;
/** Define callbacks */
cEditor.transport.selectAndUpload({
success,
error,
});
}
};
ceImage.ui = {
holder : function(){
var element = document.createElement('DIV');
element.classList.add(ceImage.elementClasses.formHolder);
return element;
},
input : function(){
var input = document.createElement('INPUT');
return input;
},
uploadButton : function(){
var button = document.createElement('SPAN');
button.classList.add(ceImage.elementClasses.uploadButton);
button.innerHTML = '<i class="ce-icon-picture"></i>';
return button;
},
/**
* @param {string} source - file path
* @param {string} style - css class
* @return {object} image - document IMG tag
*/
image : function(source, style) {
var image = document.createElement('IMG');
image.classList.add(style);
image.src = source;
return image;
},
wrapper : function() {
var div = document.createElement('div');
div.classList.add(ceImage.elementClasses.imageWrapper);
return div;
},
caption : function() {
var div = document.createElement('div');
div.classList.add(ceImage.elementClasses.imageCaption);
div.contentEditable = true;
return div;
},
/**
* Draws form for image upload
*/
formView : function() {
var holder = ceImage.ui.holder(),
uploadButton = ceImage.ui.uploadButton(),
input = ceImage.ui.input();
input.placeholder = 'Paste image URL or file';
holder.appendChild(uploadButton);
holder.appendChild(input);
uploadButton.addEventListener('click', ceImage.uploadButtonClicked, false );
return holder;
},
/**
* wraps image and caption
* @param {object} data - image information
* @return wrapped block with image and caption
*/
centeredImage : function(data) {
var file = data.file.url,
text = data.caption,
type = data.type,
image = ceImage.ui.image(file, ceImage.elementClasses.uploadedImage.centered),
caption = ceImage.ui.caption(),
wrapper = ceImage.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
* @param {object} data - image information
* @return stretched image
*/
stretchedImage : function(data) {
var file = data.file.url,
text = data.caption,
type = data.type,
image = ceImage.ui.image(file, ceImage.elementClasses.uploadedImage.stretched),
caption = ceImage.ui.caption(),
wrapper = ceImage.ui.wrapper();
caption.textContent = text;
wrapper.dataset.stretched = 'true',
/** Appeding to the wrapper */
wrapper.appendChild(image);
wrapper.appendChild(caption);
return wrapper;
}
};
ceImage.photoUploadingCallbacks = {
/** Photo was uploaded successfully */
success : function(result) {
var parsed = JSON.parse(result),
data,
image;
/**
* Preparing {Object} data to draw an image
* @uses ceImage.make method
*/
data = {
background : false,
border : false,
isStrech : false,
file : {
url : ceImage.path + 'o_' + parsed.filename,
bigUrl : null,
width : null,
height : null,
additionalData : null,
},
caption : '',
cover : null,
};
image = ceImage.make(data);
/** Replace form to image */
var form = cEditor.content.currentNode.querySelector('.' + ceImage.elementClasses.formHolder);
cEditor.content.switchBlock(form, image, 'image');
},
/** Error callback. Sends notification to user that something happend or plugin doesn't supports method */
error : function(result) {
console.log('Choosen file is not image or image is corrupted');
cEditor.notifications.errorThrown();
},
}
/**
* Add plugin it to redactor tools
*/
cEditor.tools.image = {
type : 'image',
iconClassname : 'ce-icon-picture',
make : ceImage.make,
settings : ceImage.makeSettings(),
render : ceImage.render,
save : ceImage.save
};

View file

@ -1,75 +0,0 @@
.clearfix:after {
visibility: hidden;
display: block;
font-size: 0;
content: " ";
clear: both;
height: 0;
}
.ceditor-tool-link-input {
outline: none;
border: 0;
width: 100%;
background: transparent;
font-size: 1em;
padding: 8px 25px;
transition: background 200ms;
border-left: 3px solid #65d8b3;
}
.tool-link-panel {
position: relative;
margin: 5px 0;
background: #f8f7ef;
border: 1px solid transparent;
padding: 25px 30px;
}
.tool-link-image {
float:right;
width: 75px;
border-radius: 3px;
}
.tool-link-title {
display: block;
width: 340px;
margin-bottom: 4px;
line-height: 1.2em;
font-size: 20px;
font-weight: 700;
color: #000;
}
.tool-link-description {
display: block;
margin-top: 10px;
font-size: 14px;
color: #000;
}
.tool-link-link {
width: 360px;
font-size: 10px;
margin-bottom: 4px;
letter-spacing: 1px;
overflow: hidden;
text-transform: uppercase;
text-decoration: none;
color: rgba(165,156,86,.8);
}
.tool-link-loader {
background: url("loading.gif") !important;
opacity: 0.1;
}
.tool-link-error {
background: rgb(255, 241, 241);
color: #bf4747;
}
.tool-link-error .ceditor-tool-link-input {
border-left-color: #d86b6b
}

View file

@ -1,321 +0,0 @@
/**
* Created by nostr on 29.06.16.
*/
/**
* Link tool plugin
*/
var linkTool = {
defaultText : 'Insert link here ...',
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"
},
/**
* Make initial header block
* @param {object} JSON with block data
* @return {Element} element to append
*/
makeNewBlock : function (data) {
var wrapper = linkTool.ui.mainBlock(),
tag = linkTool.ui.input();
linkTool.currentInput = tag;
wrapper.appendChild(tag);
/**
* Bind callbacks
**/
tag.addEventListener('paste', linkTool.blockPasteCallback, false);
tag.addEventListener('keydown', linkTool.blockKeyDownCallback, false);
return wrapper;
},
/**
* Method to render HTML block from JSON
*/
render : function (json) {
var block = linkTool.ui.mainBlock(),
tag = linkTool.ui.make(json);
block.appendChild(tag);
return block;
},
/**
* Method to extract JSON data from HTML block
*/
save : function (blockContent){
var linkElement = linkTool.elementClasses.link;
var block = blockContent[0],
json = {
type : 'link',
data : {
fullLink : block.querySelector("." + linkElement).href,
shortLink : block.querySelector("." + linkElement).textContent,
image : block.querySelector("." + linkTool.elementClasses.image).src,
title : block.querySelector("." + linkTool.elementClasses.title).textContent,
description : block.querySelector("." + linkTool.elementClasses.description).textContent
}
};
return json;
},
blockPasteCallback : function (event) {
var clipboardData = event.clipboardData || window.clipboardData,
pastedData = clipboardData.getData('Text'),
block = event.target.parentNode;
linkTool.renderLink(pastedData, block);
event.stopPropagation();
},
blockKeyDownCallback : function (event) {
var inputTag = event.target,
block = inputTag.parentNode,
url;
if ( block.classList.contains(linkTool.elementClasses.error) )
{
block.classList.remove(linkTool.elementClasses.error);
}
if (event.keyCode == linkTool.ENTER_KEY) {
url = inputTag.value;
linkTool.renderLink(url, block);
event.preventDefault();
}
},
/**
* @todo move request-url to accepted settings
*/
renderLink : function (url, block) {
Promise.resolve()
.then(function () {
return linkTool.urlify(url);
})
.then(function (url) {
/* Show loader gif **/
block.classList.add(linkTool.elementClasses.loader);
return fetch('/editor/parseLink?url=' + encodeURI(url));
})
.then(function (response) {
if (response.status == "200"){
return response.json();
} else {
return Error("Invalid response status: %o", response);
}
})
.then(function (json) {
linkTool.composeLinkPreview(json, block);
})
.catch(function(error) {
/* Hide loader gif **/
block.classList.remove(linkTool.elementClasses.loader);
block.classList.add(linkTool.elementClasses.error);
cEditor.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 = linkTool.ui.make(json);
linkTool.currentInput.remove();
currentBlock.appendChild(previewBlock);
currentBlock.classList.remove(linkTool.elementClasses.loader);
}
};
linkTool.ui = {
make : function (json) {
var wrapper = this.wrapper(),
siteImage = this.image(json.image, linkTool.elementClasses.image),
siteTitle = this.title(json.title),
siteDescription = this.description(json.description),
siteLink = this.link(json.linkUrl, json.linkText);
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 += "ceditor-tool-link";
return wrapper;
},
input : function () {
var inputTag = document.createElement('input');
inputTag.classList += "ceditor-tool-link-input";
inputTag.placeholder = linkTool.defaultText;
inputTag.contentEditable = false;
return inputTag;
},
wrapper : function () {
var wrapper = document.createElement('div');
wrapper.className += 'tool-link-panel clearfix';
return wrapper;
},
image : function (imageSrc, imageClass) {
var imageTag = document.createElement('img');
imageTag.classList += imageClass;
imageTag.setAttribute('src', imageSrc);
return imageTag;
},
link : function (linkUrl, linkText) {
var linkTag = document.createElement('a');
linkTag.classList += linkTool.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", linkTool.elementClasses.title);
titleTag.innerHTML = titleText;
return titleTag;
},
description : function (descriptionText) {
var descriptionTag = document.createElement('div');
descriptionTag.classList.add("tool-link-content", linkTool.elementClasses.description);
descriptionTag.innerHTML = descriptionText;
return descriptionTag;
},
};
cEditor.tools.link = {
type : 'link',
iconClassname : 'ce-icon-link',
make : linkTool.makeNewBlock,
appendCallback : linkTool.appendCallback,
render : linkTool.render,
// settings : linkTool.makeSettings(),
save : linkTool.save
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 329 B

View file

@ -1,15 +0,0 @@
.ce_plugin_list--settings{
white-space: nowrap;
}
.ce_plugin_list--select_button{
display: inline-block;
margin-left: 40px;
border-bottom: 1px solid #475588;
color: #6881e6;
cursor: pointer;
}
.ce_plugin_list--select_button:hover{
border-bottom-color: #687da5;
color: #8da8dc;
}

View file

@ -1,179 +0,0 @@
/**
* Code Plugin\
* Creates code tag and adds content to this tag
*/
var listTool = {
baseClass : "tool-list",
elementClasses : {
li : "tool-list-li"
},
/**
* Make initial header block
* @param {object} JSON with block data
* @return {Element} element to append
*/
make : function () {
var tag = listTool.ui.make(),
li = listTool.ui.block("li", "tool-link-li");
var br = document.createElement("br");
li.appendChild(br);
tag.appendChild(li);
return tag;
},
/**
* Method to render HTML block from JSON
*/
render : function (data) {
var type = data.type == 'ordered' ? 'OL' : 'UL',
tag = listTool.ui.make(type);
data.items.forEach(function (element, index, array) {
var newLi = listTool.ui.block("li", listTool.elementClasses.li);
newLi.innerHTML = element;
tag.dataset.type = data.type;
tag.appendChild(newLi);
});
return tag;
},
/**
* Method to extract JSON data from HTML block
*/
save : function (blockContent){
var block = blockContent[0],
json = {
type : 'list',
data : {
type : null,
items : [],
}
};
for(var index = 0; index < block.childNodes.length; index++)
json.data.items[index] = block.childNodes[index].textContent;
json.data.type = block.dataset.type;
return json;
},
makeSettings : function(data) {
var holder = document.createElement('DIV'),
caption = document.createElement('SPAN'),
selectTypeButton;
/** Add holder classname */
holder.className = 'ce_plugin_list--settings';
/** Add settings helper caption */
caption.textContent = 'Настройки списков';
caption.className = 'ce_plugin_list--caption';
var orderedButton = listTool.ui.button("ordered"),
unorderedButton = listTool.ui.button("unordered");
orderedButton.addEventListener('click', function (event) {
listTool.changeBlockStyle(event, 'ol');
cEditor.toolbar.settings.close();
});
unorderedButton.addEventListener('click', function (event) {
listTool.changeBlockStyle(event, 'ul');
cEditor.toolbar.settings.close();
});
holder.appendChild(caption);
holder.appendChild(orderedButton);
holder.appendChild(unorderedButton);
return holder;
},
changeBlockStyle : function (event, blockType) {
var currentBlock = cEditor.content.currentNode,
newEditable = listTool.ui.make(blockType),
oldEditable = currentBlock.querySelector("[contenteditable]");
newEditable.dataset.type = blockType;
newEditable.innerHTML = oldEditable.innerHTML;
currentBlock.appendChild(newEditable);
oldEditable.remove();
},
};
listTool.ui = {
make : function (blockType) {
var wrapper = this.block(blockType || 'UL', listTool.baseClass);
wrapper.contentEditable = true;
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 : 'Обычный список',
ordered : 'Нумерованный список'
},
button = document.createElement('SPAN');
button.textContent = types[buttonType];
button.className = 'ce_plugin_list--select_button';
return button;
}
};
/**
* Now plugin is ready.
* Add it to redactor tools
*/
cEditor.tools.list = {
type : 'list',
iconClassname : 'ce-icon-list-bullet',
make : listTool.make,
appendCallback : null,
settings : listTool.makeSettings(),
render : listTool.render,
save : listTool.save
};

View file

@ -1,69 +0,0 @@
/**
* Paragraph Plugin\
* Creates P tag and adds content to this tag
*/
var paragraphTool = {
/**
* Make initial header block
* @param {object} JSON with block data
* @return {Element} element to append
*/
make : function (data) {
var tag = document.createElement('DIV');
if (data && data.text) {
tag.innerHTML = data.text;
}
tag.contentEditable = true;
return tag;
},
/**
* Method to render HTML block from JSON
*/
render : function (data) {
return paragraphTool.make(data);
},
/**
* Method to extract JSON data from HTML block
*/
save : function (blockContent){
var block = blockContent[0],
json = {
type : 'paragraph',
data : {
text : null,
}
};
json.data.text = block.innerHTML;
return json;
},
};
/**
* Now plugin is ready.
* Add it to redactor tools
*/
cEditor.tools.paragraph = {
type : 'paragraph',
iconClassname : 'ce-icon-paragraph',
make : paragraphTool.make,
appendCallback : null,
settings : null,
render : paragraphTool.render,
save : paragraphTool.save
};

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -1,172 +0,0 @@
/** Quote - settings */
.ce_plugin_quote--settings{
white-space: nowrap;
/*padding-right: 10px; */
}
.ce_plugin_quote--select_button{
display: inline-block;
margin-left: 40px;
border-bottom: 1px solid #475588;
color: #6881e6;
cursor: pointer;
}
.ce_plugin_quote--select_button:hover{
border-bottom-color: #687da5;
color: #8da8dc;
}
/** Quote Styles */
.quoteStyle-withCaption--author:empty {
direction: rtl;
}
.quoteStyle-withCaption--author {
margin: 0 100px 3em !important;
text-align: right;
font-size: 1.16em;
font-weight: bold;
color: #000;
line-height: 1.5em;
}
.quoteStyle-simple--text,
.quoteStyle-withCaption--blockquote,
.quoteStyle-withPhoto--quote {
font-size: 1.04em;
line-height: 1.9em;
}
.quoteStyle-simple--text {
padding: 1.2em 2.1em;
margin: 2.5em 2em;
background: #FBFBFB;
}
.quoteStyle-withCaption--blockquote
{
margin: 0 1em;
padding: 1.5em 2.0em !important;
}
.quoteStyle-withCaption--blockquote:focus,
.quoteStyle-withCaption--author:focus {
outline: none;
}
.quoteStyle-simple--text:empty::before,
.quoteStyle-withCaption--blockquote:empty::before,
.quoteStyle-withPhoto--quote:empty::before
{
content : 'Введите цитату';
color: #818BA1;
opacity: 0.7;
transition: opacity 200ms ease;
}
.quoteStyle-withCaption--author:empty::before
{
content : 'Введите имя автора';
font-weight: normal;
color: #a1a5b3;;
opacity: 1;
transition: opacity 200ms ease;
}
.quoteStyle-withPhoto--author:empty::before {
content : 'Введите имя автора';
color: #000;
opacity: .8;
transition: opacity 200ms ease;
}
.quoteStyle-simple--text:focus::before,
.quoteStyle-withCaption--blockquote:focus::before,
.quoteStyle-withPhoto--quote:focus::before
{
opacity: .1;
}
.quoteStyle-withCaption--author:focus::before,
.quoteStyle-withPhoto--author:focus::before
{
opacity: .1;
}
/** Quote with photo */
.ce_redactor .quoteStyle-withPhoto--wrapper {
margin: 0;
padding: 4.5em 3.6em !important;
}
.quoteStyle-withPhoto--photo {
width: 70px;
height: 70px;
overflow: hidden;
text-align: center;
line-height: 70px;
border-radius: 50%;
float: left;
margin-right: 25px;
border: 2px dashed #D7E2EE;
}
.quoteStyle-withPhoto--photo:hover {
cursor: pointer;
background: #F0F3F6;
}
.quoteStyle-withPhoto--photo:hover::after {
display: block;
}
.authorsPhoto {
height: 110%;
vertical-align: top;
}
.authorsPhoto-wrapper {
border: 0 !important;
}
.quoteStyle-withPhoto--photo .ce-icon-picture {
font-family: "codex_editor";
font-size: 1.5em;
color: #d5dbea;
}
.quoteStyle-withPhoto--author {
font-size: 1.36em;
font-weight: bold;
color: #000;
outline: none;
overflow: hidden;
margin-bottom: 8px;
}
.quoteStyle-withPhoto--job {
font-size: 1.04em;
color: #818BA1;
outline: none;
overflow: hidden;
}
.quoteStyle-withPhoto--job:empty::before {
content : 'Введите должность автора';
color: #A5ABBC;
opacity: 1;
transition: opacity 200ms ease;
}
.quoteStyle-withPhoto--job:focus::before {
opacity: .1;
}
.quoteStyle-withPhoto--quote {
margin-top: 33px;
line-height: 1.82em;
outline: none;
overflow: hidden;
}
.quoteStyle-withPhoto--authorWrapper {
margin-top: 10px;
}

View file

@ -1,403 +0,0 @@
/**
* Codex Team
* @author Khaydarov Murod
*/
var quoteTools = {
/** Default path to redactors images */
path : '/upload/redactor_images/',
/**
* Make Quote from JSON datasets
*/
makeBlockToAppend : function(data) {
var tag;
if (data && data.type) {
switch (data.type) {
case 'simple':
tag = quoteTools.makeSimpleQuote(data);
break;
case 'withCaption':
tag = quoteTools.makeQuoteWithCaption(data);
break;
case 'withPhoto':
tag = quoteTools.makeQuoteWithPhoto(data);
break;
}
} else {
tag = document.createElement('BLOCKQUOTE');
tag.contentEditable = 'true';
tag.dataset.quoteStyle = 'simple';
tag.classList.add(quoteTools.styles.quoteText);
tag.classList.add(quoteTools.styles.simple.text);
}
return tag;
},
render : function(data) {
return quoteTools.makeBlockToAppend(data);
},
save : function(blockContent) {
/**
* Extracts JSON quote data from HTML block
* @param {Text} text, {Text} author, {Object} photo
*/
var block = blockContent[0],
parsedblock = quoteTools.parseBlockQuote(block);
json = {
type : 'quote',
data : {
style : parsedblock.style,
text : parsedblock.text,
author : parsedblock.author,
job : parsedblock.job,
photo : parsedblock.photo,
}
};
return json;
},
makeSettings : function(data) {
var holder = document.createElement('DIV'),
caption = document.createElement('SPAN'),
types = {
simple : 'Простая цитата',
withCaption : 'Цитата с подписью',
withPhoto : 'Цитата с фото и ФИО'
},
selectTypeButton;
/** Add holder classname */
holder.className = quoteTools.styles.settings.holder,
/** Add settings helper caption */
caption.textContent = 'Настройки цитат';
caption.className = quoteTools.styles.settings.caption;
holder.appendChild(caption);
/** Now add type selectors */
for (var type in types){
selectTypeButton = document.createElement('SPAN');
selectTypeButton.textContent = types[type];
selectTypeButton.className = quoteTools.styles.settings.buttons;
var quoteStyle = quoteTools.selectTypeQuoteStyle(type);
quoteTools.addSelectTypeClickListener(selectTypeButton, quoteStyle);
holder.appendChild(selectTypeButton);
}
return holder;
},
selectTypeQuoteStyle : function(type) {
/**
* Choose Quote style to replace
*/
switch (type) {
case 'simple':
quoteStyleFunction = quoteTools.makeSimpleQuote;
break;
case 'withCaption':
quoteStyleFunction = quoteTools.makeQuoteWithCaption;
break;
case 'withPhoto':
quoteStyleFunction = quoteTools.makeQuoteWithPhoto;
break;
}
return quoteStyleFunction;
},
addSelectTypeClickListener : function(el, quoteStyle) {
el.addEventListener('click', function () {
/**
* Parsing currentNode to JSON.
*/
var parsedOldQuote = quoteTools.parseBlockQuote(),
newStyledQuote = quoteStyle(parsedOldQuote);
var wrapper = cEditor.content.composeNewBlock(newStyledQuote, 'quote');
wrapper.appendChild(newStyledQuote);
cEditor.content.replaceBlock(cEditor.content.currentNode, wrapper, 'quote');
/** Close settings after replacing */
cEditor.toolbar.settings.close();
}, false);
},
makeSimpleQuote : function(data) {
var wrapper = quoteTools.ui.makeBlock('BLOCKQUOTE', [quoteTools.styles.simple.text, quoteTools.styles.quoteText]);
wrapper.innerHTML = data.text || '';
wrapper.dataset.quoteStyle = 'simple';
wrapper.contentEditable = 'true';
return wrapper;
},
makeQuoteWithCaption : function(data) {
var wrapper = quoteTools.ui.blockquote(),
text = quoteTools.ui.makeBlock('DIV', [quoteTools.styles.withCaption.blockquote, quoteTools.styles.quoteText]),
author = quoteTools.ui.makeBlock('DIV', [quoteTools.styles.withCaption.author, quoteTools.styles.quoteAuthor]);
/* make text block ontentEditable */
text.contentEditable = 'true';
text.innerHTML = data.text;
/* make Author contentEditable */
author.contentEditable = 'true';
author.textContent = data.author;
/* Appending created components */
wrapper.dataset.quoteStyle = 'withCaption';
wrapper.appendChild(text);
wrapper.appendChild(author);
return wrapper;
},
makeQuoteWithPhoto : function(data) {
var wrapper = quoteTools.ui.blockquote();
photo = quoteTools.ui.makeBlock('DIV', [quoteTools.styles.withPhoto.photo]),
author = quoteTools.ui.makeBlock('DIV', [quoteTools.styles.withPhoto.author, quoteTools.styles.quoteAuthor]),
job = quoteTools.ui.makeBlock('DIV', [quoteTools.styles.withPhoto.job, quoteTools.styles.authorsJob]),
quote = quoteTools.ui.makeBlock('DIV', [quoteTools.styles.withPhoto.quote, quoteTools.styles.quoteText])
/* Default Image src */
var icon = quoteTools.ui.makeBlock('SPAN', ['ce-icon-picture']);
photo.appendChild(icon);
photo.addEventListener('click', quoteTools.fileUploadClicked, false);
/* make author block contentEditable */
author.contentEditable = 'true';
author.textContent = data.author;
/* Author's position and job */
job.contentEditable = 'true';
job.textContent = data.job;
var authorsWrapper = quoteTools.ui.makeBlock('DIV', [quoteTools.styles.withPhoto.authorHolder]);
authorsWrapper.appendChild(author);
authorsWrapper.appendChild(job);
/* make quote text contentEditable */
quote.contentEditable = 'true';
quote.innerHTML = data.text;
wrapper.classList.add(quoteTools.styles.withPhoto.wrapper);
wrapper.dataset.quoteStyle = 'withPhoto';
wrapper.appendChild(photo);
wrapper.appendChild(authorsWrapper);
wrapper.appendChild(quote);
return wrapper;
},
parseBlockQuote : function(block) {
var currentNode = block || cEditor.content.currentNode,
photo = currentNode.getElementsByTagName('img')[0],
author = currentNode.querySelector('.' + quoteTools.styles.quoteAuthor),
job = currentNode.querySelector('.' + quoteTools.styles.authorsJob),
quote ;
/** Simple quote text placed in Blockquote tag*/
if ( currentNode.dataset.quoteStyle == 'simple' )
quote = currentNode.textContent;
else
quote = currentNode.querySelector('.' + quoteTools.styles.quoteText).textContent;
if (job)
job = job.textContent;
if (author)
author = author.textContent;
if (photo)
photo = photo.src;
var data = {
style : currentNode.dataset.quoteStyle,
text : quote,
author : author,
job : job,
photo : photo,
};
return data;
},
fileUploadClicked : function() {
var success = quoteTools.photoUploadingCallbacks.success,
error = quoteTools.photoUploadingCallbacks.error;
cEditor.transport.selectAndUpload({
success,
error,
});
}
};
quoteTools.styles = {
quoteText : 'ce_quote--text',
quoteAuthor : 'ce_quote--author',
authorsJob : 'ce_quote--job',
authorsPhoto : 'authorsPhoto',
authorsPhotoWrapper : 'authorsPhoto-wrapper',
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 : 'ce_plugin_quote--settings',
caption : 'ce_plugin_quote--caption',
buttons : 'ce_plugin_quote--select_button',
},
}
quoteTools.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;
}
};
quoteTools.photoUploadingCallbacks = {
/**
* Success callbacks for uploaded photo.
* Replace upload icon with uploaded photo
*/
success : function(result) {
var parsed = JSON.parse(result),
filename = parsed.filename,
uploadImageWrapper = cEditor.content.currentNode.querySelector('.' + quoteTools.styles.withPhoto.photo),
authorsPhoto = quoteTools.ui.img(quoteTools.styles.authorsPhoto);
authorsPhoto.src = quoteTools.path + 'b_' + filename;
/** Remove icon from image wrapper */
uploadImageWrapper.innerHTML = '';
/** Appending uploaded image */
uploadImageWrapper.classList.add(quoteTools.styles.authorsPhotoWrapper);
uploadImageWrapper.appendChild(authorsPhoto);
},
/** 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');
cEditor.notifications.errorThrown();
}
};
cEditor.tools.quote = {
type : 'quote',
iconClassname : 'ce-icon-quote',
make : quoteTools.makeBlockToAppend,
appendCallback : null,
settings : quoteTools.makeSettings(),
render : quoteTools.render,
save : quoteTools.save,
};

68
webpack.config.js Normal file
View file

@ -0,0 +1,68 @@
/**
* Webpack configuration
*
* @author Codex Team
* @copyright Khaydarov Murod
*/
'use strict';
const NODE_ENV = process.env.NODE_ENV || 'development';
var webpack = require('webpack');
var ExtractTextWebpackPlugin = require('extract-text-webpack-plugin');
module.exports = {
entry: {
codex: "./index"
},
output: {
filename: "./codex.editor.js",
library: ["codex", "editor"]
},
watch: true,
watchOptions: {
aggregateTimeOut: 100
},
devtool: NODE_ENV == 'development' ? "source-map" : null,
resolve : {
modulesDirectories : ['./node_modules', './modules'],
extensions : ['', '.js']
},
resolveLoader : {
modulesDirectories: ['./node_modules'],
moduleTemplates: ["*-webpack-loader", "*-web-loader", "*-loader", "*"],
extensions: ['', '.js']
},
plugins: [
new webpack.DefinePlugin({
NODE_ENV: JSON.stringify(NODE_ENV)
}),
new webpack.ProvidePlugin({
_ : 'lodash'
})
],
module : {
loaders : [{
test : /\.js$/,
exclude: /(node_modules)/,
loader : 'babel',
query: {
presets: [__dirname + '/node_modules/babel-preset-es2015']
}
},
{
test : /\.css$/,
exclude: /(node_modules)/,
loader: ExtractTextWebpackPlugin.extract('style-loader', 'css-loader')
}]
}
};