dante2-editor 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +33 -0
- data/Gemfile +25 -0
- data/Gemfile.lock +42 -0
- data/README.md +107 -0
- data/app/assets/index.html +55 -0
- data/app/assets/license.html +52 -0
- data/app/assets/options.html +57 -0
- data/app/assets/store.json +1 -0
- data/app/components/App.cjsx +1083 -0
- data/app/components/blocks/embed.cjsx +109 -0
- data/app/components/blocks/image.cjsx +316 -0
- data/app/components/blocks/placeholder.cjsx +60 -0
- data/app/components/blocks/video.cjsx +75 -0
- data/app/components/debug.cjsx +96 -0
- data/app/components/decorators/link.cjsx +61 -0
- data/app/components/popovers/addButton.cjsx +247 -0
- data/app/components/popovers/image.cjsx +160 -0
- data/app/components/popovers/link.cjsx +87 -0
- data/app/components/popovers/toolTip.cjsx +326 -0
- data/app/data/poc.js +15 -0
- data/app/demo.js +7 -0
- data/app/images/dante/media-loading-placeholder.png +0 -0
- data/app/images/site/dante-demo.png +0 -0
- data/app/images/site/dante-editor-logo.png +0 -0
- data/app/images/site/github-logo.png +0 -0
- data/app/initialize.js +4 -0
- data/app/model/index.js +194 -0
- data/app/styles/dante.scss +22 -0
- data/app/styles/dante/_animations.scss +54 -0
- data/app/styles/dante/_caption.scss +61 -0
- data/app/styles/dante/_debug.scss +124 -0
- data/app/styles/dante/_fonts.scss +17 -0
- data/app/styles/dante/_graf.scss +242 -0
- data/app/styles/dante/_icons.scss +62 -0
- data/app/styles/dante/_media.scss +39 -0
- data/app/styles/dante/_menu.scss +201 -0
- data/app/styles/dante/_needsorder.scss +209 -0
- data/app/styles/dante/_popover.scss +212 -0
- data/app/styles/dante/_post.scss +67 -0
- data/app/styles/dante/_scaffold.scss +24 -0
- data/app/styles/dante/_tooltip.scss +130 -0
- data/app/styles/dante/_utilities.scss +59 -0
- data/app/styles/dante/_variables.scss +96 -0
- data/app/styles/dante/blame.scss +246 -0
- data/app/styles/draft.css +297 -0
- data/app/styles/fonts/dante/dante.eot +0 -0
- data/app/styles/fonts/dante/dante.svg +18 -0
- data/app/styles/fonts/dante/dante.ttf +0 -0
- data/app/styles/fonts/dante/dante.woff +0 -0
- data/app/styles/fonts/dante/fontello.eot +0 -0
- data/app/styles/fonts/dante/fontello.svg +36 -0
- data/app/styles/fonts/dante/fontello.ttf +0 -0
- data/app/styles/fonts/dante/fontello.woff +0 -0
- data/app/styles/layout/layout.scss +64 -0
- data/app/styles/layout/normalize.css +375 -0
- data/app/styles/layout/scaffold.scss +8 -0
- data/app/styles/layout/tooltips.scss +216 -0
- data/app/utils/find_entities.coffee +20 -0
- data/app/utils/html2content.coffee +120 -0
- data/app/utils/logger.coffee +0 -0
- data/app/utils/save_content.coffee +63 -0
- data/app/utils/selection.js +53 -0
- data/config.ru +64 -0
- data/dante2.gemspec +19 -0
- data/docs/app.css +2 -0
- data/docs/app.css.map +1 -0
- data/docs/app.js +3 -0
- data/docs/app.js.map +1 -0
- data/docs/dante-vendors.js +28 -0
- data/docs/dante-vendors.js.map +1 -0
- data/docs/dante.css +2 -0
- data/docs/dante.css.map +1 -0
- data/docs/dante.js +4 -0
- data/docs/dante.js.map +1 -0
- data/docs/doc.html +57 -0
- data/docs/fonts/dante.eot +0 -0
- data/docs/fonts/dante.svg +18 -0
- data/docs/fonts/dante.ttf +0 -0
- data/docs/fonts/dante.woff +0 -0
- data/docs/fonts/fontello.eot +0 -0
- data/docs/fonts/fontello.svg +36 -0
- data/docs/fonts/fontello.ttf +0 -0
- data/docs/fonts/fontello.woff +0 -0
- data/docs/images/dante-editor-logo.png +0 -0
- data/docs/images/github-logo.png +0 -0
- data/docs/index.html +55 -0
- data/docs/license.html +52 -0
- data/lib/dante2-editor.rb +5 -0
- data/lib/dante2-editor/rails.rb +16 -0
- data/lib/dante2-editor/version.rb +5 -0
- data/package.json +61 -0
- data/rakefile +1 -0
- data/webpack.config.js +148 -0
- data/yarn.lock +4704 -0
- metadata +139 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: b78a4eed0a1ef6eeae30783c70c7292b850998e2
|
4
|
+
data.tar.gz: 5ec8a4832a950e7450e99f8bd38b108b8721153b
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: f608f000bc57b9e234cfd10f2f2de4e021f8c49267611bf841c636fdc9e63a9ff2a5e78599747cb4eac18dde96efed4addf0a00fc87b439e4d40186b2d63a817
|
7
|
+
data.tar.gz: fa07fc3b9af66474ee8f8fa069cf6c5071b08f06fbed561538bceef8a6106ad3b788c08e18b2cd7187eea6ad7cbc85cfbb4b2944ca5a481d30bb438a2617e6fb
|
data/.gitignore
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
# Numerous always-ignore extensions
|
2
|
+
*.diff
|
3
|
+
*.err
|
4
|
+
*.orig
|
5
|
+
*.log
|
6
|
+
*.rej
|
7
|
+
*.swo
|
8
|
+
*.swp
|
9
|
+
*.vi
|
10
|
+
*~
|
11
|
+
*.sass-cache
|
12
|
+
|
13
|
+
# OS or Editor folders
|
14
|
+
.DS_Store
|
15
|
+
.cache
|
16
|
+
.project
|
17
|
+
.settings
|
18
|
+
.tmproj
|
19
|
+
nbproject
|
20
|
+
Thumbs.db
|
21
|
+
|
22
|
+
# NPM packages folder.
|
23
|
+
node_modules/
|
24
|
+
|
25
|
+
# Brunch folder for temporary files.
|
26
|
+
tmp/
|
27
|
+
/pkg/
|
28
|
+
|
29
|
+
# Brunch output folder.
|
30
|
+
public/
|
31
|
+
|
32
|
+
# Bower stuff.
|
33
|
+
bower_components/
|
data/Gemfile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# If you have OpenSSL installed, we recommend updating
|
2
|
+
# the following line to use "https"
|
3
|
+
source 'http://rubygems.org'
|
4
|
+
|
5
|
+
#gem "therubyracer"
|
6
|
+
# gem "middleman", "~>3.1.4"
|
7
|
+
|
8
|
+
# Live-reloading plugin
|
9
|
+
# gem "middleman-livereload", "~> 3.1.0"
|
10
|
+
# gem 'middleman-gh-pages'
|
11
|
+
# For faster file watcher updates on Windows:
|
12
|
+
# gem "wdm", "~> 0.1.0", :platforms => [:mswin, :mingw]
|
13
|
+
# gem "github-markup"
|
14
|
+
# gem "redcarpet"
|
15
|
+
gem "sinatra"
|
16
|
+
gem 'sinatra-contrib'
|
17
|
+
gem "pry"
|
18
|
+
gem 'rack-cors', :require => 'rack/cors'
|
19
|
+
|
20
|
+
#gemspec
|
21
|
+
|
22
|
+
# Cross-templating language block fix for Ruby 1.8
|
23
|
+
#platforms :mri_18 do
|
24
|
+
# gem "ruby18_source_location"
|
25
|
+
#end
|
data/Gemfile.lock
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
GEM
|
2
|
+
remote: http://rubygems.org/
|
3
|
+
specs:
|
4
|
+
backports (3.6.8)
|
5
|
+
coderay (1.1.1)
|
6
|
+
method_source (0.8.2)
|
7
|
+
multi_json (1.12.1)
|
8
|
+
pry (0.10.4)
|
9
|
+
coderay (~> 1.1.0)
|
10
|
+
method_source (~> 0.8.1)
|
11
|
+
slop (~> 3.4)
|
12
|
+
rack (1.6.4)
|
13
|
+
rack-cors (0.4.0)
|
14
|
+
rack-protection (1.5.3)
|
15
|
+
rack
|
16
|
+
rack-test (0.6.3)
|
17
|
+
rack (>= 1.0)
|
18
|
+
sinatra (1.4.7)
|
19
|
+
rack (~> 1.5)
|
20
|
+
rack-protection (~> 1.4)
|
21
|
+
tilt (>= 1.3, < 3)
|
22
|
+
sinatra-contrib (1.4.7)
|
23
|
+
backports (>= 2.0)
|
24
|
+
multi_json
|
25
|
+
rack-protection
|
26
|
+
rack-test
|
27
|
+
sinatra (~> 1.4.0)
|
28
|
+
tilt (>= 1.3, < 3)
|
29
|
+
slop (3.6.0)
|
30
|
+
tilt (2.0.5)
|
31
|
+
|
32
|
+
PLATFORMS
|
33
|
+
ruby
|
34
|
+
|
35
|
+
DEPENDENCIES
|
36
|
+
pry
|
37
|
+
rack-cors
|
38
|
+
sinatra
|
39
|
+
sinatra-contrib
|
40
|
+
|
41
|
+
BUNDLED WITH
|
42
|
+
1.10.6
|
data/README.md
ADDED
@@ -0,0 +1,107 @@
|
|
1
|
+
# Dante II - The rematch
|
2
|
+
|
3
|
+
## just another medium clone built on top of DraftJs
|
4
|
+
|
5
|
+
> Dante II is a complete rewrite of [DanteEditor](https://michelson.github.io/Dante). This version is built on top of Facebook's Draft-Js and reaches all Dante's features with a shiny ultra mega super uber maintainable architecture.
|
6
|
+
|
7
|
+
See the demo at: [https://michelson.github.io/dante2/](https://michelson.github.io/dante2/)
|
8
|
+
|
9
|
+
## Why rewrite a new version of Dante?
|
10
|
+
|
11
|
+
The previous version of Dante relies a lot on DOM manipulation which causes a mix of presentation and logic. Even with their modular plugin system this condition suppose an sphagetti model to work with on every feature. The biggest problem with this approach is: if you want to make a change that affects the presentation of your users content, let's say you might want to change the default markup for paragraphs, you'll probably end updating all your content in your database, because dealing with "DOM only" suppose that you are going to save html into database, right ?
|
12
|
+
|
13
|
+
|
14
|
+
## A redesign was needed!
|
15
|
+
|
16
|
+
Draft-Js handles selection, ranges and markup blocks as a data layer contained in a structure known as editorState, with a clear separation on how rendering, styling and interaction works. So you save content data, not html. That's awesome because you can change the appearance of articles (styles & markup) without database changes.
|
17
|
+
|
18
|
+
In Draft every change provided from user input is stacked in this editorState building an history of changes, out of the box. This means that pasting, undo/redo and replace/insert blocks at certain selection points are basically calls to the DraftJs API that updates the editorState without DOM manipulation. Also, all the custom blocks are composed as React components!. So, this version have some dependencies which are included in source. DraftJs, React, Immutable. no Jquery.
|
19
|
+
|
20
|
+
**New Features:**
|
21
|
+
+ Improved undo/redo.
|
22
|
+
+ Save Content as a data JSON structure.
|
23
|
+
+ Load Content as a data JSON structure.
|
24
|
+
+ Handle image blocks on Copy/Paste and Drop.
|
25
|
+
+ Global storage lock to handle file uploads.
|
26
|
+
|
27
|
+
**Features**:
|
28
|
+
|
29
|
+
+ Image upload for paste html.
|
30
|
+
+ Image upload for legacy images on existent texts.
|
31
|
+
+ The medium (+) Tooltip to embed or upload media.
|
32
|
+
+ Tab navigation.
|
33
|
+
+ Pluggins are React components
|
34
|
+
|
35
|
+
**Embeds**:
|
36
|
+
|
37
|
+
+ Image Uploader with preview and caption option with a lock system.
|
38
|
+
+ Embed data for pasted link through OEmbed services.
|
39
|
+
+ Embed media information for pasted links through OEmbed services.
|
40
|
+
+ Add or remove tooltip buttons with ease with plugin system.
|
41
|
+
+ Add custom blocks many with custom options
|
42
|
+
|
43
|
+
|
44
|
+
## Usage
|
45
|
+
|
46
|
+
The interface to initialize is almost the Dante as the previous version.
|
47
|
+
|
48
|
+
|
49
|
+
```javascript
|
50
|
+
new Dante({
|
51
|
+
upload_url: "http://localhost:9292/uploads/new",
|
52
|
+
store_url: "http://localhost:3333/store.json",
|
53
|
+
el: "app"
|
54
|
+
})
|
55
|
+
|
56
|
+
```
|
57
|
+
|
58
|
+
### Options:
|
59
|
+
|
60
|
+
Many configuration options and plugin usage in the documentation page:
|
61
|
+
|
62
|
+
See [https://michelson.github.io/dante2/doc.html](https://michelson.github.io/dante2/doc.html)
|
63
|
+
|
64
|
+
|
65
|
+
### Installation for development
|
66
|
+
|
67
|
+
**node + webpack**
|
68
|
+
|
69
|
+
+ `npm install or yarn install`
|
70
|
+
|
71
|
+
|
72
|
+
#### Development:
|
73
|
+
|
74
|
+
+ `npm start` or `yarn start`
|
75
|
+
|
76
|
+
Then open http://localhost:8080
|
77
|
+
|
78
|
+
|
79
|
+
#### Build
|
80
|
+
|
81
|
+
+ `npm build` or `yarn build`
|
82
|
+
|
83
|
+
#### Upload test server (ruby, optional)
|
84
|
+
|
85
|
+
For development purposes we have a server, written in ruby, to handle file uploading
|
86
|
+
|
87
|
+
+ `bundle install`
|
88
|
+
|
89
|
+
+ `bundle exec rackup`
|
90
|
+
|
91
|
+
and open http://localhost:9292
|
92
|
+
|
93
|
+
|
94
|
+
### Ruby:
|
95
|
+
For rubists you can install this library as a gem file.
|
96
|
+
|
97
|
+
Just add the gem as dante2-editor
|
98
|
+
|
99
|
+
|
100
|
+
### Open source license
|
101
|
+
|
102
|
+
If you are creating an open source application under a license compatible with the [GNU GPL license v3](https://www.gnu.org/licenses/gpl-3.0.html), you may use Dante2 under the terms of the GPLv3.[Read more about Dante2's license](https://michelson.github.io/dante2/license.html).
|
103
|
+
|
104
|
+
|
105
|
+
### Alternatives
|
106
|
+
|
107
|
+
If you are looking for alternatives you can always use the [MIT licensed Dante (1)](https://michelson.github.io/Dante) or choose along others [medium clones](http://howtox.com/medium-editor-clones-in-js) or check out [many many awesome draft-js based editors](https://github.com/nikgraf/awesome-draft-js)
|
@@ -0,0 +1,55 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<meta charset="utf-8">
|
5
|
+
<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
|
6
|
+
<!-- WARNING: for iOS 7, remove the width=device-width and height=device-height attributes. See https://issues.apache.org/jira/browse/CB-4323 -->
|
7
|
+
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height, target-densitydpi=device-dpi" />
|
8
|
+
<title>Dante2</title>
|
9
|
+
</head>
|
10
|
+
<body>
|
11
|
+
|
12
|
+
<div id="header">
|
13
|
+
<div class="logo">
|
14
|
+
<div class="menu-buttons">
|
15
|
+
<a class="menu-button" target="_blank" href="https://michelson.github.io/dante2/doc.html">
|
16
|
+
Documentation
|
17
|
+
</a>
|
18
|
+
|
19
|
+
<a class="menu-button" target="_blank" href="https://michelson.github.io/dante2/license.html">
|
20
|
+
License
|
21
|
+
</a>
|
22
|
+
</div>
|
23
|
+
|
24
|
+
|
25
|
+
<img src=<%= require('images/site/dante-editor-logo.png') %> alt="dante editor" height="21">
|
26
|
+
<span>Dante Editor - 0.2.0 </span>
|
27
|
+
|
28
|
+
</div>
|
29
|
+
|
30
|
+
<a class="github tooltip-left" data-tooltip="Fork me on github" target="_blank" href="https://github.com/michelson/dante2">
|
31
|
+
<img src=<%= require('images/site/github-logo.png') %> alt="Fork me on github" height="28">
|
32
|
+
</a>
|
33
|
+
</div>
|
34
|
+
|
35
|
+
<div class="danteArticle"></div>
|
36
|
+
<div id="app"></div>
|
37
|
+
|
38
|
+
<script type="text/javascript">
|
39
|
+
document.addEventListener('DOMContentLoaded', function(){
|
40
|
+
editor = new Dante(
|
41
|
+
{
|
42
|
+
api_key: "86c28a410a104c8bb58848733c82f840",
|
43
|
+
el: "app",
|
44
|
+
content: window.PocData,
|
45
|
+
debug: true,
|
46
|
+
data_storage: {
|
47
|
+
url: "http://localhost:3333/store.json"
|
48
|
+
}
|
49
|
+
}
|
50
|
+
)
|
51
|
+
editor.render()
|
52
|
+
})
|
53
|
+
</script>
|
54
|
+
</body>
|
55
|
+
</html>
|
@@ -0,0 +1,52 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<meta charset="utf-8">
|
5
|
+
<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
|
6
|
+
<!-- WARNING: for iOS 7, remove the width=device-width and height=device-height attributes. See https://issues.apache.org/jira/browse/CB-4323 -->
|
7
|
+
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height, target-densitydpi=device-dpi" />
|
8
|
+
<title>Dante2</title>
|
9
|
+
</head>
|
10
|
+
<body>
|
11
|
+
|
12
|
+
|
13
|
+
<div id="header">
|
14
|
+
<div class="logo">
|
15
|
+
<div class="menu-buttons">
|
16
|
+
<a class="menu-button" target="_blank" href="https://michelson.github.io/dante2/doc.html">
|
17
|
+
Documentation
|
18
|
+
</a>
|
19
|
+
|
20
|
+
<a class="menu-button" target="_blank" href="https://michelson.github.io/dante2/license.html">
|
21
|
+
License
|
22
|
+
</a>
|
23
|
+
</div>
|
24
|
+
|
25
|
+
<img src='https://michelson.github.io/dante2/images/dante-editor-logo.png' alt="dante editor" height="21">
|
26
|
+
<span>Dante Editor - 0.2.0 </span>
|
27
|
+
|
28
|
+
</div>
|
29
|
+
|
30
|
+
<a class="github tooltip-left" data-tooltip="Fork me on github" target="_blank" href="https://github.com/michelson/dante2">
|
31
|
+
<img src='https://michelson.github.io/dante2/images/github-logo.png' alt="Fork me on github" height="28">
|
32
|
+
</a>
|
33
|
+
</div>
|
34
|
+
|
35
|
+
<div class="danteArticle"></div>
|
36
|
+
<div id="app"></div>
|
37
|
+
|
38
|
+
<script type="text/javascript">
|
39
|
+
document.addEventListener('DOMContentLoaded', function(){
|
40
|
+
editor = new Dante(
|
41
|
+
{
|
42
|
+
el: "app",
|
43
|
+
content: window.PocDataLicense,
|
44
|
+
debug: false,
|
45
|
+
read_only: true
|
46
|
+
}
|
47
|
+
)
|
48
|
+
editor.render()
|
49
|
+
})
|
50
|
+
</script>
|
51
|
+
</body>
|
52
|
+
</html>
|
@@ -0,0 +1,57 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<meta charset="utf-8">
|
5
|
+
<meta name="viewport" content="initial-scale=1.0, user-scalable=no" />
|
6
|
+
<!-- WARNING: for iOS 7, remove the width=device-width and height=device-height attributes. See https://issues.apache.org/jira/browse/CB-4323 -->
|
7
|
+
<meta name="viewport" content="user-scalable=no, initial-scale=1, maximum-scale=1, minimum-scale=1, width=device-width, height=device-height, target-densitydpi=device-dpi" />
|
8
|
+
<title>Dante2</title>
|
9
|
+
<link rel="stylesheet" type="text/css" href="/application.css">
|
10
|
+
<script src="/vendor.js"></script>
|
11
|
+
<script src="/application.js"></script>
|
12
|
+
<script>require('initialize');</script>
|
13
|
+
</head>
|
14
|
+
<body>
|
15
|
+
|
16
|
+
<div id="header">
|
17
|
+
<div class="logo">
|
18
|
+
<div class="menu-buttons">
|
19
|
+
<a class="menu-button" target="_blank" href="https://michelson.github.io/dante2/doc.html">
|
20
|
+
Documentation
|
21
|
+
</a>
|
22
|
+
|
23
|
+
<a class="menu-button" target="_blank" href="https://michelson.github.io/dante2/license.html">
|
24
|
+
License
|
25
|
+
</a>
|
26
|
+
</div>
|
27
|
+
|
28
|
+
|
29
|
+
<img src='https://michelson.github.io/dante2/images/dante-editor-logo.png' alt="dante editor" height="21">
|
30
|
+
<span>Dante Editor - 0.2.0 </span>
|
31
|
+
|
32
|
+
</div>
|
33
|
+
|
34
|
+
<a class="github tooltip-left" data-tooltip="Fork me on github" target="_blank" href="https://github.com/michelson/dante2">
|
35
|
+
<img src='https://michelson.github.io/dante2/images/github-logo.png' alt="Fork me on github" height="28">
|
36
|
+
</a>
|
37
|
+
</div>
|
38
|
+
|
39
|
+
<div id="app"></div>
|
40
|
+
|
41
|
+
<script type="text/javascript">
|
42
|
+
document.addEventListener('DOMContentLoaded', function(){
|
43
|
+
editor = new Dante(
|
44
|
+
{
|
45
|
+
upload_url: "http://localhost:9292/uploads/new",
|
46
|
+
el: "app",
|
47
|
+
content: window.Doc,
|
48
|
+
read_only: true
|
49
|
+
}
|
50
|
+
)
|
51
|
+
editor.render()
|
52
|
+
window.dante_editor = editor
|
53
|
+
})
|
54
|
+
</script>
|
55
|
+
|
56
|
+
</body>
|
57
|
+
</html>
|
@@ -0,0 +1 @@
|
|
1
|
+
{"status": "ok"}
|
@@ -0,0 +1,1083 @@
|
|
1
|
+
|
2
|
+
React = require('react')
|
3
|
+
ReactDOM = require('react-dom')
|
4
|
+
Immutable = require('immutable')
|
5
|
+
{ Map, fromJS } = require('immutable')
|
6
|
+
{
|
7
|
+
convertToRaw
|
8
|
+
convertFromRaw
|
9
|
+
CompositeDecorator
|
10
|
+
getVisibleSelectionRect
|
11
|
+
getDefaultKeyBinding
|
12
|
+
getSelectionOffsetKeyForNode
|
13
|
+
KeyBindingUtil
|
14
|
+
ContentState
|
15
|
+
Editor
|
16
|
+
EditorState
|
17
|
+
Entity
|
18
|
+
RichUtils
|
19
|
+
DefaultDraftBlockRenderMap
|
20
|
+
SelectionState
|
21
|
+
Modifier
|
22
|
+
BlockMapBuilder
|
23
|
+
getSafeBodyFromHTML
|
24
|
+
} = require('draft-js')
|
25
|
+
|
26
|
+
DraftPasteProcessor = require('draft-js/lib/DraftPasteProcessor')
|
27
|
+
|
28
|
+
# {stateToHTML} = require('draft-js-export-html')
|
29
|
+
{
|
30
|
+
convertToHTML
|
31
|
+
convertFromHTML
|
32
|
+
} = require('draft-convert')
|
33
|
+
|
34
|
+
isSoftNewlineEvent = require('draft-js/lib/isSoftNewlineEvent')
|
35
|
+
|
36
|
+
{
|
37
|
+
addNewBlock
|
38
|
+
resetBlockWithType
|
39
|
+
updateDataOfBlock
|
40
|
+
updateTextOfBlock
|
41
|
+
getCurrentBlock
|
42
|
+
addNewBlockAt
|
43
|
+
updateDataOfBlock
|
44
|
+
} = require('../model/index.js')
|
45
|
+
|
46
|
+
DanteImagePopover = require('./popovers/image')
|
47
|
+
DanteAnchorPopover = require('./popovers/link')
|
48
|
+
|
49
|
+
{
|
50
|
+
getSelectionRect
|
51
|
+
getSelection
|
52
|
+
} = require("../utils/selection.js")
|
53
|
+
DanteInlineTooltip = require('./popovers/addButton.cjsx')
|
54
|
+
DanteTooltip = require('./popovers/toolTip.cjsx')
|
55
|
+
Link = require('./decorators/link.cjsx')
|
56
|
+
|
57
|
+
Debug = require('./debug.cjsx')
|
58
|
+
findEntities = require('../utils/find_entities.coffee')
|
59
|
+
ImageBlock = require('./blocks/image.cjsx')
|
60
|
+
EmbedBlock = require('./blocks/embed.cjsx')
|
61
|
+
VideoBlock = require('./blocks/video.cjsx')
|
62
|
+
PlaceholderBlock = require('./blocks/placeholder.cjsx')
|
63
|
+
|
64
|
+
SaveBehavior = require('../utils/save_content.coffee')
|
65
|
+
customHTML2Content = require('../utils/html2content.coffee')
|
66
|
+
|
67
|
+
class Dante
|
68
|
+
constructor: (options={})->
|
69
|
+
console.log "init editor Dante!"
|
70
|
+
|
71
|
+
# deep merge on config
|
72
|
+
config = Map(fromJS(@defaultOptions(options)))
|
73
|
+
|
74
|
+
@options = config.mergeDeep(options).toJS()
|
75
|
+
console.log @options
|
76
|
+
|
77
|
+
defaultOptions: (options={})->
|
78
|
+
# default options
|
79
|
+
defaultOptions = {}
|
80
|
+
defaultOptions.el = 'app'
|
81
|
+
defaultOptions.content = ""
|
82
|
+
defaultOptions.read_only = false
|
83
|
+
defaultOptions.spellcheck = false
|
84
|
+
defaultOptions.title_placeholder = "Title"
|
85
|
+
defaultOptions.body_placeholder = "Write your story"
|
86
|
+
# @defaultOptions.api_key = "86c28a410a104c8bb58848733c82f840"
|
87
|
+
|
88
|
+
defaultOptions.widgets = [
|
89
|
+
{
|
90
|
+
title: 'add an image'
|
91
|
+
icon: 'image'
|
92
|
+
type: 'image'
|
93
|
+
block: 'ImageBlock'
|
94
|
+
editable: true
|
95
|
+
renderable: true
|
96
|
+
breakOnContinuous: true
|
97
|
+
wrapper_class: "graf graf--figure"
|
98
|
+
selected_class: "is-selected is-mediaFocused"
|
99
|
+
selectedFn: (block)=>
|
100
|
+
direction = block.getData().toJS().direction
|
101
|
+
switch direction
|
102
|
+
when "left" then "graf--layoutOutsetLeft"
|
103
|
+
when "center" then ""
|
104
|
+
when "wide" then "sectionLayout--fullWidth"
|
105
|
+
when "fill" then "graf--layoutFillWidth"
|
106
|
+
else
|
107
|
+
handleEnterWithoutText: (ctx, block)->
|
108
|
+
editorState = ctx.state.editorState
|
109
|
+
ctx.onChange(addNewBlockAt(editorState, block.getKey()))
|
110
|
+
handleEnterWithText: (ctx, block)->
|
111
|
+
editorState = ctx.state.editorState
|
112
|
+
ctx.onChange(addNewBlockAt(editorState, block.getKey()))
|
113
|
+
widget_options:
|
114
|
+
displayOnInlineTooltip: true
|
115
|
+
insertion: "upload"
|
116
|
+
insert_block: "image"
|
117
|
+
options:
|
118
|
+
upload_url: options.upload_url
|
119
|
+
upload_callback: options.image_upload_callback
|
120
|
+
image_delete_callback: options.image_delete_callback
|
121
|
+
image_caption_placeholder: options.image_caption_placeholder
|
122
|
+
}
|
123
|
+
{
|
124
|
+
icon: 'embed'
|
125
|
+
title: 'insert embed'
|
126
|
+
type: 'embed'
|
127
|
+
block: 'EmbedBlock'
|
128
|
+
editable: true
|
129
|
+
renderable: true
|
130
|
+
breakOnContinuous: true
|
131
|
+
wrapper_class: "graf graf--mixtapeEmbed"
|
132
|
+
selected_class: "is-selected is-mediaFocused"
|
133
|
+
widget_options:
|
134
|
+
displayOnInlineTooltip: true
|
135
|
+
insertion: "placeholder"
|
136
|
+
insert_block: "embed"
|
137
|
+
options:
|
138
|
+
endpoint: "//api.embed.ly/1/extract?key=#{options.api_key}&url="
|
139
|
+
placeholder: 'Paste a link to embed content from another site (e.g. Twitter) and press Enter'
|
140
|
+
handleEnterWithoutText: (ctx, block)->
|
141
|
+
editorState = ctx.state.editorState
|
142
|
+
ctx.onChange(addNewBlockAt(editorState, block.getKey()))
|
143
|
+
handleEnterWithText: (ctx, block)->
|
144
|
+
editorState = ctx.state.editorState
|
145
|
+
ctx.onChange(addNewBlockAt(editorState, block.getKey()))
|
146
|
+
}
|
147
|
+
{
|
148
|
+
icon: 'video'
|
149
|
+
title: 'insert video'
|
150
|
+
editable: true
|
151
|
+
type: 'video'
|
152
|
+
block: 'VideoBlock'
|
153
|
+
renderable: true
|
154
|
+
breakOnContinuous: true
|
155
|
+
wrapper_class: "graf--figure graf--iframe"
|
156
|
+
selected_class:" is-selected is-mediaFocused"
|
157
|
+
widget_options:
|
158
|
+
displayOnInlineTooltip: true
|
159
|
+
insertion: "placeholder"
|
160
|
+
insert_block: "video"
|
161
|
+
options:
|
162
|
+
endpoint: "//api.embed.ly/1/oembed?key=#{options.api_key}&url="
|
163
|
+
placeholder: 'Paste a YouTube, Vine, Vimeo, or other video link, and press Enter'
|
164
|
+
caption: 'Type caption for embed (optional)'
|
165
|
+
|
166
|
+
handleEnterWithoutText: (ctx, block)->
|
167
|
+
editorState = ctx.state.editorState
|
168
|
+
ctx.onChange(addNewBlockAt(editorState, block.getKey()))
|
169
|
+
|
170
|
+
handleEnterWithText: (ctx, block)->
|
171
|
+
editorState = ctx.state.editorState
|
172
|
+
ctx.onChange(addNewBlockAt(editorState, block.getKey()))
|
173
|
+
}
|
174
|
+
{
|
175
|
+
renderable: true
|
176
|
+
editable: true
|
177
|
+
block: 'PlaceholderBlock'
|
178
|
+
type: 'placeholder'
|
179
|
+
wrapper_class: "is-embedable"
|
180
|
+
selected_class:" is-selected is-mediaFocused"
|
181
|
+
widget_options:
|
182
|
+
displayOnInlineTooltip: false
|
183
|
+
handleEnterWithText: (ctx, block)->
|
184
|
+
editorState = ctx.state.editorState
|
185
|
+
data =
|
186
|
+
provisory_text: block.getText()
|
187
|
+
endpoint: block.getData().get('endpoint')
|
188
|
+
type: block.getData().get('type')
|
189
|
+
|
190
|
+
ctx.onChange(resetBlockWithType(
|
191
|
+
editorState,
|
192
|
+
data.type,
|
193
|
+
data)
|
194
|
+
)
|
195
|
+
}
|
196
|
+
]
|
197
|
+
|
198
|
+
defaultOptions.tooltips = [
|
199
|
+
{
|
200
|
+
ref: 'insert_tooltip'
|
201
|
+
component: DanteTooltip
|
202
|
+
displayOnSelection: true
|
203
|
+
selectionElements: [
|
204
|
+
"unstyled"
|
205
|
+
"blockquote"
|
206
|
+
"ordered-list"
|
207
|
+
"unordered-list"
|
208
|
+
"unordered-list-item"
|
209
|
+
"ordered-list-item"
|
210
|
+
"code-block"
|
211
|
+
'header-one'
|
212
|
+
'header-two'
|
213
|
+
'header-three'
|
214
|
+
'header-four'
|
215
|
+
]
|
216
|
+
widget_options:
|
217
|
+
block_types: [
|
218
|
+
# {label: 'p', style: 'unstyled'},
|
219
|
+
{label: 'h2', style: 'header-one', type: "block"},
|
220
|
+
{label: 'h3', style: 'header-two', type: "block"},
|
221
|
+
{label: 'h4', style: 'header-three', type: "block"},
|
222
|
+
{label: 'blockquote', style: 'blockquote', type: "block"},
|
223
|
+
{label: 'insertunorderedlist', style: 'unordered-list-item', type: "block"},
|
224
|
+
{label: 'insertorderedlist', style: 'ordered-list-item', type: "block"},
|
225
|
+
{label: 'code', style: 'code-block', type: "block"}
|
226
|
+
{label: 'bold', style: 'BOLD', type: "inline"},
|
227
|
+
{label: 'italic', style: 'ITALIC', type: "inline"},
|
228
|
+
# {label: 'underline', style: 'UNDERLINE', type: "inline"},
|
229
|
+
# {label: 'monospace', style: 'CODE', type: "inline"},
|
230
|
+
# {label: 'strikethrough', style: 'STRIKETHROUGH', type: "inline"}
|
231
|
+
]
|
232
|
+
}
|
233
|
+
{
|
234
|
+
ref: 'add_tooltip'
|
235
|
+
component: DanteInlineTooltip
|
236
|
+
}
|
237
|
+
{
|
238
|
+
ref: 'anchor_popover'
|
239
|
+
component: DanteAnchorPopover
|
240
|
+
}
|
241
|
+
{
|
242
|
+
ref: 'image_popover'
|
243
|
+
component: DanteImagePopover
|
244
|
+
}
|
245
|
+
]
|
246
|
+
|
247
|
+
defaultOptions.xhr = {
|
248
|
+
before_handler: null
|
249
|
+
success_handler: null
|
250
|
+
error_handler: null
|
251
|
+
}
|
252
|
+
|
253
|
+
defaultOptions.data_storage = {
|
254
|
+
url: null
|
255
|
+
method: "POST"
|
256
|
+
success_handler: null
|
257
|
+
failure_handler: null
|
258
|
+
interval: 1500
|
259
|
+
}
|
260
|
+
|
261
|
+
defaultOptions.default_wrappers = [
|
262
|
+
{className: 'graf--p', block: 'unstyled'},
|
263
|
+
{className: 'graf--h2', block: 'header-one'},
|
264
|
+
{className: 'graf--h3', block: 'header-two'},
|
265
|
+
{className: 'graf--h4', block: 'header-three'},
|
266
|
+
{className: 'graf--blockquote', block: 'blockquote'},
|
267
|
+
{className: 'graf--insertunorderedlist', block: 'unordered-list-item'},
|
268
|
+
{className: 'graf--insertorderedlist', block: 'ordered-list-item'},
|
269
|
+
{className: 'graf--code', block: 'code-block'}
|
270
|
+
{className: 'graf--bold', block: 'BOLD'},
|
271
|
+
{className: 'graf--italic', block: 'ITALIC'},
|
272
|
+
]
|
273
|
+
|
274
|
+
defaultOptions.continuousBlocks = [
|
275
|
+
"unstyled"
|
276
|
+
"blockquote"
|
277
|
+
"ordered-list"
|
278
|
+
"unordered-list"
|
279
|
+
"unordered-list-item"
|
280
|
+
"ordered-list-item"
|
281
|
+
"code-block"
|
282
|
+
]
|
283
|
+
|
284
|
+
defaultOptions.key_commands = {
|
285
|
+
"alt-shift": [
|
286
|
+
{ key: 65, cmd: 'add-new-block'}
|
287
|
+
],
|
288
|
+
"alt-cmd": [
|
289
|
+
{ key: 49, cmd: 'toggle_block:header-one'}
|
290
|
+
{ key: 50, cmd: 'toggle_block:header-two'}
|
291
|
+
{ key: 53, cmd: 'toggle_block:blockquote'}
|
292
|
+
],
|
293
|
+
"cmd": [
|
294
|
+
{ key: 66, cmd: 'toggle_inline:BOLD'}
|
295
|
+
{ key: 73, cmd: 'toggle_inline:ITALIC'}
|
296
|
+
{ key: 75, cmd: 'insert:link'}
|
297
|
+
]
|
298
|
+
}
|
299
|
+
|
300
|
+
defaultOptions.character_convert_mapping = {
|
301
|
+
'> ': "blockquote",
|
302
|
+
'*.': "unordered-list-item",
|
303
|
+
'* ': "unordered-list-item",
|
304
|
+
'- ': "unordered-list-item",
|
305
|
+
'1.': "ordered-list-item",
|
306
|
+
'# ': 'header-one',
|
307
|
+
'##': 'header-two',
|
308
|
+
'==': "unstyled",
|
309
|
+
'` ': "code-block",
|
310
|
+
}
|
311
|
+
|
312
|
+
defaultOptions
|
313
|
+
|
314
|
+
getContent: ->
|
315
|
+
#console.log @options.content
|
316
|
+
#console.log "IS POC DATA?", @options.content is PocData
|
317
|
+
#return PocData if @options.poc
|
318
|
+
#console.log @options.content , PocData
|
319
|
+
#PocData
|
320
|
+
@options.content
|
321
|
+
|
322
|
+
render: ->
|
323
|
+
ReactDOM.render(<DanteEditor content={@getContent()}
|
324
|
+
config={@options}/>, document.getElementById(@options.el))
|
325
|
+
|
326
|
+
class DanteEditor extends React.Component
|
327
|
+
constructor: (props) ->
|
328
|
+
super props
|
329
|
+
#window.main_editor = @
|
330
|
+
|
331
|
+
@decorator = new CompositeDecorator([
|
332
|
+
{
|
333
|
+
strategy: findEntities.bind(null, 'LINK', @),
|
334
|
+
component: Link
|
335
|
+
}
|
336
|
+
])
|
337
|
+
|
338
|
+
@.blockRenderMap = Map({
|
339
|
+
"image":
|
340
|
+
element: 'figure'
|
341
|
+
"video":
|
342
|
+
element: 'figure'
|
343
|
+
"embed":
|
344
|
+
element: 'div'
|
345
|
+
'unstyled':
|
346
|
+
wrapper: null
|
347
|
+
element: 'div'
|
348
|
+
'paragraph':
|
349
|
+
wrapper: null
|
350
|
+
element: 'div'
|
351
|
+
'placeholder':
|
352
|
+
wrapper: null
|
353
|
+
element: 'div'
|
354
|
+
|
355
|
+
})
|
356
|
+
|
357
|
+
@extendedBlockRenderMap = DefaultDraftBlockRenderMap.merge(@blockRenderMap);
|
358
|
+
|
359
|
+
@state =
|
360
|
+
editorState: @initializeState()
|
361
|
+
read_only: @props.config.read_only
|
362
|
+
blockRenderMap: @extendedBlockRenderMap
|
363
|
+
locks: 0
|
364
|
+
debug: @props.config.debug
|
365
|
+
|
366
|
+
@widgets = @props.config.widgets
|
367
|
+
@tooltips = @props.config.tooltips
|
368
|
+
|
369
|
+
@key_commands = @props.config.key_commands
|
370
|
+
|
371
|
+
@continuousBlocks = @props.config.continuousBlocks
|
372
|
+
|
373
|
+
@block_types = @props.config.block_types
|
374
|
+
|
375
|
+
@default_wrappers = @props.config.default_wrappers
|
376
|
+
|
377
|
+
@character_convert_mapping = @props.config.character_convert_mapping
|
378
|
+
|
379
|
+
@save = new SaveBehavior
|
380
|
+
getLocks: @getLocks
|
381
|
+
config:
|
382
|
+
xhr: @props.config.xhr
|
383
|
+
data_storage: @props.config.data_storage
|
384
|
+
editorState: @state.editorState
|
385
|
+
editorContent: @emitSerializedOutput()
|
386
|
+
|
387
|
+
initializeState: ()=>
|
388
|
+
if @.props.content #and @.props.content.trim() isnt ""
|
389
|
+
@decodeEditorContent(@props.content)
|
390
|
+
else
|
391
|
+
EditorState.createEmpty(@decorator)
|
392
|
+
|
393
|
+
refreshSelection: (newEditorState)=>
|
394
|
+
editorState = @.state.editorState
|
395
|
+
# Setting cursor position after inserting to content
|
396
|
+
s = @.state.editorState.getSelection()
|
397
|
+
c = editorState.getCurrentContent()
|
398
|
+
|
399
|
+
selectionState = SelectionState.createEmpty(s.getAnchorKey())
|
400
|
+
focusOffset = s.getFocusOffset()
|
401
|
+
anchorKey = s.getAnchorKey()
|
402
|
+
# console.log anchorKey, focusOffset
|
403
|
+
selectionState = selectionState.merge({
|
404
|
+
anchorOffset: focusOffset,
|
405
|
+
focusKey: anchorKey,
|
406
|
+
focusOffset: focusOffset,
|
407
|
+
})
|
408
|
+
|
409
|
+
newState = EditorState.forceSelection(newEditorState, selectionState)
|
410
|
+
|
411
|
+
@onChange(newState)
|
412
|
+
|
413
|
+
forceRender: (editorState)=>
|
414
|
+
selection = @.state.editorState.getSelection()
|
415
|
+
content = editorState.getCurrentContent()
|
416
|
+
newEditorState = EditorState.createWithContent(content, @decorator)
|
417
|
+
|
418
|
+
@refreshSelection(newEditorState)
|
419
|
+
|
420
|
+
onChange: (editorState) =>
|
421
|
+
@setPreContent()
|
422
|
+
|
423
|
+
# console.log "BB", editorState.toJS()
|
424
|
+
@.setState({editorState})
|
425
|
+
#console.log "changed at", editorState.getSelection().getAnchorKey()
|
426
|
+
#console.log "collapsed?", editorState.getSelection().isCollapsed()
|
427
|
+
|
428
|
+
currentBlock = getCurrentBlock(@state.editorState);
|
429
|
+
blockType = currentBlock.getType()
|
430
|
+
|
431
|
+
if (!editorState.getSelection().isCollapsed())
|
432
|
+
|
433
|
+
tooltip = @tooltipsWithProp('displayOnSelection')[0]
|
434
|
+
return unless @tooltipHasSelectionElement(tooltip, blockType)
|
435
|
+
|
436
|
+
@handleTooltipDisplayOn('displayOnSelection')
|
437
|
+
else
|
438
|
+
@handleTooltipDisplayOn('displayOnSelection', false)
|
439
|
+
|
440
|
+
setTimeout ()=>
|
441
|
+
@relocateTooltips()
|
442
|
+
, 0
|
443
|
+
|
444
|
+
@dispatchChangesToSave()
|
445
|
+
|
446
|
+
dispatchChangesToSave: =>
|
447
|
+
clearTimeout @saveTimeout
|
448
|
+
@saveTimeout = setTimeout =>
|
449
|
+
@save.store(@emitSerializedOutput())
|
450
|
+
, 100
|
451
|
+
|
452
|
+
setPreContent: =>
|
453
|
+
content = @emitSerializedOutput()
|
454
|
+
# console.log "SET PRE CONTENT", content
|
455
|
+
@save.editorContent = content
|
456
|
+
|
457
|
+
focus: () =>
|
458
|
+
#@props.refs.richEditor.focus()
|
459
|
+
|
460
|
+
getEditorState: =>
|
461
|
+
@state.editorState
|
462
|
+
|
463
|
+
emitSerializedOutput: =>
|
464
|
+
#s = @state.editorState.getCurrentContent()
|
465
|
+
|
466
|
+
raw = convertToRaw( @state.editorState.getCurrentContent() )
|
467
|
+
# console.log raw
|
468
|
+
|
469
|
+
#raw_as_json = JSON.stringify(raw)
|
470
|
+
#console.log raw_as_json
|
471
|
+
#raw_as_json
|
472
|
+
raw
|
473
|
+
|
474
|
+
decodeEditorContent: (raw_as_json)=>
|
475
|
+
# console.log "CONTENT", raw_as_json
|
476
|
+
# new_content = convertFromRaw(JSON.parse(raw_as_json))
|
477
|
+
new_content = convertFromRaw(raw_as_json)
|
478
|
+
editorState = EditorState.createWithContent(new_content, @decorator)
|
479
|
+
|
480
|
+
## title utils
|
481
|
+
getTextFromEditor: =>
|
482
|
+
c = @.state.editorState.getCurrentContent()
|
483
|
+
out = c.getBlocksAsArray().map (o)=>
|
484
|
+
o.getText()
|
485
|
+
.join("\n")
|
486
|
+
|
487
|
+
console.log out
|
488
|
+
return out
|
489
|
+
|
490
|
+
emitHTML2: ()->
|
491
|
+
|
492
|
+
html = convertToHTML(
|
493
|
+
entityToHTML: (entity, originalText) =>
|
494
|
+
if entity.type is 'LINK'
|
495
|
+
return "<a href=\"#{entity.data.url}\">#{originalText}</a>"
|
496
|
+
else
|
497
|
+
return originalText
|
498
|
+
|
499
|
+
)(@.state.editorState.getCurrentContent())
|
500
|
+
#html = convertToHTML(@.state.editorState.getCurrentContent())
|
501
|
+
|
502
|
+
getLocks: =>
|
503
|
+
@state.locks
|
504
|
+
|
505
|
+
addLock: =>
|
506
|
+
@setState
|
507
|
+
locks: @state.locks +=1
|
508
|
+
|
509
|
+
removeLock: =>
|
510
|
+
#return unless @state.locks < 0
|
511
|
+
@setState
|
512
|
+
locks: @state.locks -=1
|
513
|
+
|
514
|
+
renderableBlocks: =>
|
515
|
+
@widgets.filter (o)->
|
516
|
+
o.renderable
|
517
|
+
.map (o)->
|
518
|
+
o.type
|
519
|
+
|
520
|
+
defaultWrappers: (blockType)=>
|
521
|
+
@default_wrappers.filter (o)=>
|
522
|
+
o.block is blockType
|
523
|
+
.map (o)->
|
524
|
+
o.className
|
525
|
+
|
526
|
+
blockRenderer: (block)=>
|
527
|
+
|
528
|
+
switch block.getType()
|
529
|
+
|
530
|
+
when "atomic"
|
531
|
+
|
532
|
+
entity = block.getEntityAt(0)
|
533
|
+
|
534
|
+
#return null unless entity
|
535
|
+
entity_type = Entity.get(entity).getType()
|
536
|
+
|
537
|
+
if @renderableBlocks().includes(block.getType())
|
538
|
+
return @handleBlockRenderer(block)
|
539
|
+
|
540
|
+
return null;
|
541
|
+
|
542
|
+
handleBlockRenderer: (block)=>
|
543
|
+
dataBlock = @getDataBlock(block)
|
544
|
+
return null unless dataBlock
|
545
|
+
|
546
|
+
read_only = if @state.read_only then false else null
|
547
|
+
editable = if read_only isnt null then read_only else dataBlock.editable
|
548
|
+
# console.log "blockrender for #{block.getType()} #{editable}"
|
549
|
+
|
550
|
+
return (
|
551
|
+
component: eval(dataBlock.block)
|
552
|
+
editable: editable
|
553
|
+
props:
|
554
|
+
data: block.getData()
|
555
|
+
getEditorState: @getEditorState
|
556
|
+
setEditorState: @onChange
|
557
|
+
addLock: @addLock
|
558
|
+
removeLock: @removeLock
|
559
|
+
getLocks: @getLocks
|
560
|
+
config: dataBlock.options
|
561
|
+
)
|
562
|
+
|
563
|
+
return null
|
564
|
+
|
565
|
+
blockStyleFn: (block)=>
|
566
|
+
currentBlock = getCurrentBlock(@.state.editorState)
|
567
|
+
is_selected = if currentBlock.getKey() is block.getKey() then "is-selected" else ""
|
568
|
+
|
569
|
+
if @renderableBlocks().includes(block.getType())
|
570
|
+
return @styleForBlock(block, currentBlock, is_selected)
|
571
|
+
|
572
|
+
defaultBlockClass = @defaultWrappers(block.getType())
|
573
|
+
if defaultBlockClass.length > 0
|
574
|
+
return "graf #{defaultBlockClass[0]} #{is_selected}"
|
575
|
+
else
|
576
|
+
return "graf nana #{is_selected}"
|
577
|
+
|
578
|
+
getDataBlock: (block)=>
|
579
|
+
@widgets.find (o)=>
|
580
|
+
o.type is block.getType()
|
581
|
+
|
582
|
+
styleForBlock: (block, currentBlock, is_selected)=>
|
583
|
+
dataBlock = @getDataBlock(block)
|
584
|
+
|
585
|
+
return null unless dataBlock
|
586
|
+
|
587
|
+
selectedFn = if dataBlock.selectedFn then dataBlock.selectedFn(block) else null
|
588
|
+
selected_class = if is_selected then dataBlock.selected_class else ''
|
589
|
+
|
590
|
+
return "#{dataBlock.wrapper_class} #{selected_class} #{selectedFn}"
|
591
|
+
|
592
|
+
handleTooltipDisplayOn: (prop, display=true)->
|
593
|
+
setTimeout =>
|
594
|
+
items = @tooltipsWithProp(prop)
|
595
|
+
items.map (o)=>
|
596
|
+
@refs[o.ref].display(display)
|
597
|
+
@refs[o.ref].relocate()
|
598
|
+
, 20
|
599
|
+
|
600
|
+
handlePasteText: (text, html)=>
|
601
|
+
|
602
|
+
# https://github.com/facebook/draft-js/issues/685
|
603
|
+
###
|
604
|
+
html = "<p>chao</p>
|
605
|
+
<avv>aaa</avv>
|
606
|
+
<p>oli</p>
|
607
|
+
<img src='x'/>"
|
608
|
+
###
|
609
|
+
|
610
|
+
# if not html then fallback to default handler
|
611
|
+
|
612
|
+
return @handleTXTPaste(text, html) unless html
|
613
|
+
return @handleHTMLPaste(text, html) if html
|
614
|
+
|
615
|
+
handleTXTPaste: (text, html)=>
|
616
|
+
currentBlock = getCurrentBlock(@state.editorState)
|
617
|
+
|
618
|
+
editorState = @state.editorState
|
619
|
+
|
620
|
+
switch currentBlock.getType()
|
621
|
+
when "image", "video", "placeholder"
|
622
|
+
newContent = Modifier.replaceText(
|
623
|
+
editorState.getCurrentContent(),
|
624
|
+
new SelectionState({
|
625
|
+
anchorKey: currentBlock.getKey(),
|
626
|
+
anchorOffset: 0,
|
627
|
+
focusKey: currentBlock.getKey(),
|
628
|
+
focusOffset: 2
|
629
|
+
}),
|
630
|
+
text)
|
631
|
+
|
632
|
+
editorState = EditorState.push(
|
633
|
+
editorState,
|
634
|
+
newContent,
|
635
|
+
'replace-text'
|
636
|
+
);
|
637
|
+
|
638
|
+
@onChange(editorState)
|
639
|
+
|
640
|
+
return true
|
641
|
+
else
|
642
|
+
return false
|
643
|
+
|
644
|
+
handleHTMLPaste: (text, html)=>
|
645
|
+
|
646
|
+
currentBlock = getCurrentBlock(@state.editorState)
|
647
|
+
|
648
|
+
# TODO: make this configurable
|
649
|
+
switch currentBlock.getType()
|
650
|
+
when "image", "video", "placeholder"
|
651
|
+
return @handleTXTPaste(text, html)
|
652
|
+
else
|
653
|
+
# keep going
|
654
|
+
|
655
|
+
newContentState = customHTML2Content(html, @extendedBlockRenderMap)
|
656
|
+
|
657
|
+
selection = @state.editorState.getSelection();
|
658
|
+
endKey = selection.getEndKey()
|
659
|
+
|
660
|
+
content = @state.editorState.getCurrentContent();
|
661
|
+
blocksBefore = content.blockMap.toSeq().takeUntil((v) => (v.key is endKey))
|
662
|
+
blocksAfter = content.blockMap.toSeq().skipUntil((v) => (v.key is endKey)).rest()
|
663
|
+
|
664
|
+
newBlockKey = newContentState.blockMap.first().getKey()
|
665
|
+
|
666
|
+
newBlockMap = blocksBefore.concat(
|
667
|
+
newContentState.blockMap,
|
668
|
+
blocksAfter
|
669
|
+
).toOrderedMap();
|
670
|
+
|
671
|
+
newContent = content.merge({
|
672
|
+
blockMap: newBlockMap,
|
673
|
+
selectionBefore: selection,
|
674
|
+
selectionAfter: selection.merge({
|
675
|
+
anchorKey: newBlockKey,
|
676
|
+
anchorOffset: 0,
|
677
|
+
focusKey: newBlockKey,
|
678
|
+
focusOffset: 0,
|
679
|
+
isBackward: false,
|
680
|
+
})
|
681
|
+
});
|
682
|
+
|
683
|
+
pushedContentState = EditorState.push(
|
684
|
+
@state.editorState,
|
685
|
+
newContent,
|
686
|
+
'insert-fragment'
|
687
|
+
)
|
688
|
+
|
689
|
+
@onChange(pushedContentState)
|
690
|
+
|
691
|
+
true
|
692
|
+
|
693
|
+
handlePasteImage: (files)=>
|
694
|
+
#TODO: check file types
|
695
|
+
files.map (file)=>
|
696
|
+
opts =
|
697
|
+
url: URL.createObjectURL(file)
|
698
|
+
file: file
|
699
|
+
|
700
|
+
@onChange(addNewBlock(@state.editorState, 'image', opts))
|
701
|
+
|
702
|
+
handleDroppedFiles: (state, files)=>
|
703
|
+
files.map (file)=>
|
704
|
+
opts =
|
705
|
+
url: URL.createObjectURL(file)
|
706
|
+
file: file
|
707
|
+
|
708
|
+
@onChange(addNewBlock(@state.editorState, 'image', opts))
|
709
|
+
|
710
|
+
handleUpArrow: (e)=>
|
711
|
+
setTimeout =>
|
712
|
+
@forceRender(@state.editorState)
|
713
|
+
, 10
|
714
|
+
|
715
|
+
handleDownArrow: (e)=>
|
716
|
+
setTimeout =>
|
717
|
+
@forceRender(@state.editorState)
|
718
|
+
, 10
|
719
|
+
|
720
|
+
handleReturn: (e) =>
|
721
|
+
if this.props.handleReturn
|
722
|
+
if this.props.handleReturn()
|
723
|
+
return true;
|
724
|
+
|
725
|
+
editorState = @state.editorState
|
726
|
+
###
|
727
|
+
#if (isSoftNewlineEvent(e)) {
|
728
|
+
# this.onChange(RichUtils.insertSoftNewline(editorState));
|
729
|
+
# return true;
|
730
|
+
#}
|
731
|
+
###
|
732
|
+
if !e.altKey && !e.metaKey && !e.ctrlKey
|
733
|
+
currentBlock = getCurrentBlock(editorState)
|
734
|
+
blockType = currentBlock.getType()
|
735
|
+
selection = editorState.getSelection()
|
736
|
+
|
737
|
+
config_block = @getDataBlock(currentBlock)
|
738
|
+
|
739
|
+
#if blockType.indexOf('atomic') is 0
|
740
|
+
# @.onChange(addNewBlockAt(editorState, currentBlock.getKey()))
|
741
|
+
# return true;
|
742
|
+
|
743
|
+
if currentBlock.getText().length is 0
|
744
|
+
|
745
|
+
if config_block && config_block.handleEnterWithoutText
|
746
|
+
config_block.handleEnterWithText(@, currentBlock)
|
747
|
+
@closePopOvers()
|
748
|
+
return true
|
749
|
+
|
750
|
+
#TODO turn this in configurable
|
751
|
+
switch (blockType)
|
752
|
+
when "header-one"
|
753
|
+
@.onChange(resetBlockWithType(editorState, "unstyled"));
|
754
|
+
return true;
|
755
|
+
else
|
756
|
+
return false;
|
757
|
+
|
758
|
+
if currentBlock.getText().length > 0
|
759
|
+
|
760
|
+
if blockType is "unstyled"
|
761
|
+
# hack hackety hack
|
762
|
+
# https://github.com/facebook/draft-js/issues/304
|
763
|
+
newContent = Modifier.splitBlock(
|
764
|
+
@state.editorState.getCurrentContent(),
|
765
|
+
@state.editorState.getSelection()
|
766
|
+
)
|
767
|
+
|
768
|
+
newEditorState = EditorState.push(@state.editorState, newContent, 'insert-characters')
|
769
|
+
@.onChange(newEditorState)
|
770
|
+
|
771
|
+
setTimeout =>
|
772
|
+
#TODO: check is element is in viewport
|
773
|
+
a = document.getElementsByClassName("is-selected")
|
774
|
+
pos = a[0].getBoundingClientRect()
|
775
|
+
window.scrollTo(0, pos.top + window.scrollY - 100)
|
776
|
+
, 0
|
777
|
+
|
778
|
+
return true
|
779
|
+
|
780
|
+
if config_block && config_block.handleEnterWithText
|
781
|
+
config_block.handleEnterWithText(@, currentBlock)
|
782
|
+
@closePopOvers()
|
783
|
+
return true
|
784
|
+
|
785
|
+
if ( currentBlock.getLength() is selection.getStartOffset())
|
786
|
+
if @continuousBlocks.indexOf(blockType) < 0
|
787
|
+
@.onChange(addNewBlockAt(editorState, currentBlock.getKey()))
|
788
|
+
return true
|
789
|
+
|
790
|
+
return false
|
791
|
+
|
792
|
+
# selection.isCollapsed() and # should we check collapsed here?
|
793
|
+
if ( currentBlock.getLength() is selection.getStartOffset()) #or (config_block && config_block.breakOnContinuous))
|
794
|
+
# it will match the unstyled for custom blocks
|
795
|
+
if @continuousBlocks.indexOf(blockType) < 0
|
796
|
+
@.onChange(addNewBlockAt(editorState, currentBlock.getKey()))
|
797
|
+
return true
|
798
|
+
return false
|
799
|
+
|
800
|
+
return false
|
801
|
+
|
802
|
+
#return false
|
803
|
+
|
804
|
+
# TODO: make this configurable
|
805
|
+
handleBeforeInput: (chars)=>
|
806
|
+
currentBlock = getCurrentBlock(@state.editorState)
|
807
|
+
blockType = currentBlock.getType()
|
808
|
+
selection = @.state.editorState.getSelection()
|
809
|
+
editorState = @state.editorState
|
810
|
+
|
811
|
+
# close popovers
|
812
|
+
if currentBlock.getText().length isnt 0
|
813
|
+
#@closeInlineButton()
|
814
|
+
@closePopOvers()
|
815
|
+
|
816
|
+
# handle space on link
|
817
|
+
endOffset = selection.getEndOffset()
|
818
|
+
endKey = currentBlock.getEntityAt(endOffset - 1)
|
819
|
+
endEntityType = endKey && Entity.get(endKey).getType()
|
820
|
+
afterEndKey = currentBlock.getEntityAt(endOffset)
|
821
|
+
afterEndEntityType = afterEndKey && Entity.get(afterEndKey).getType()
|
822
|
+
|
823
|
+
# will insert blank space when link found
|
824
|
+
if (chars is ' ' && endEntityType is 'LINK' && afterEndEntityType isnt 'LINK')
|
825
|
+
newContentState = Modifier.insertText(
|
826
|
+
editorState.getCurrentContent(),
|
827
|
+
selection,
|
828
|
+
' '
|
829
|
+
)
|
830
|
+
newEditorState = EditorState.push(editorState, newContentState, 'insert-characters')
|
831
|
+
@.onChange(newEditorState)
|
832
|
+
return true
|
833
|
+
|
834
|
+
# block transform
|
835
|
+
if blockType.indexOf('atomic') is 0
|
836
|
+
return false;
|
837
|
+
|
838
|
+
blockLength = currentBlock.getLength()
|
839
|
+
if selection.getAnchorOffset() > 1 || blockLength > 1
|
840
|
+
return false
|
841
|
+
|
842
|
+
blockTo = @character_convert_mapping[currentBlock.getText() + chars]
|
843
|
+
|
844
|
+
console.log "BLOCK TO SHOW: #{blockTo}"
|
845
|
+
|
846
|
+
if !blockTo
|
847
|
+
return false
|
848
|
+
|
849
|
+
@onChange(resetBlockWithType(editorState, blockTo))
|
850
|
+
|
851
|
+
return true
|
852
|
+
|
853
|
+
# TODO: make this configurable
|
854
|
+
handleKeyCommand: (command)=>
|
855
|
+
editorState = @.state.editorState
|
856
|
+
|
857
|
+
if @.props.handleKeyCommand && @.props.handleKeyCommand(command)
|
858
|
+
return true
|
859
|
+
|
860
|
+
if command is 'add-new-block'
|
861
|
+
@.onChange(addNewBlock(editorState, 'blockquote'))
|
862
|
+
return true
|
863
|
+
|
864
|
+
block = getCurrentBlock(editorState);
|
865
|
+
|
866
|
+
if command.indexOf('toggle_inline:') is 0
|
867
|
+
newBlockType = command.split(':')[1]
|
868
|
+
currentBlockType = block.getType()
|
869
|
+
@onChange(
|
870
|
+
RichUtils.toggleInlineStyle(editorState, newBlockType)
|
871
|
+
)
|
872
|
+
return true
|
873
|
+
|
874
|
+
if command.indexOf('toggle_block:') is 0
|
875
|
+
newBlockType = command.split(':')[1]
|
876
|
+
currentBlockType = block.getType()
|
877
|
+
|
878
|
+
@onChange(
|
879
|
+
RichUtils.toggleBlockType(editorState, newBlockType)
|
880
|
+
)
|
881
|
+
return true;
|
882
|
+
|
883
|
+
newState = RichUtils.handleKeyCommand(@state.editorState, command);
|
884
|
+
if newState
|
885
|
+
@.onChange(newState);
|
886
|
+
return true;
|
887
|
+
|
888
|
+
return false;
|
889
|
+
|
890
|
+
findCommandKey: (opt, command)=>
|
891
|
+
# console.log "COMMAND find: #{opt} #{command}"
|
892
|
+
@key_commands[opt].find (o)->
|
893
|
+
o.key is command
|
894
|
+
|
895
|
+
KeyBindingFn: (e)=>
|
896
|
+
|
897
|
+
#⌘ + B / Ctrl + B Bold
|
898
|
+
#⌘ + I / Ctrl + I Italic
|
899
|
+
#⌘ + K / Ctrl + K Turn into link
|
900
|
+
#⌘ + Alt + 1 / Ctrl + Alt + 1 Header
|
901
|
+
#⌘ + Alt + 2 / Ctrl + Alt + 2 Sub-Header
|
902
|
+
#⌘ + Alt + 5 / Ctrl + Alt + 5 Quote (Press once for a block quote, again for a pull quote and a third time to turn off quote)
|
903
|
+
|
904
|
+
# console.log "CTRL #{e.ctrlKey} #{e.which} alt: #{e.altKey} shift #{e.shiftKey}"
|
905
|
+
# console.log('in window::'+ e.ctrlKey+'in mac os'+e.metaKey+'####'+e.META_MASK+'##$&&'+e.CTRL_MASK);
|
906
|
+
|
907
|
+
if (e.altKey)
|
908
|
+
if (e.shiftKey)
|
909
|
+
cmd = @findCommandKey("alt-shift", e.which)
|
910
|
+
return cmd.cmd if cmd
|
911
|
+
|
912
|
+
return getDefaultKeyBinding(e)
|
913
|
+
|
914
|
+
if e.ctrlKey or e.metaKey
|
915
|
+
cmd = @findCommandKey("alt-cmd", e.which)
|
916
|
+
return cmd.cmd if cmd
|
917
|
+
return getDefaultKeyBinding(e)
|
918
|
+
|
919
|
+
else if e.ctrlKey or e.metaKey
|
920
|
+
cmd = @findCommandKey("cmd", e.which)
|
921
|
+
return cmd.cmd if cmd
|
922
|
+
return getDefaultKeyBinding(e)
|
923
|
+
|
924
|
+
return getDefaultKeyBinding(e)
|
925
|
+
|
926
|
+
# will update block state todo: movo to utils
|
927
|
+
updateBlockData: (block, options)=>
|
928
|
+
data = block.getData()
|
929
|
+
newData = data.merge(options)
|
930
|
+
newState = updateDataOfBlock(@state.editorState, block, newData)
|
931
|
+
# this fixes enter from image caption
|
932
|
+
@forceRender(newState)
|
933
|
+
|
934
|
+
setDirection: (direction_type)=>
|
935
|
+
|
936
|
+
contentState = @state.editorState.getCurrentContent()
|
937
|
+
selectionState = @state.editorState.getSelection()
|
938
|
+
block = contentState.getBlockForKey(selectionState.anchorKey);
|
939
|
+
|
940
|
+
@updateBlockData(block, {direction: direction_type})
|
941
|
+
|
942
|
+
## read only utils
|
943
|
+
toggleEditable: =>
|
944
|
+
@closePopOvers()
|
945
|
+
|
946
|
+
@setState
|
947
|
+
read_only: !@state.read_only
|
948
|
+
, @testEmitAndDecode
|
949
|
+
|
950
|
+
closePopOvers: ()=>
|
951
|
+
@tooltips.map (o)=>
|
952
|
+
@refs[o.ref].hide()
|
953
|
+
|
954
|
+
relocateTooltips: ()=>
|
955
|
+
@tooltips.map (o)=>
|
956
|
+
@refs[o.ref].relocate()
|
957
|
+
|
958
|
+
tooltipsWithProp: (prop)=>
|
959
|
+
@tooltips.filter (o)=>
|
960
|
+
o[prop]
|
961
|
+
|
962
|
+
tooltipHasSelectionElement: (tooltip, element)=>
|
963
|
+
tooltip.selectionElements.includes(element)
|
964
|
+
|
965
|
+
#################################
|
966
|
+
# TODO: this methods belongs to popovers/link
|
967
|
+
#################################
|
968
|
+
|
969
|
+
handleShowPopLinkOver: (e)=>
|
970
|
+
@showPopLinkOver()
|
971
|
+
|
972
|
+
handleHidePopLinkOver: (e)=>
|
973
|
+
@hidePopLinkOver()
|
974
|
+
|
975
|
+
showPopLinkOver: (el)=>
|
976
|
+
# handles popover display
|
977
|
+
# using anchor or from popover
|
978
|
+
|
979
|
+
# set url first in order to calculate popover width
|
980
|
+
@refs.anchor_popover.setState
|
981
|
+
url: if el then el.href else @refs.anchor_popover.state.url
|
982
|
+
|
983
|
+
parent_el = ReactDOM.findDOMNode(@);
|
984
|
+
coords = @refs.anchor_popover.relocate(
|
985
|
+
el
|
986
|
+
) if el
|
987
|
+
|
988
|
+
@refs.anchor_popover.setPosition coords if coords
|
989
|
+
|
990
|
+
@refs.anchor_popover.setState
|
991
|
+
show: true
|
992
|
+
|
993
|
+
@isHover = true
|
994
|
+
@cancelHide()
|
995
|
+
|
996
|
+
hidePopLinkOver: ()=>
|
997
|
+
@hideTimeout = setTimeout ()=>
|
998
|
+
@refs.anchor_popover.hide()
|
999
|
+
, 300
|
1000
|
+
|
1001
|
+
cancelHide: ()->
|
1002
|
+
# console.log "Cancel Hide"
|
1003
|
+
clearTimeout @hideTimeout
|
1004
|
+
|
1005
|
+
###############################
|
1006
|
+
|
1007
|
+
render: =>
|
1008
|
+
|
1009
|
+
return (
|
1010
|
+
<div id="content" suppressContentEditableWarning={true}>
|
1011
|
+
|
1012
|
+
<article className="postArticle">
|
1013
|
+
<div className="postContent">
|
1014
|
+
<div className="notesSource">
|
1015
|
+
<div id="editor"
|
1016
|
+
className="postField postField--body">
|
1017
|
+
|
1018
|
+
<section className="section--first section--last">
|
1019
|
+
<div className="section-divider layoutSingleColumn">
|
1020
|
+
<hr className="section-divider"/>
|
1021
|
+
</div>
|
1022
|
+
|
1023
|
+
<div className="section-content">
|
1024
|
+
<div ref="richEditor"
|
1025
|
+
className="section-inner layoutSingleColumn"
|
1026
|
+
onClick={@.focus}>
|
1027
|
+
<Editor
|
1028
|
+
blockRendererFn={@.blockRenderer}
|
1029
|
+
editorState={@state.editorState}
|
1030
|
+
onChange={@onChange}
|
1031
|
+
onUpArrow={@handleUpArrow}
|
1032
|
+
onDownArrow={@handleDownArrow}
|
1033
|
+
handleReturn={@handleReturn}
|
1034
|
+
blockRenderMap={@state.blockRenderMap}
|
1035
|
+
blockStyleFn={@.blockStyleFn}
|
1036
|
+
handlePastedText={@handlePasteText}
|
1037
|
+
handlePastedFiles={@handlePasteImage}
|
1038
|
+
handleDroppedFiles={@handleDroppedFiles}
|
1039
|
+
handleKeyCommand={@.handleKeyCommand}
|
1040
|
+
keyBindingFn={@KeyBindingFn}
|
1041
|
+
handleBeforeInput={@.handleBeforeInput}
|
1042
|
+
readOnly={@state.read_only}
|
1043
|
+
placeholder={@props.config.body_placeholder}
|
1044
|
+
ref="editor"
|
1045
|
+
/>
|
1046
|
+
</div>
|
1047
|
+
</div>
|
1048
|
+
</section>
|
1049
|
+
|
1050
|
+
</div>
|
1051
|
+
</div>
|
1052
|
+
</div>
|
1053
|
+
</article>
|
1054
|
+
|
1055
|
+
{
|
1056
|
+
@tooltips.map (o, i)=>
|
1057
|
+
<o.component
|
1058
|
+
ref={o.ref}
|
1059
|
+
key={i}
|
1060
|
+
editor={@}
|
1061
|
+
editorState={@state.editorState}
|
1062
|
+
onChange={@onChange}
|
1063
|
+
configTooltip={o}
|
1064
|
+
widget_options={o.widget_options}
|
1065
|
+
|
1066
|
+
showPopLinkOver={@showPopLinkOver}
|
1067
|
+
hidePopLinkOver={@hidePopLinkOver}
|
1068
|
+
handleOnMouseOver={@handleShowPopLinkOver}
|
1069
|
+
handleOnMouseOut={@handleHidePopLinkOver}
|
1070
|
+
/>
|
1071
|
+
}
|
1072
|
+
|
1073
|
+
{
|
1074
|
+
if @state.debug
|
1075
|
+
<Debug locks={@state.locks}
|
1076
|
+
editor={@}
|
1077
|
+
/>
|
1078
|
+
}
|
1079
|
+
|
1080
|
+
</div>
|
1081
|
+
)
|
1082
|
+
|
1083
|
+
module.exports = Dante
|