diff --git a/.eslintrc b/.eslintrc index 99f891d9..c10de0c1 100644 --- a/.eslintrc +++ b/.eslintrc @@ -46,6 +46,7 @@ rules: # https://github.com/eslint/eslint/tree/master/docs/rules#best-practices no-fallthrough: 2 no-redeclare: 2 + no-constant-condition: 2 # Stylistic Issues # https://github.com/eslint/eslint/tree/master/docs/rules#stylistic-issues @@ -60,18 +61,17 @@ rules: object-curly-spacing: [1, "always"] quotes: [2, "single", "avoid-escape"] semi: 2 - space-after-keywords: 2 + keyword-spacing: 2 space-before-blocks: [2, "always"] space-before-function-paren: [2, "never"] space-in-parens: [2, "never"] space-infix-ops: 2 - space-return-throw-case: 2 space-unary-ops: 2 # ECMAScript 6 # http://eslint.org/docs/rules/#ecmascript-6 arrow-spacing: [2, {"before": true, "after": true}] - no-arrow-condition: 2 + no-confusing-arrow: 2 prefer-const: 2 # Strict Mode @@ -97,7 +97,6 @@ rules: react/jsx-no-duplicate-props: 1 react/jsx-no-undef: 1 react/jsx-pascal-case: 1 - react/jsx-sort-prop-types: 1 react/jsx-uses-react: 1 react/jsx-uses-vars: 1 react/no-danger: 1 diff --git a/example/index.html b/example/index.html index 2d910b39..aa1c14e8 100644 --- a/example/index.html +++ b/example/index.html @@ -68,5 +68,27 @@ + diff --git a/package.json b/package.json index b6f2bec3..05201017 100644 --- a/package.json +++ b/package.json @@ -67,16 +67,16 @@ }, "dependencies": { "bricks.js": "^1.7.0", - "commonmark": "^0.24.0", - "commonmark-react-renderer": "^4.1.2", - "draft-js": "^0.7.0", - "draft-js-export-markdown": "^0.2.0", - "draft-js-import-markdown": "^0.1.6", "fuzzy": "^0.1.1", "js-base64": "^2.1.9", "json-loader": "^0.5.4", "localforage": "^1.4.2", "lodash": "^4.13.1", - "pluralize": "^3.0.0" + "markup-it": "git+https://github.com/cassiozen/markup-it.git", + "pluralize": "^3.0.0", + "prismjs": "^1.5.1", + "react-portal": "^2.2.1", + "selection-position": "^1.0.0", + "slate": "^0.13.6" } } diff --git a/src/actions/editor.js b/src/actions/editor.js new file mode 100644 index 00000000..c9e35448 --- /dev/null +++ b/src/actions/editor.js @@ -0,0 +1,8 @@ +export const SWITCH_VISUAL_MODE = 'SWITCH_VISUAL_MODE'; + +export function switchVisualMode(useVisualMode) { + return { + type: SWITCH_VISUAL_MODE, + payload: useVisualMode + }; +} diff --git a/src/components/UI/icon/Icon.css b/src/components/UI/icon/Icon.css index 5fcd9fe9..af041851 100644 --- a/src/components/UI/icon/Icon.css +++ b/src/components/UI/icon/Icon.css @@ -1,305 +1,352 @@ @charset "UTF-8"; +/* The icons font contains the complete entypo set + font awesome editor icons */ + @font-face { - font-family: 'entypo'; - src: url('./entypo.eot'); - src: url('./entypo.eot?#iefix') format('embedded-opentype'), - url('./entypo.woff') format('woff'), - url('./entypo.ttf') format('truetype'), - url('./entypo.svg#entypo') format('svg'); - font-weight: normal; font-style: normal; + font-family: 'icons'; + src: url('./icons.eot'); + src: url('./icons.eot#iefix') format('embedded-opentype'), + url('./icons.woff2') format('woff2'), + url('./icons.woff') format('woff'), + url('./icons.ttf') format('truetype'), + url('./icons.svg#icons') format('svg'); + font-weight: normal; + font-style: normal; } .root { - font-family: entypo; + font-family: 'icons'; font-style: normal; + font-weight: normal; + speak: none; display: inline-block; - width: 1.1em; + width: 1em; margin-right: .1em; text-align: center; + + /* For safety - reset parent styles, that can break glyph codes*/ + font-variant: normal; + text-transform: none; + + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; } -.note:before { content: "\266a"; } /* '\266a' */ -.note-beamed:before { content: "\266b"; } /* '\266b' */ -.music:before { content: "🎵"; } /* '\1f3b5' */ -.search:before { content: "🔍"; } /* '\1f50d' */ -.flashlight:before { content: "🔦"; } /* '\1f526' */ -.mail:before { content: "\2709"; } /* '\2709' */ -.heart:before { content: "\2665"; } /* '\2665' */ -.heart-empty:before { content: "\2661"; } /* '\2661' */ -.star:before { content: "\2605"; } /* '\2605' */ -.star-empty:before { content: "\2606"; } /* '\2606' */ -.user:before { content: "👤"; } /* '\1f464' */ -.users:before { content: "👥"; } /* '\1f465' */ -.user-add:before { content: "\e700"; } /* '\e700' */ -.video:before { content: "🎬"; } /* '\1f3ac' */ -.picture:before { content: "🌄"; } /* '\1f304' */ -.camera:before { content: "📷"; } /* '\1f4f7' */ -.layout:before { content: "\268f"; } /* '\268f' */ -.menu:before { content: "\2630"; } /* '\2630' */ -.check:before { content: "\2713"; } /* '\2713' */ -.cancel:before { content: "\2715"; } /* '\2715' */ -.cancel-circled:before { content: "\2716"; } /* '\2716' */ -.cancel-squared:before { content: "\274e"; } /* '\274e' */ -.plus:before { content: "\2b"; } /* '\2b' */ -.plus-circled:before { content: "\2795"; } /* '\2795' */ -.plus-squared:before { content: "\229e"; } /* '\229e' */ -.minus:before { content: "\2d"; } /* '\2d' */ -.minus-circled:before { content: "\2796"; } /* '\2796' */ -.minus-squared:before { content: "\229f"; } /* '\229f' */ -.help:before { content: "\2753"; } /* '\2753' */ -.help-circled:before { content: "\e704"; } /* '\e704' */ -.info:before { content: "\2139"; } /* '\2139' */ -.info-circled:before { content: "\e705"; } /* '\e705' */ -.back:before { content: "🔙"; } /* '\1f519' */ -.home:before { content: "\2302"; } /* '\2302' */ -.link:before { content: "🔗"; } /* '\1f517' */ -.attach:before { content: "📎"; } /* '\1f4ce' */ -.lock:before { content: "🔒"; } /* '\1f512' */ -.lock-open:before { content: "🔓"; } /* '\1f513' */ -.eye:before { content: "\e70a"; } /* '\e70a' */ -.tag:before { content: "\e70c"; } /* '\e70c' */ -.bookmark:before { content: "🔖"; } /* '\1f516' */ -.bookmarks:before { content: "📑"; } /* '\1f4d1' */ -.flag:before { content: "\2691"; } /* '\2691' */ -.thumbs-up:before { content: "👍"; } /* '\1f44d' */ -.thumbs-down:before { content: "👎"; } /* '\1f44e' */ -.download:before { content: "📥"; } /* '\1f4e5' */ -.upload:before { content: "📤"; } /* '\1f4e4' */ -.upload-cloud:before { content: "\e711"; } /* '\e711' */ -.reply:before { content: "\e712"; } /* '\e712' */ -.reply-all:before { content: "\e713"; } /* '\e713' */ -.forward:before { content: "\27a6"; } /* '\27a6' */ -.quote:before { content: "\275e"; } /* '\275e' */ -.code:before { content: "\e714"; } /* '\e714' */ -.export:before { content: "\e715"; } /* '\e715' */ -.pencil:before { content: "\270e"; } /* '\270e' */ -.feather:before { content: "\2712"; } /* '\2712' */ -.print:before { content: "\e716"; } /* '\e716' */ -.retweet:before { content: "\e717"; } /* '\e717' */ -.keyboard:before { content: "\2328"; } /* '\2328' */ -.comment:before { content: "\e718"; } /* '\e718' */ -.chat:before { content: "\e720"; } /* '\e720' */ -.bell:before { content: "🔔"; } /* '\1f514' */ -.attention:before { content: "\26a0"; } /* '\26a0' */ -.alert:before { content: "💥"; } /* '\1f4a5' */ -.vcard:before { content: "\e722"; } /* '\e722' */ -.address:before { content: "\e723"; } /* '\e723' */ -.location:before { content: "\e724"; } /* '\e724' */ -.map:before { content: "\e727"; } /* '\e727' */ -.direction:before { content: "\27a2"; } /* '\27a2' */ -.compass:before { content: "\e728"; } /* '\e728' */ -.cup:before { content: "\2615"; } /* '\2615' */ -.trash:before { content: "\e729"; } /* '\e729' */ -.doc:before { content: "\e730"; } /* '\e730' */ -.docs:before { content: "\e736"; } /* '\e736' */ -.doc-landscape:before { content: "\e737"; } /* '\e737' */ -.doc-text:before { content: "📄"; } /* '\1f4c4' */ -.doc-text-inv:before { content: "\e731"; } /* '\e731' */ -.newspaper:before { content: "📰"; } /* '\1f4f0' */ -.book-open:before { content: "📖"; } /* '\1f4d6' */ -.book:before { content: "📕"; } /* '\1f4d5' */ -.folder:before { content: "📁"; } /* '\1f4c1' */ -.archive:before { content: "\e738"; } /* '\e738' */ -.box:before { content: "📦"; } /* '\1f4e6' */ -.rss:before { content: "\e73a"; } /* '\e73a' */ -.phone:before { content: "📞"; } /* '\1f4de' */ -.cog:before { content: "\2699"; } /* '\2699' */ -.tools:before { content: "\2692"; } /* '\2692' */ -.share:before { content: "\e73c"; } /* '\e73c' */ -.shareable:before { content: "\e73e"; } /* '\e73e' */ -.basket:before { content: "\e73d"; } /* '\e73d' */ -.bag:before { content: "👜"; } /* '\1f45c' */ -.calendar:before { content: "📅"; } /* '\1f4c5' */ -.login:before { content: "\e740"; } /* '\e740' */ -.logout:before { content: "\e741"; } /* '\e741' */ -.mic:before { content: "🎤"; } /* '\1f3a4' */ -.mute:before { content: "🔇"; } /* '\1f507' */ -.sound:before { content: "🔊"; } /* '\1f50a' */ -.volume:before { content: "\e742"; } /* '\e742' */ -.clock:before { content: "🕔"; } /* '\1f554' */ -.hourglass:before { content: "\23f3"; } /* '\23f3' */ -.lamp:before { content: "💡"; } /* '\1f4a1' */ -.light-down:before { content: "🔅"; } /* '\1f505' */ -.light-up:before { content: "🔆"; } /* '\1f506' */ -.adjust:before { content: "\25d1"; } /* '\25d1' */ -.block:before { content: "🚫"; } /* '\1f6ab' */ -.resize-full:before { content: "\e744"; } /* '\e744' */ -.resize-small:before { content: "\e746"; } /* '\e746' */ -.popup:before { content: "\e74c"; } /* '\e74c' */ -.publish:before { content: "\e74d"; } /* '\e74d' */ -.window:before { content: "\e74e"; } /* '\e74e' */ -.arrow-combo:before { content: "\e74f"; } /* '\e74f' */ -.down-circled:before { content: "\e758"; } /* '\e758' */ -.left-circled:before { content: "\e759"; } /* '\e759' */ -.right-circled:before { content: "\e75a"; } /* '\e75a' */ -.up-circled:before { content: "\e75b"; } /* '\e75b' */ -.down-open:before { content: "\e75c"; } /* '\e75c' */ -.left-open:before { content: "\e75d"; } /* '\e75d' */ -.right-open:before { content: "\e75e"; } /* '\e75e' */ -.up-open:before { content: "\e75f"; } /* '\e75f' */ -.down-open-mini:before { content: "\e760"; } /* '\e760' */ -.left-open-mini:before { content: "\e761"; } /* '\e761' */ -.right-open-mini:before { content: "\e762"; } /* '\e762' */ -.up-open-mini:before { content: "\e763"; } /* '\e763' */ -.down-open-big:before { content: "\e764"; } /* '\e764' */ -.left-open-big:before { content: "\e765"; } /* '\e765' */ -.right-open-big:before { content: "\e766"; } /* '\e766' */ -.up-open-big:before { content: "\e767"; } /* '\e767' */ -.down:before { content: "\2b07"; } /* '\2b07' */ -.left:before { content: "\2b05"; } /* '\2b05' */ -.right:before { content: "\27a1"; } /* '\27a1' */ -.up:before { content: "\2b06"; } /* '\2b06' */ -.down-dir:before { content: "\25be"; } /* '\25be' */ -.left-dir:before { content: "\25c2"; } /* '\25c2' */ -.right-dir:before { content: "\25b8"; } /* '\25b8' */ -.up-dir:before { content: "\25b4"; } /* '\25b4' */ -.down-bold:before { content: "\e4b0"; } /* '\e4b0' */ -.left-bold:before { content: "\e4ad"; } /* '\e4ad' */ -.right-bold:before { content: "\e4ae"; } /* '\e4ae' */ -.up-bold:before { content: "\e4af"; } /* '\e4af' */ -.down-thin:before { content: "\2193"; } /* '\2193' */ -.left-thin:before { content: "\2190"; } /* '\2190' */ -.right-thin:before { content: "\2192"; } /* '\2192' */ -.up-thin:before { content: "\2191"; } /* '\2191' */ -.ccw:before { content: "\27f2"; } /* '\27f2' */ -.cw:before { content: "\27f3"; } /* '\27f3' */ -.arrows-ccw:before { content: "🔄"; } /* '\1f504' */ -.level-down:before { content: "\21b3"; } /* '\21b3' */ -.level-up:before { content: "\21b0"; } /* '\21b0' */ -.shuffle:before { content: "🔀"; } /* '\1f500' */ -.loop:before { content: "🔁"; } /* '\1f501' */ -.switch:before { content: "\21c6"; } /* '\21c6' */ -.play:before { content: "\25b6"; } /* '\25b6' */ -.stop:before { content: "\25a0"; } /* '\25a0' */ -.pause:before { content: "\2389"; } /* '\2389' */ -.record:before { content: "\26ab"; } /* '\26ab' */ -.to-end:before { content: "\23ed"; } /* '\23ed' */ -.to-start:before { content: "\23ee"; } /* '\23ee' */ -.fast-forward:before { content: "\23e9"; } /* '\23e9' */ -.fast-backward:before { content: "\23ea"; } /* '\23ea' */ -.progress-0:before { content: "\e768"; } /* '\e768' */ -.progress-1:before { content: "\e769"; } /* '\e769' */ -.progress-2:before { content: "\e76a"; } /* '\e76a' */ -.progress-3:before { content: "\e76b"; } /* '\e76b' */ -.target:before { content: "🎯"; } /* '\1f3af' */ -.palette:before { content: "🎨"; } /* '\1f3a8' */ -.list:before { content: "\e005"; } /* '\e005' */ -.list-add:before { content: "\e003"; } /* '\e003' */ -.signal:before { content: "📶"; } /* '\1f4f6' */ -.trophy:before { content: "🏆"; } /* '\1f3c6' */ -.battery:before { content: "🔋"; } /* '\1f50b' */ -.back-in-time:before { content: "\e771"; } /* '\e771' */ -.monitor:before { content: "💻"; } /* '\1f4bb' */ -.mobile:before { content: "📱"; } /* '\1f4f1' */ -.network:before { content: "\e776"; } /* '\e776' */ -.cd:before { content: "💿"; } /* '\1f4bf' */ -.inbox:before { content: "\e777"; } /* '\e777' */ -.install:before { content: "\e778"; } /* '\e778' */ -.globe:before { content: "🌎"; } /* '\1f30e' */ -.cloud:before { content: "\2601"; } /* '\2601' */ -.cloud-thunder:before { content: "\26c8"; } /* '\26c8' */ -.flash:before { content: "\26a1"; } /* '\26a1' */ -.moon:before { content: "\263d"; } /* '\263d' */ -.flight:before { content: "\2708"; } /* '\2708' */ -.paper-plane:before { content: "\e79b"; } /* '\e79b' */ -.leaf:before { content: "🍂"; } /* '\1f342' */ -.lifebuoy:before { content: "\e788"; } /* '\e788' */ -.mouse:before { content: "\e789"; } /* '\e789' */ -.briefcase:before { content: "💼"; } /* '\1f4bc' */ -.suitcase:before { content: "\e78e"; } /* '\e78e' */ -.dot:before { content: "\e78b"; } /* '\e78b' */ -.dot-2:before { content: "\e78c"; } /* '\e78c' */ -.dot-3:before { content: "\e78d"; } /* '\e78d' */ -.brush:before { content: "\e79a"; } /* '\e79a' */ -.magnet:before { content: "\e7a1"; } /* '\e7a1' */ -.infinity:before { content: "\221e"; } /* '\221e' */ -.erase:before { content: "\232b"; } /* '\232b' */ -.chart-pie:before { content: "\e751"; } /* '\e751' */ -.chart-line:before { content: "📈"; } /* '\1f4c8' */ -.chart-bar:before { content: "📊"; } /* '\1f4ca' */ -.chart-area:before { content: "🔾"; } /* '\1f53e' */ -.tape:before { content: "\2707"; } /* '\2707' */ -.graduation-cap:before { content: "🎓"; } /* '\1f393' */ -.language:before { content: "\e752"; } /* '\e752' */ -.ticket:before { content: "🎫"; } /* '\1f3ab' */ -.water:before { content: "💦"; } /* '\1f4a6' */ -.droplet:before { content: "💧"; } /* '\1f4a7' */ -.air:before { content: "\e753"; } /* '\e753' */ -.credit-card:before { content: "💳"; } /* '\1f4b3' */ -.floppy:before { content: "💾"; } /* '\1f4be' */ -.clipboard:before { content: "📋"; } /* '\1f4cb' */ -.megaphone:before { content: "📣"; } /* '\1f4e3' */ -.database:before { content: "\e754"; } /* '\e754' */ -.drive:before { content: "\e755"; } /* '\e755' */ -.bucket:before { content: "\e756"; } /* '\e756' */ -.thermometer:before { content: "\e757"; } /* '\e757' */ -.key:before { content: "🔑"; } /* '\1f511' */ -.flow-cascade:before { content: "\e790"; } /* '\e790' */ -.flow-branch:before { content: "\e791"; } /* '\e791' */ -.flow-tree:before { content: "\e792"; } /* '\e792' */ -.flow-line:before { content: "\e793"; } /* '\e793' */ -.flow-parallel:before { content: "\e794"; } /* '\e794' */ -.rocket:before { content: "🚀"; } /* '\1f680' */ -.gauge:before { content: "\e7a2"; } /* '\e7a2' */ -.traffic-cone:before { content: "\e7a3"; } /* '\e7a3' */ -.cc:before { content: "\e7a5"; } /* '\e7a5' */ -.cc-by:before { content: "\e7a6"; } /* '\e7a6' */ -.cc-nc:before { content: "\e7a7"; } /* '\e7a7' */ -.cc-nc-eu:before { content: "\e7a8"; } /* '\e7a8' */ -.cc-nc-jp:before { content: "\e7a9"; } /* '\e7a9' */ -.cc-sa:before { content: "\e7aa"; } /* '\e7aa' */ -.cc-nd:before { content: "\e7ab"; } /* '\e7ab' */ -.cc-pd:before { content: "\e7ac"; } /* '\e7ac' */ -.cc-zero:before { content: "\e7ad"; } /* '\e7ad' */ -.cc-share:before { content: "\e7ae"; } /* '\e7ae' */ -.cc-remix:before { content: "\e7af"; } /* '\e7af' */ -.github:before { content: "\f300"; } /* '\f300' */ -.github-circled:before { content: "\f301"; } /* '\f301' */ -.flickr:before { content: "\f303"; } /* '\f303' */ -.flickr-circled:before { content: "\f304"; } /* '\f304' */ -.vimeo:before { content: "\f306"; } /* '\f306' */ -.vimeo-circled:before { content: "\f307"; } /* '\f307' */ -.twitter:before { content: "\f309"; } /* '\f309' */ -.twitter-circled:before { content: "\f30a"; } /* '\f30a' */ -.facebook:before { content: "\f30c"; } /* '\f30c' */ -.facebook-circled:before { content: "\f30d"; } /* '\f30d' */ -.facebook-squared:before { content: "\f30e"; } /* '\f30e' */ -.gplus:before { content: "\f30f"; } /* '\f30f' */ -.gplus-circled:before { content: "\f310"; } /* '\f310' */ -.pinterest:before { content: "\f312"; } /* '\f312' */ -.pinterest-circled:before { content: "\f313"; } /* '\f313' */ -.tumblr:before { content: "\f315"; } /* '\f315' */ -.tumblr-circled:before { content: "\f316"; } /* '\f316' */ -.linkedin:before { content: "\f318"; } /* '\f318' */ -.linkedin-circled:before { content: "\f319"; } /* '\f319' */ -.dribbble:before { content: "\f31b"; } /* '\f31b' */ -.dribbble-circled:before { content: "\f31c"; } /* '\f31c' */ -.stumbleupon:before { content: "\f31e"; } /* '\f31e' */ -.stumbleupon-circled:before { content: "\f31f"; } /* '\f31f' */ -.lastfm:before { content: "\f321"; } /* '\f321' */ -.lastfm-circled:before { content: "\f322"; } /* '\f322' */ -.rdio:before { content: "\f324"; } /* '\f324' */ -.rdio-circled:before { content: "\f325"; } /* '\f325' */ -.spotify:before { content: "\f327"; } /* '\f327' */ -.spotify-circled:before { content: "\f328"; } /* '\f328' */ -.qq:before { content: "\f32a"; } /* '\f32a' */ -.instagrem:before { content: "\f32d"; } /* '\f32d' */ -.dropbox:before { content: "\f330"; } /* '\f330' */ -.evernote:before { content: "\f333"; } /* '\f333' */ -.flattr:before { content: "\f336"; } /* '\f336' */ -.skype:before { content: "\f339"; } /* '\f339' */ -.skype-circled:before { content: "\f33a"; } /* '\f33a' */ -.renren:before { content: "\f33c"; } /* '\f33c' */ -.sina-weibo:before { content: "\f33f"; } /* '\f33f' */ -.paypal:before { content: "\f342"; } /* '\f342' */ -.picasa:before { content: "\f345"; } /* '\f345' */ -.soundcloud:before { content: "\f348"; } /* '\f348' */ -.mixi:before { content: "\f34b"; } /* '\f34b' */ -.behance:before { content: "\f34e"; } /* '\f34e' */ -.google-circles:before { content: "\f351"; } /* '\f351' */ -.vkontakte:before { content: "\f354"; } /* '\f354' */ -.smashing:before { content: "\f357"; } /* '\f357' */ -.sweden:before { content: "\f601"; } /* '\f601' */ -.db-shape:before { content: "\f600"; } /* '\f600' */ -.logo-db:before { content: "\f603"; } /* '\f603' */ +.bold:before { content: '\e800'; } /* '' */ +.italic:before { content: '\e801'; } /* '' */ +.list:before { content: '\e802'; } /* '' */ +.font:before { content: '\e803'; } /* '' */ +.text-height:before { content: '\e804'; } /* '' */ +.text-width:before { content: '\e805'; } /* '' */ +.align-left:before { content: '\e806'; } /* '' */ +.align-center:before { content: '\e807'; } /* '' */ +.align-right:before { content: '\e808'; } /* '' */ +.align-justify:before { content: '\e809'; } /* '' */ +.indent-left:before { content: '\e80a'; } /* '' */ +.indent-right:before { content: '\e80b'; } /* '' */ +.note:before { content: '\e80c'; } /* '' */ +.note-beamed:before { content: '\e80d'; } /* '' */ +.music:before { content: '\e80e'; } /* '' */ +.search:before { content: '\e80f'; } /* '' */ +.flashlight:before { content: '\e810'; } /* '' */ +.mail:before { content: '\e811'; } /* '' */ +.heart:before { content: '\e812'; } /* '' */ +.heart-empty:before { content: '\e813'; } /* '' */ +.star:before { content: '\e814'; } /* '' */ +.star-empty:before { content: '\e815'; } /* '' */ +.user:before { content: '\e816'; } /* '' */ +.users:before { content: '\e817'; } /* '' */ +.user-add:before { content: '\e818'; } /* '' */ +.video-alt:before { content: '\e819'; } /* '' */ +.picture-alt:before { content: '\e81a'; } /* '' */ +.camera:before { content: '\e81b'; } /* '' */ +.layout:before { content: '\e81c'; } /* '' */ +.menu:before { content: '\e81d'; } /* '' */ +.check:before { content: '\e81e'; } /* '' */ +.cancel:before { content: '\e81f'; } /* '' */ +.leaf:before { content: '\e820'; } /* '' */ +.lifebuoy:before { content: '\e821'; } /* '' */ +.water:before { content: '\e822'; } /* '' */ +.droplet:before { content: '\e823'; } /* '' */ +.cc:before { content: '\e824'; } /* '' */ +.cc-by:before { content: '\e825'; } /* '' */ +.lamp:before { content: '\e826'; } /* '' */ +.light-down:before { content: '\e827'; } /* '' */ +.light-up:before { content: '\e828'; } /* '' */ +.adjust:before { content: '\e829'; } /* '' */ +.block:before { content: '\e82a'; } /* '' */ +.resize-full:before { content: '\e82b'; } /* '' */ +.resize-small:before { content: '\e82c'; } /* '' */ +.popup:before { content: '\e82d'; } /* '' */ +.publish:before { content: '\e82e'; } /* '' */ +.window:before { content: '\e82f'; } /* '' */ +.arrow-combo:before { content: '\e830'; } /* '' */ +.down-circled:before { content: '\e831'; } /* '' */ +.left-circled:before { content: '\e832'; } /* '' */ +.right-circled:before { content: '\e833'; } /* '' */ +.up-circled:before { content: '\e834'; } /* '' */ +.down-open:before { content: '\e835'; } /* '' */ +.left-open:before { content: '\e836'; } /* '' */ +.right-open:before { content: '\e837'; } /* '' */ +.up-open:before { content: '\e838'; } /* '' */ +.down-open-mini:before { content: '\e839'; } /* '' */ +.left-open-mini:before { content: '\e83a'; } /* '' */ +.right-open-mini:before { content: '\e83b'; } /* '' */ +.up-open-mini:before { content: '\e83c'; } /* '' */ +.down-open-big:before { content: '\e83d'; } /* '' */ +.left-open-big:before { content: '\e83e'; } /* '' */ +.right-open-big:before { content: '\e83f'; } /* '' */ +.up-open-big:before { content: '\e840'; } /* '' */ +.down:before { content: '\e841'; } /* '' */ +.left:before { content: '\e842'; } /* '' */ +.right:before { content: '\e843'; } /* '' */ +.up:before { content: '\e844'; } /* '' */ +.down-dir:before { content: '\e845'; } /* '' */ +.left-dir:before { content: '\e846'; } /* '' */ +.right-dir:before { content: '\e847'; } /* '' */ +.up-dir:before { content: '\e848'; } /* '' */ +.down-bold:before { content: '\e849'; } /* '' */ +.left-bold:before { content: '\e84a'; } /* '' */ +.right-bold:before { content: '\e84b'; } /* '' */ +.up-bold:before { content: '\e84c'; } /* '' */ +.down-thin:before { content: '\e84d'; } /* '' */ +.left-thin:before { content: '\e84e'; } /* '' */ +.right-thin:before { content: '\e84f'; } /* '' */ +.up-thin:before { content: '\e850'; } /* '' */ +.ccw:before { content: '\e851'; } /* '' */ +.cw:before { content: '\e852'; } /* '' */ +.arrows-ccw:before { content: '\e853'; } /* '' */ +.level-down:before { content: '\e854'; } /* '' */ +.level-up:before { content: '\e855'; } /* '' */ +.shuffle:before { content: '\e856'; } /* '' */ +.loop:before { content: '\e857'; } /* '' */ +.switch:before { content: '\e858'; } /* '' */ +.play:before { content: '\e859'; } /* '' */ +.stop:before { content: '\e85a'; } /* '' */ +.pause:before { content: '\e85b'; } /* '' */ +.record:before { content: '\e85c'; } /* '' */ +.to-end:before { content: '\e85d'; } /* '' */ +.to-start:before { content: '\e85e'; } /* '' */ +.fast-forward:before { content: '\e85f'; } /* '' */ +.fast-backward:before { content: '\e860'; } /* '' */ +.progress-0:before { content: '\e861'; } /* '' */ +.progress-alt:before { content: '\e862'; } /* '' */ +.progress-2:before { content: '\e863'; } /* '' */ +.progress-3:before { content: '\e864'; } /* '' */ +.target:before { content: '\e865'; } /* '' */ +.palette:before { content: '\e866'; } /* '' */ +.list-alt:before { content: '\e867'; } /* '' */ +.list-add:before { content: '\e868'; } /* '' */ +.signal:before { content: '\e869'; } /* '' */ +.trophy:before { content: '\e86a'; } /* '' */ +.battery:before { content: '\e86b'; } /* '' */ +.back-in-time:before { content: '\e86c'; } /* '' */ +.monitor:before { content: '\e86d'; } /* '' */ +.mobile:before { content: '\e86e'; } /* '' */ +.network:before { content: '\e86f'; } /* '' */ +.cd:before { content: '\e870'; } /* '' */ +.inbox:before { content: '\e871'; } /* '' */ +.install:before { content: '\e872'; } /* '' */ +.globe:before { content: '\e873'; } /* '' */ +.cloud:before { content: '\e874'; } /* '' */ +.cloud-thunder:before { content: '\e875'; } /* '' */ +.flash:before { content: '\e876'; } /* '' */ +.moon:before { content: '\e877'; } /* '' */ +.mouse:before { content: '\e878'; } /* '' */ +.briefcase:before { content: '\e879'; } /* '' */ +.suitcase:before { content: '\e87a'; } /* '' */ +.dot:before { content: '\e87b'; } /* '' */ +.dot-2:before { content: '\e87c'; } /* '' */ +.dot-3:before { content: '\e87d'; } /* '' */ +.brush:before { content: '\e87e'; } /* '' */ +.magnet:before { content: '\e87f'; } /* '' */ +.infinity:before { content: '\e880'; } /* '' */ +.erase:before { content: '\e881'; } /* '' */ +.chart-pie:before { content: '\e882'; } /* '' */ +.chart-line:before { content: '\e883'; } /* '' */ +.chart-bar:before { content: '\e884'; } /* '' */ +.chart-area:before { content: '\e885'; } /* '' */ +.tape:before { content: '\e886'; } /* '' */ +.graduation-cap:before { content: '\e887'; } /* '' */ +.air:before { content: '\e888'; } /* '' */ +.credit-card:before { content: '\e889'; } /* '' */ +.floppy:before { content: '\e88a'; } /* '' */ +.clipboard:before { content: '\e88b'; } /* '' */ +.megaphone:before { content: '\e88c'; } /* '' */ +.database:before { content: '\e88d'; } /* '' */ +.drive:before { content: '\e88e'; } /* '' */ +.bucket:before { content: '\e88f'; } /* '' */ +.thermometer:before { content: '\e890'; } /* '' */ +.key:before { content: '\e891'; } /* '' */ +.flow-cascade:before { content: '\e892'; } /* '' */ +.flow-branch:before { content: '\e893'; } /* '' */ +.flow-tree:before { content: '\e894'; } /* '' */ +.flow-line:before { content: '\e895'; } /* '' */ +.flow-parallel:before { content: '\e896'; } /* '' */ +.rocket:before { content: '\e897'; } /* '' */ +.cc-nc:before { content: '\e898'; } /* '' */ +.cc-nc-eu:before { content: '\e899'; } /* '' */ +.cc-nc-jp:before { content: '\e89a'; } /* '' */ +.cc-sa:before { content: '\e89b'; } /* '' */ +.cc-nd:before { content: '\e89c'; } /* '' */ +.cc-pd:before { content: '\e89d'; } /* '' */ +.cc-zero:before { content: '\e89e'; } /* '' */ +.cc-share:before { content: '\e89f'; } /* '' */ +.cc-remix:before { content: '\e8a0'; } /* '' */ +.flight:before { content: '\e8a1'; } /* '' */ +.paper-plane:before { content: '\e8a2'; } /* '' */ +.language:before { content: '\e8a3'; } /* '' */ +.ticket:before { content: '\e8a4'; } /* '' */ +.gauge:before { content: '\e8a5'; } /* '' */ +.traffic-cone:before { content: '\e8a6'; } /* '' */ +.cancel-circled:before { content: '\e8a7'; } /* '' */ +.cancel-squared:before { content: '\e8a8'; } /* '' */ +.plus:before { content: '\e8a9'; } /* '' */ +.plus-circled:before { content: '\e8aa'; } /* '' */ +.plus-squared:before { content: '\e8ab'; } /* '' */ +.minus:before { content: '\e8ac'; } /* '' */ +.minus-circled:before { content: '\e8ad'; } /* '' */ +.minus-squared:before { content: '\e8ae'; } /* '' */ +.help:before { content: '\e8af'; } /* '' */ +.help-circled:before { content: '\e8b0'; } /* '' */ +.info:before { content: '\e8b1'; } /* '' */ +.info-circled:before { content: '\e8b2'; } /* '' */ +.back:before { content: '\e8b3'; } /* '' */ +.home:before { content: '\e8b4'; } /* '' */ +.link:before { content: '\e8b5'; } /* '' */ +.attach:before { content: '\e8b6'; } /* '' */ +.lock:before { content: '\e8b7'; } /* '' */ +.lock-open:before { content: '\e8b8'; } /* '' */ +.eye:before { content: '\e8b9'; } /* '' */ +.tag:before { content: '\e8ba'; } /* '' */ +.bookmark:before { content: '\e8bb'; } /* '' */ +.bookmarks:before { content: '\e8bc'; } /* '' */ +.flag:before { content: '\e8bd'; } /* '' */ +.thumbs-up:before { content: '\e8be'; } /* '' */ +.thumbs-down:before { content: '\e8bf'; } /* '' */ +.download:before { content: '\e8c0'; } /* '' */ +.upload:before { content: '\e8c1'; } /* '' */ +.upload-cloud:before { content: '\e8c2'; } /* '' */ +.reply:before { content: '\e8c3'; } /* '' */ +.reply-all:before { content: '\e8c4'; } /* '' */ +.forward:before { content: '\e8c5'; } /* '' */ +.quote:before { content: '\e8c6'; } /* '' */ +.code:before { content: '\e8c7'; } /* '' */ +.export:before { content: '\e8c8'; } /* '' */ +.pencil:before { content: '\e8c9'; } /* '' */ +.feather:before { content: '\e8ca'; } /* '' */ +.print:before { content: '\e8cb'; } /* '' */ +.retweet:before { content: '\e8cc'; } /* '' */ +.keyboard:before { content: '\e8cd'; } /* '' */ +.comment:before { content: '\e8ce'; } /* '' */ +.chat:before { content: '\e8cf'; } /* '' */ +.bell:before { content: '\e8d0'; } /* '' */ +.attention:before { content: '\e8d1'; } /* '' */ +.alert:before { content: '\e8d2'; } /* '' */ +.vcard:before { content: '\e8d3'; } /* '' */ +.address:before { content: '\e8d4'; } /* '' */ +.location:before { content: '\e8d5'; } /* '' */ +.map:before { content: '\e8d6'; } /* '' */ +.direction:before { content: '\e8d7'; } /* '' */ +.compass:before { content: '\e8d8'; } /* '' */ +.cup:before { content: '\e8d9'; } /* '' */ +.trash:before { content: '\e8da'; } /* '' */ +.doc:before { content: '\e8db'; } /* '' */ +.docs:before { content: '\e8dc'; } /* '' */ +.doc-landscape:before { content: '\e8dd'; } /* '' */ +.doc-text:before { content: '\e8de'; } /* '' */ +.doc-text-inv:before { content: '\e8df'; } /* '' */ +.newspaper:before { content: '\e8e0'; } /* '' */ +.book-open:before { content: '\e8e1'; } /* '' */ +.book:before { content: '\e8e2'; } /* '' */ +.folder:before { content: '\e8e3'; } /* '' */ +.archive:before { content: '\e8e4'; } /* '' */ +.box:before { content: '\e8e5'; } /* '' */ +.rss:before { content: '\e8e6'; } /* '' */ +.phone:before { content: '\e8e7'; } /* '' */ +.cog:before { content: '\e8e8'; } /* '' */ +.tools:before { content: '\e8e9'; } /* '' */ +.share:before { content: '\e8ea'; } /* '' */ +.shareable:before { content: '\e8eb'; } /* '' */ +.basket:before { content: '\e8ec'; } /* '' */ +.bag:before { content: '\e8ed'; } /* '' */ +.calendar:before { content: '\e8ee'; } /* '' */ +.login:before { content: '\e8ef'; } /* '' */ +.logout:before { content: '\e8f0'; } /* '' */ +.mic:before { content: '\e8f1'; } /* '' */ +.mute:before { content: '\e8f2'; } /* '' */ +.sound:before { content: '\e8f3'; } /* '' */ +.volume:before { content: '\e8f4'; } /* '' */ +.clock:before { content: '\e8f5'; } /* '' */ +.hourglass:before { content: '\e8f6'; } /* '' */ +.link-alt:before { content: '\e8f7'; } /* '' */ +.video:before { content: '\e8f8'; } /* '' */ +.h1:before { content: '\e8f9'; } /* '' */ +.picture:before { content: '\e8fa'; } /* '' */ +.scissors:before { content: '\e8fb'; } /* '' */ +.h2:before { content: '\e8fc'; } /* '' */ +.list-bullet:before { content: '\f0ca'; } /* '' */ +.list-numbered:before { content: '\f0cb'; } /* '' */ +.strike:before { content: '\f0cc'; } /* '' */ +.underline:before { content: '\f0cd'; } /* '' */ +.table:before { content: '\f0ce'; } /* '' */ +.columns:before { content: '\f0db'; } /* '' */ +.paste:before { content: '\f0ea'; } /* '' */ +.quote-left:before { content: '\f10d'; } /* '' */ +.quote-right:before { content: '\f10e'; } /* '' */ +.code-alt:before { content: '\f121'; } /* '' */ +.crop:before { content: '\f125'; } /* '' */ +.unlink:before { content: '\f127'; } /* '' */ +.superscript:before { content: '\f12b'; } /* '' */ +.subscript:before { content: '\f12c'; } /* '' */ +.header:before { content: '\f1dc'; } /* '' */ +.paragraph:before { content: '\f1dd'; } /* '' */ +.github:before { content: '\f300'; } /* '' */ +.github-circled:before { content: '\f301'; } /* '' */ +.flickr:before { content: '\f303'; } /* '' */ +.flickr-circled:before { content: '\f304'; } /* '' */ +.vimeo:before { content: '\f306'; } /* '' */ +.vimeo-circled:before { content: '\f307'; } /* '' */ +.twitter:before { content: '\f309'; } /* '' */ +.twitter-circled:before { content: '\f30a'; } /* '' */ +.facebook:before { content: '\f30c'; } /* '' */ +.facebook-circled:before { content: '\f30d'; } /* '' */ +.facebook-squared:before { content: '\f30e'; } /* '' */ +.gplus:before { content: '\f30f'; } /* '' */ +.gplus-circled:before { content: '\f310'; } /* '' */ +.pinterest:before { content: '\f312'; } /* '' */ +.pinterest-circled:before { content: '\f313'; } /* '' */ +.tumblr:before { content: '\f315'; } /* '' */ +.tumblr-circled:before { content: '\f316'; } /* '' */ +.linkedin:before { content: '\f318'; } /* '' */ +.linkedin-circled:before { content: '\f319'; } /* '' */ +.dribbble:before { content: '\f31b'; } /* '' */ +.dribbble-circled:before { content: '\f31c'; } /* '' */ +.stumbleupon:before { content: '\f31e'; } /* '' */ +.stumbleupon-circled:before { content: '\f31f'; } /* '' */ +.lastfm:before { content: '\f321'; } /* '' */ +.lastfm-circled:before { content: '\f322'; } /* '' */ +.rdio:before { content: '\f324'; } /* '' */ +.rdio-circled:before { content: '\f325'; } /* '' */ +.spotify:before { content: '\f327'; } /* '' */ +.spotify-circled:before { content: '\f328'; } /* '' */ +.qq:before { content: '\f32a'; } /* '' */ +.instagram:before { content: '\f32d'; } /* '' */ +.dropbox:before { content: '\f330'; } /* '' */ +.evernote:before { content: '\f333'; } /* '' */ +.flattr:before { content: '\f336'; } /* '' */ +.skype:before { content: '\f339'; } /* '' */ +.skype-circled:before { content: '\f33a'; } /* '' */ +.renren:before { content: '\f33c'; } /* '' */ +.sina-weibo:before { content: '\f33f'; } /* '' */ +.paypal:before { content: '\f342'; } /* '' */ +.picasa:before { content: '\f345'; } /* '' */ +.soundcloud:before { content: '\f348'; } /* '' */ +.mixi:before { content: '\f34b'; } /* '' */ +.behance:before { content: '\f34e'; } /* '' */ +.google-circles:before { content: '\f351'; } /* '' */ +.vkontakte:before { content: '\f354'; } /* '' */ +.smashing:before { content: '\f357'; } /* '' */ +.db-shape:before { content: '\f600'; } /* '' */ +.sweden:before { content: '\f601'; } /* '' */ +.logo-db:before { content: '\f603'; } /* '' */ diff --git a/src/components/UI/icon/Icon.js b/src/components/UI/icon/Icon.js index 07a56d47..a084f9ec 100644 --- a/src/components/UI/icon/Icon.js +++ b/src/components/UI/icon/Icon.js @@ -2,6 +2,12 @@ import React from 'react'; import styles from './Icon.css'; const availableIcons = [ + // Font Awesome Editor Icons + 'bold', 'italic', 'list', 'font', 'text-height', 'text-width', 'align-left', 'align-center', 'align-right', + 'align-justify', 'indent-left', 'indent-right', 'list-bullet', 'list-numbered', 'strike', 'underline', 'table', + 'superscript', 'subscript', 'header', 'h1', 'h2', 'paragraph', 'link', 'unlink', 'quote-left', 'quote-right', 'code', + 'picture','video', + // Entypo 'note', 'note-beamed', 'music', 'search', @@ -10,8 +16,8 @@ const availableIcons = [ 'heart', 'heart-empty', 'star', 'star-empty', 'user', 'users', 'user-add', - 'video', - 'picture', + 'video-alt', + 'picture-alt', 'camera', 'layout', 'menu', @@ -23,7 +29,7 @@ const availableIcons = [ 'info', 'info-circled', 'back', 'home', - 'link', + 'link-alt', 'attach', 'lock', 'lock-open', 'eye', @@ -33,7 +39,7 @@ const availableIcons = [ 'thumbs-up', 'thumbs-down', 'download', 'upload', 'upload-cloud', 'reply', 'reply-all', 'forward', 'quote', - 'code', + 'code-alt', 'export', 'pencil', 'feather', @@ -191,8 +197,10 @@ const iconPropType = (props, propName) => { } }; -export default function Icon({ style, className = '', type }) { - return ; +const noop = function() {}; + +export default function Icon({ style, className = '', type, onClick = noop}) { + return ; } Icon.propTypes = { diff --git a/src/components/UI/icon/entypo.eot b/src/components/UI/icon/entypo.eot deleted file mode 100644 index 41f223e4..00000000 Binary files a/src/components/UI/icon/entypo.eot and /dev/null differ diff --git a/src/components/UI/icon/entypo.svg b/src/components/UI/icon/entypo.svg deleted file mode 100644 index 86b3b308..00000000 --- a/src/components/UI/icon/entypo.svg +++ /dev/null @@ -1,834 +0,0 @@ - - - - -Created by FontForge 20110222 at Sun Nov 11 15:34:13 2012 - By Vitaly,,, -Copyright (C) 2012 by Daniel Bruce - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/src/components/UI/icon/entypo.ttf b/src/components/UI/icon/entypo.ttf deleted file mode 100644 index 331ea3a1..00000000 Binary files a/src/components/UI/icon/entypo.ttf and /dev/null differ diff --git a/src/components/UI/icon/entypo.woff b/src/components/UI/icon/entypo.woff deleted file mode 100644 index b0771de8..00000000 Binary files a/src/components/UI/icon/entypo.woff and /dev/null differ diff --git a/src/components/UI/icon/icons.eot b/src/components/UI/icon/icons.eot new file mode 100755 index 00000000..76ace2d0 Binary files /dev/null and b/src/components/UI/icon/icons.eot differ diff --git a/src/components/UI/icon/icons.svg b/src/components/UI/icon/icons.svg new file mode 100755 index 00000000..825440f7 --- /dev/null +++ b/src/components/UI/icon/icons.svg @@ -0,0 +1,646 @@ + + + +Copyright (C) 2016 by original authors @ fontello.com + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/UI/icon/icons.ttf b/src/components/UI/icon/icons.ttf new file mode 100755 index 00000000..f764608b Binary files /dev/null and b/src/components/UI/icon/icons.ttf differ diff --git a/src/components/UI/icon/icons.woff b/src/components/UI/icon/icons.woff new file mode 100755 index 00000000..c7c39828 Binary files /dev/null and b/src/components/UI/icon/icons.woff differ diff --git a/src/components/UI/icon/icons.woff2 b/src/components/UI/icon/icons.woff2 new file mode 100755 index 00000000..08903e52 Binary files /dev/null and b/src/components/UI/icon/icons.woff2 differ diff --git a/src/components/Widgets/MarkdownControl.js b/src/components/Widgets/MarkdownControl.js index 20210b9f..a315cb80 100644 --- a/src/components/Widgets/MarkdownControl.js +++ b/src/components/Widgets/MarkdownControl.js @@ -1,45 +1,85 @@ import React, { PropTypes } from 'react'; -import { Editor, EditorState, RichUtils } from 'draft-js'; -import { stateToMarkdown } from 'draft-js-export-markdown'; -import { stateFromMarkdown } from 'draft-js-import-markdown'; +import RawEditor from './MarkdownControlElements/RawEditor'; +import VisualEditor from './MarkdownControlElements/VisualEditor'; +import { processEditorPlugins } from './richText'; +import { connect } from 'react-redux'; +import { switchVisualMode } from '../../actions/editor'; -export default class MarkdownControl extends React.Component { - constructor(props) { - super(props); - this.state = { - editorState: EditorState.createWithContent(stateFromMarkdown(props.value || '')) - }; - this.handleChange = this.handleChange.bind(this); - this.handleKeyCommand = this.handleKeyCommand.bind(this); +class MarkdownControl extends React.Component { + constructor(props, context) { + super(props, context); + this.useVisualEditor = this.useVisualEditor.bind(this); + this.useRawEditor = this.useRawEditor.bind(this); } - handleChange(editorState) { - const content = editorState.getCurrentContent(); - this.setState({ editorState }); - this.props.onChange(stateToMarkdown(content)); + componentWillMount() { + processEditorPlugins(this.context.plugins.editor); } - handleKeyCommand(command) { - const newState = RichUtils.handleKeyCommand(this.state.editorState, command); - if (newState) { - this.handleChange(newState); - return true; + useVisualEditor() { + this.props.switchVisualMode(true); + } + + useRawEditor() { + this.props.switchVisualMode(false); + } + + renderEditor() { + const { editor, onChange, onAddMedia, getMedia, value } = this.props; + if (editor.get('useVisualMode')) { + return ( +
+ + +
+ ); + } else { + return ( +
+ + +
+ ); } - return false; } render() { - const { editorState } = this.state; return ( - ); +
+ + { this.renderEditor() } +
+ ); } } +export default MarkdownControl; + MarkdownControl.propTypes = { + editor: PropTypes.object.isRequired, onChange: PropTypes.func.isRequired, + onAddMedia: PropTypes.func.isRequired, + getMedia: PropTypes.func.isRequired, + switchVisualMode: PropTypes.func.isRequired, value: PropTypes.node, }; + +MarkdownControl.contextTypes = { + plugins: PropTypes.object, +}; + +export default connect( + state => ({ editor: state.editor }), + { switchVisualMode } +)(MarkdownControl); diff --git a/src/components/Widgets/MarkdownControlElements/RawEditor/index.css b/src/components/Widgets/MarkdownControlElements/RawEditor/index.css new file mode 100644 index 00000000..e69de29b diff --git a/src/components/Widgets/MarkdownControlElements/RawEditor/index.js b/src/components/Widgets/MarkdownControlElements/RawEditor/index.js new file mode 100644 index 00000000..175f2471 --- /dev/null +++ b/src/components/Widgets/MarkdownControlElements/RawEditor/index.js @@ -0,0 +1,125 @@ +import React, { PropTypes } from 'react'; +import { Editor, Plain, Mark } from 'slate'; +import Prism from 'prismjs'; +import marks from './prismMarkdown'; +import styles from './index.css'; + + +Prism.languages.markdown = Prism.languages.extend('markup', {}); +Prism.languages.insertBefore('markdown', 'prolog', marks); +Prism.languages.markdown['bold'].inside['url'] = Prism.util.clone(Prism.languages.markdown['url']); +Prism.languages.markdown['italic'].inside['url'] = Prism.util.clone(Prism.languages.markdown['url']); +Prism.languages.markdown['bold'].inside['italic'] = Prism.util.clone(Prism.languages.markdown['italic']); +Prism.languages.markdown['italic'].inside['bold'] = Prism.util.clone(Prism.languages.markdown['bold']); + +function renderDecorations(text, block) { + let characters = text.characters.asMutable(); + const string = text.text; + const grammar = Prism.languages.markdown; + const tokens = Prism.tokenize(string, grammar); + let offset = 0; + + for (const token of tokens) { + if (typeof token == 'string') { + offset += token.length; + continue; + } + + const length = offset + token.matchedStr.length; + const name = token.alias || token.type; + const type = `highlight-${name}`; + + for (let i = offset; i < length; i++) { + let char = characters.get(i); + let { marks } = char; + marks = marks.add(Mark.create({ type })); + char = char.merge({ marks }); + characters = characters.set(i, char); + } + + offset = length; + } + + return characters.asImmutable(); +} + + +const SCHEMA = { + rules: [ + { + match: (object) => object.kind == 'block', + decorate: renderDecorations + } + ], + marks: { + 'highlight-comment': { + opacity: '0.33' + }, + 'highlight-important': { + fontWeight: 'bold', + color: '#006', + }, + 'highlight-keyword': { + fontWeight: 'bold', + color: '#006', + }, + 'highlight-url': { + color: '#006', + }, + 'highlight-punctuation': { + color: '#006', + } + } +}; + +class RawEditor extends React.Component { + + constructor(props) { + super(props); + + const content = props.value ? Plain.deserialize(props.value) : Plain.deserialize(''); + + this.state = { + state: content + }; + + this.handleChange = this.handleChange.bind(this); + this.handleDocumentChange = this.handleDocumentChange.bind(this); + + } + + /** + * Slate keeps track of selections, scroll position etc. + * So, onChange gets dispatched on every interaction (click, arrows, everything...) + * It also have an onDocumentChange, that get's dispached only when the actual + * content changes + */ + handleChange(state) { + this.setState({ state }); + } + + handleDocumentChange(document, state) { + const content = Plain.serialize(state, { terse: true }); + this.props.onChange(content); + } + + render() { + return ( + + ); + } +} + +export default RawEditor; + +RawEditor.propTypes = { + onChange: PropTypes.func.isRequired, + value: PropTypes.node, +}; diff --git a/src/components/Widgets/MarkdownControlElements/RawEditor/prismMarkdown.js b/src/components/Widgets/MarkdownControlElements/RawEditor/prismMarkdown.js new file mode 100644 index 00000000..1ea5e5d3 --- /dev/null +++ b/src/components/Widgets/MarkdownControlElements/RawEditor/prismMarkdown.js @@ -0,0 +1,116 @@ +const marks = { + 'blockquote': { + // > ... + pattern: /^>(?:[\t ]*>)*/m, + alias: 'punctuation' + }, + 'code': [ + { + // Prefixed by 4 spaces or 1 tab + pattern: /^(?: {4}|\t).+/m, + alias: 'keyword' + }, + { + // `code` + // ``code`` + pattern: /``.+?``|`[^`\n]+`/, + alias: 'keyword' + } + ], + 'title': [ + { + // title 1 + // ======= + + // title 2 + // ------- + pattern: /\w+.*(?:\r?\n|\r)(?:==+|--+)/, + alias: 'important', + inside: { + punctuation: /==+$|--+$/ + } + }, + { + // # title 1 + // ###### title 6 + pattern: /(^\s*)#+.+/m, + lookbehind: true, + alias: 'important', + inside: { + punctuation: /^#+|#+$/ + } + } + ], + 'hr': { + // *** + // --- + // * * * + // ----------- + pattern: /(^\s*)([*-])([\t ]*\2){2,}(?=\s*$)/m, + lookbehind: true, + alias: 'punctuation' + }, + 'list': { + // * item + // + item + // - item + // 1. item + pattern: /(^\s*)(?:[*+-]|\d+\.)(?=[\t ].)/m, + lookbehind: true, + alias: 'punctuation' + }, + 'url-reference': { + // [id]: http://example.com "Optional title" + // [id]: http://example.com 'Optional title' + // [id]: http://example.com (Optional title) + // [id]: "Optional title" + pattern: /!?\[[^\]]+\]:[\t ]+(?:\S+|<(?:\\.|[^>\\])+>)(?:[\t ]+(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\)))?/, + inside: { + 'variable': { + pattern: /^(!?\[)[^\]]+/, + lookbehind: true + }, + 'string': /(?:"(?:\\.|[^"\\])*"|'(?:\\.|[^'\\])*'|\((?:\\.|[^)\\])*\))$/, + 'punctuation': /^[\[\]!:]|[<>]/ + }, + alias: 'url' + }, + 'bold': { + // **strong** + // __strong__ + + // Allow only one line break + pattern: /(^|[^\\])(\*\*|__)(?:(?:\r?\n|\r)(?!\r?\n|\r)|.)+?\2/, + lookbehind: true, + inside: { + 'punctuation': /^\*\*|^__|\*\*$|__$/ + } + }, + 'italic': { + // *em* + // _em_ + + // Allow only one line break + pattern: /(^|[^\\])([*_])(?:(?:\r?\n|\r)(?!\r?\n|\r)|.)+?\2/, + lookbehind: true, + inside: { + 'punctuation': /^[*_]|[*_]$/ + } + }, + 'url': { + // [example](http://example.com "Optional title") + // [example] [id] + pattern: /!?\[[^\]]+\](?:\([^\s)]+(?:[\t ]+"(?:\\.|[^"\\])*")?\)| ?\[[^\]\n]*\])/, + inside: { + 'variable': { + pattern: /(!?\[)[^\]]+(?=\]$)/, + lookbehind: true + }, + 'string': { + pattern: /"(?:\\.|[^"\\])*"(?=\)$)/ + } + } + } +}; + +export default marks; diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/Block.css b/src/components/Widgets/MarkdownControlElements/VisualEditor/Block.css new file mode 100644 index 00000000..df0af926 --- /dev/null +++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/Block.css @@ -0,0 +1,71 @@ +.root { + border: dotted 1px #ddd; + position: relative; + margin: 9px 0 15px 0; +} + + +.type:after { + content: attr(data-type); + font-size: 10px; + color: #aaa; + position: absolute; + top : -7px;; + margin-left: 1em; + padding: 0 3px; + display: inline; + background-color: #fafafa; + pointer-events: none; +} + +.body { + padding: 8px; +} + +.body img{ + max-width: 100%; + height: auto; +} + +.Paragraph { + +} + +.Heading1, .Heading2, .Heading3, .Heading4, .Heading5, .Heading6 { + margin: 0; + font-weight: bold +} + +.Heading1 { + font-size: 1.2em; +} + +.Heading2 { + font-size: 1.15em; +} + +.Heading3 { + font-size: 1.1em; +} + +.Heading4 { + font-size: 1.07em; +} + +.Heading5 { + font-size: 1.05em; +} + +.Heading6 { + font-size: 1.03em; +} + +.blockquote { + padding-left: 5px; + border-left: solid 3px #ccc; +} + +.body ul { + padding-left: 20px; + margin: 0; +} diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/Block.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/Block.js new file mode 100644 index 00000000..0b1a3649 --- /dev/null +++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/Block.js @@ -0,0 +1,32 @@ +import React, { PropTypes } from 'react'; +import styles from './Block.css'; + +const AVAILABLE_TYPES = [ + 'Paragraph', + 'Heading1', + 'Heading2', + 'Heading3', + 'Heading4', + 'Heading5', + 'Heading6', + 'List', + 'blockquote' +]; + +export function Block({ type, children }) { + return ( +
+
+
+ {children} +
+
+ ); +} + +Block.propTypes = { + children: PropTypes.node.isRequired, + type: PropTypes.oneOf(AVAILABLE_TYPES).isRequired +}; + +export default Block; diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/BlockTypesMenu.css b/src/components/Widgets/MarkdownControlElements/VisualEditor/BlockTypesMenu.css new file mode 100644 index 00000000..9868af79 --- /dev/null +++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/BlockTypesMenu.css @@ -0,0 +1,32 @@ +.root { + position: absolute; +} + +.button { + margin-top: 2px; + color: #ddd; + transition: color 0.5s ease; + cursor: pointer; +} +.button:hover { + color: #aaa; +} + +.menu { + position: absolute; + top: -5px; + left: 20px; + height: 32px; + white-space: nowrap; + background-color: rgba(126, 126, 126, 0.1); +} + +.icon { + margin: 8px; + cursor: pointer; + color: #555; +} + +.input { + display: none; +} diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/BlockTypesMenu.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/BlockTypesMenu.js new file mode 100644 index 00000000..0855d40b --- /dev/null +++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/BlockTypesMenu.js @@ -0,0 +1,163 @@ +import React, { Component, PropTypes } from 'react'; +import Portal from 'react-portal'; +import { Icon } from '../../../UI'; +import MediaProxy from '../../../../valueObjects/MediaProxy'; +import styles from './BlockTypesMenu.css'; + +export default class BlockTypesMenu extends Component { + constructor(props) { + super(props); + + this.state = { + expanded: false, + menu: null + }; + + this.updateMenuPosition = this.updateMenuPosition.bind(this); + this.toggleMenu = this.toggleMenu.bind(this); + this.handleOpen = this.handleOpen.bind(this); + this.handleBlockTypeClick = this.handleBlockTypeClick.bind(this); + this.handlePluginClick = this.handlePluginClick.bind(this); + this.handleFileUploadClick = this.handleFileUploadClick.bind(this); + this.handleFileUploadChange = this.handleFileUploadChange.bind(this); + this.renderBlockTypeButton = this.renderBlockTypeButton.bind(this); + this.renderPluginButton = this.renderPluginButton.bind(this); + } + + /** + * On update, update the menu. + */ + componentDidMount() { + this.updateMenuPosition(); + } + + componentWillUpdate() { + if (this.state.expanded) { + this.setState({ expanded: false }); + } + } + + componentDidUpdate() { + this.updateMenuPosition(); + } + + updateMenuPosition() { + const { menu } = this.state; + const { position } = this.props; + if (!menu) return; + + menu.style.opacity = 1; + menu.style.top = `${position.top}px`; + menu.style.left = `${position.left - menu.offsetWidth * 2}px`; + + } + + toggleMenu() { + this.setState({ expanded: !this.state.expanded }); + } + + handleBlockTypeClick(e, type) { + this.props.onClickBlock(type); + } + + handlePluginClick(e, plugin) { + const data = {}; + plugin.fields.forEach(field => { + data[field.name] = window.prompt(field.label); + }); + this.props.onClickPlugin(plugin.id, data); + } + + handleFileUploadClick() { + this._fileInput.click(); + } + + handleFileUploadChange(e) { + e.stopPropagation(); + e.preventDefault(); + + const fileList = e.dataTransfer ? e.dataTransfer.files : e.target.files; + const files = [...fileList]; + const imageType = /^image\//; + + // Iterate through the list of files and return the first image on the list + const file = files.find((currentFile) => { + if (imageType.test(currentFile.type)) { + return currentFile; + } + }); + + if (file) { + const mediaProxy = new MediaProxy(file.name, file); + this.props.onClickImage(mediaProxy); + } + + } + + renderBlockTypeButton(type, icon) { + const onClick = e => this.handleBlockTypeClick(e, type); + return ( + + ); + } + + renderPluginButton(plugin) { + const onClick = e => this.handlePluginClick(e, plugin); + return ( + + ); + } + + renderMenu() { + const { plugins } = this.props; + if (this.state.expanded) { + return ( +
+ {this.renderBlockTypeButton('hr', 'dot-3')} + {plugins.map(plugin => this.renderPluginButton(plugin))} + + this._fileInput = el} + /> +
+ ); + } else { + return null; + } + } + + /** + * When the portal opens, cache the menu element. + */ + handleOpen(portal) { + this.setState({ menu: portal.firstChild }); + } + + render() { + const { isOpen } = this.props; + return ( + +
+ + {this.renderMenu()} +
+
+ ); + } +} + +BlockTypesMenu.propTypes = { + isOpen: PropTypes.bool.isRequired, + plugins: PropTypes.array.isRequired, + position: PropTypes.shape({ + top: PropTypes.number.isRequired, + left: PropTypes.number.isRequired + }), + onClickBlock: PropTypes.func.isRequired, + onClickPlugin: PropTypes.func.isRequired, + onClickImage: PropTypes.func.isRequired +}; diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/StylesMenu.css b/src/components/Widgets/MarkdownControlElements/VisualEditor/StylesMenu.css new file mode 100644 index 00000000..c87888af --- /dev/null +++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/StylesMenu.css @@ -0,0 +1,39 @@ + +.button { + color: #ccc; + cursor: pointer; +} + +.button[data-active="true"] { + color: black; +} + + +.menu > * { + display: inline-block; +} + +.menu > * + * { + margin-left: 10px; +} + +.hoverMenu { + padding: 8px 7px 6px; + position: absolute; + z-index: 1; + top: -10000px; + left: -10000px; + margin-top: -6px; + opacity: 0; + background-color: #222; + border-radius: 4px; + transition: opacity .75s; +} + +.hoverMenu .button { + color: #aaa; +} + +.hoverMenu .button[data-active="true"] { + color: #fff; +} diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/StylesMenu.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/StylesMenu.js new file mode 100644 index 00000000..f2aafc3e --- /dev/null +++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/StylesMenu.js @@ -0,0 +1,151 @@ +import React, { Component, PropTypes } from 'react'; +import Portal from 'react-portal'; +import { Icon } from '../../../UI'; +import styles from './StylesMenu.css'; + +export default class StylesMenu extends Component { + + constructor(props) { + super(props); + + this.state = { + menu: null + }; + + this.hasMark = this.hasMark.bind(this); + this.hasBlock = this.hasBlock.bind(this); + this.renderMarkButton = this.renderMarkButton.bind(this); + this.renderBlockButton = this.renderBlockButton.bind(this); + this.renderLinkButton = this.renderLinkButton.bind(this); + this.updateMenuPosition = this.updateMenuPosition.bind(this); + this.handleMarkClick = this.handleMarkClick.bind(this); + this.handleInlineClick = this.handleInlineClick.bind(this); + this.handleBlockClick = this.handleBlockClick.bind(this); + this.handleOpen = this.handleOpen.bind(this); + } + + /** + * On update, update the menu. + */ + componentDidMount() { + this.updateMenuPosition(); + } + + componentDidUpdate() { + this.updateMenuPosition(); + } + + updateMenuPosition() { + const { menu } = this.state; + const { position } = this.props; + if (!menu) return; + + menu.style.opacity = 1; + menu.style.top = `${position.top - menu.offsetHeight}px`; + menu.style.left = `${position.left - menu.offsetWidth / 2 + position.width / 2}px`; + } + + /** + * Used to set toolbar buttons to active state + */ + hasMark(type) { + const { marks } = this.props; + return marks.some(mark => mark.type == type); + } + hasBlock(type) { + const { blocks } = this.props; + return blocks.some(node => node.type == type); + } + hasLinks(type) { + const { inlines } = this.props; + return inlines.some(inline => inline.type == 'link'); + } + + handleMarkClick(e, type) { + e.preventDefault(); + this.props.onClickMark(type); + } + + renderMarkButton(type, icon) { + const isActive = this.hasMark(type); + const onMouseDown = e => this.handleMarkClick(e, type); + return ( + + + + ); + } + + handleInlineClick(e, type, isActive) { + e.preventDefault(); + this.props.onClickInline(type, isActive); + } + + renderLinkButton() { + const isActive = this.hasLinks(); + const onMouseDown = e => this.handleInlineClick(e, 'link', isActive); + return ( + + + + ); + } + + handleBlockClick(e, type) { + e.preventDefault(); + const isActive = this.hasBlock(type); + const isList = this.hasBlock('list-item'); + this.props.onClickBlock(type, isActive, isList); + } + + renderBlockButton(type, icon, checkType) { + checkType = checkType || type; + const isActive = this.hasBlock(checkType); + const onMouseDown = e => this.handleBlockClick(e, type); + return ( + + + + ); + } + + /** + * When the portal opens, cache the menu element. + */ + handleOpen(portal) { + this.setState({ menu: portal.firstChild }); + } + + render() { + const { isOpen } = this.props; + return ( + +
+ {this.renderMarkButton('BOLD', 'bold')} + {this.renderMarkButton('ITALIC', 'italic')} + {this.renderMarkButton('CODE', 'code')} + {this.renderLinkButton()} + {this.renderBlockButton('header_one', 'h1')} + {this.renderBlockButton('header_two', 'h2')} + {this.renderBlockButton('blockquote', 'quote-left')} + {this.renderBlockButton('unordered_list', 'list-bullet', 'list_item')} +
+
+ ); + } + +} + +StylesMenu.propTypes = { + isOpen: PropTypes.bool.isRequired, + position: PropTypes.shape({ + top: PropTypes.number.isRequired, + left: PropTypes.number.isRequired + }), + marks: PropTypes.object.isRequired, + blocks: PropTypes.object.isRequired, + inlines: PropTypes.object.isRequired, + onClickBlock: PropTypes.func.isRequired, + onClickMark: PropTypes.func.isRequired, + onClickInline: PropTypes.func.isRequired +}; diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/index.css b/src/components/Widgets/MarkdownControlElements/VisualEditor/index.css new file mode 100644 index 00000000..6eb82211 --- /dev/null +++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/index.css @@ -0,0 +1,26 @@ +.active { + box-shadow: 0 0 0 2px blue; +} + +:global .plugin { + background-color: #ddd; + color: #555; + text-align: center; + width: 200px; + padding: 8px; + border-radius: 2px; +} + +:global .plugin_icon { + font-size: 50px; + margin: 12px 0; +} + +:global .plugin_fields { + font-size: 11px; + outline:none; +} + +:global .active { + box-shadow: 0 0 0 2px blue; +} diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/index.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/index.js new file mode 100644 index 00000000..b7d57822 --- /dev/null +++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/index.js @@ -0,0 +1,355 @@ +import React, { PropTypes } from 'react'; +import _ from 'lodash'; +import { Editor, Raw } from 'slate'; +import position from 'selection-position'; +import MarkupIt, { SlateUtils } from 'markup-it'; +import { emptyParagraphBlock } from '../constants'; +import { DEFAULT_NODE, SCHEMA } from './schema'; +import { getNodes, getSyntaxes, getPlugins } from '../../richText'; +import StylesMenu from './StylesMenu'; +import BlockTypesMenu from './BlockTypesMenu'; +import styles from './index.css'; + +/** + * Slate Render Configuration + */ +class VisualEditor extends React.Component { + constructor(props) { + super(props); + + this.getMedia = this.getMedia.bind(this); + const MarkdownSyntax = getSyntaxes(this.getMedia).markdown; + this.markdown = new MarkupIt(MarkdownSyntax); + + SCHEMA.nodes = _.merge(SCHEMA.nodes, getNodes()); + + this.blockEdit = false; + this.menuPositions = { + stylesMenu: { + top: 0, + left: 0, + width: 0, + height: 0 + }, + blockTypesMenu: { + top: 0, + left: 0, + width: 0, + height: 0 + } + }; + + let rawJson; + if (props.value !== undefined) { + const content = this.markdown.toContent(props.value); + rawJson = SlateUtils.encode(content, null, getPlugins().map(plugin => plugin.id)); + } else { + rawJson = emptyParagraphBlock; + } + this.state = { + state: Raw.deserialize(rawJson, { terse: true }) + }; + + this.handleChange = this.handleChange.bind(this); + this.handleDocumentChange = this.handleDocumentChange.bind(this); + this.handleMarkStyleClick = this.handleMarkStyleClick.bind(this); + this.handleBlockStyleClick = this.handleBlockStyleClick.bind(this); + this.handleInlineClick = this.handleInlineClick.bind(this); + this.handleBlockTypeClick = this.handleBlockTypeClick.bind(this); + this.handlePluginClick = this.handlePluginClick.bind(this); + this.handleImageClick = this.handleImageClick.bind(this); + this.focusAndAddParagraph = this.focusAndAddParagraph.bind(this); + this.handleKeyDown = this.handleKeyDown.bind(this); + this.calculateHoverMenuPosition = _.throttle(this.calculateHoverMenuPosition.bind(this), 30); + this.calculateBlockMenuPosition = _.throttle(this.calculateBlockMenuPosition.bind(this), 100); + this.renderBlockTypesMenu = this.renderBlockTypesMenu.bind(this); + } + + getMedia(src) { + return this.props.getMedia(src); + } + + /** + * Slate keeps track of selections, scroll position etc. + * So, onChange gets dispatched on every interaction (click, arrows, everything...) + * It also have an onDocumentChange, that get's dispached only when the actual + * content changes + */ + handleChange(state) { + if (this.blockEdit) { + this.blockEdit = false; + } else { + this.calculateHoverMenuPosition(); + this.setState({ state }, this.calculateBlockMenuPosition); + } + } + + handleDocumentChange(document, state) { + const rawJson = Raw.serialize(state, { terse: true }); + const content = SlateUtils.decode(rawJson); + this.props.onChange(this.markdown.toText(content)); + } + + calculateHoverMenuPosition() { + const rect = position(); + this.menuPositions.stylesMenu = { + top: rect.top + window.scrollY, + left: rect.left + window.scrollX, + width: rect.width, + height: rect.height + }; + } + + calculateBlockMenuPosition() { + // Don't bother calculating position if block is not empty + if (this.state.state.blocks.get(0).isEmpty) { + const blockElement = document.querySelectorAll(`[data-key='${this.state.state.selection.focusKey}']`); + if (blockElement.length > 0) { + const rect = blockElement[0].getBoundingClientRect(); + this.menuPositions.blockTypesMenu = { + top: rect.top + window.scrollY, + left: rect.left + window.scrollX + }; + // Force re-render so the menu is positioned on these new coordinates + this.forceUpdate(); + } + } + } + + /** + * Toggle marks / blocks when button is clicked + */ + handleMarkStyleClick(type) { + let { state } = this.state; + + state = state + .transform() + .toggleMark(type) + .apply(); + + this.setState({ state }); + } + + handleBlockStyleClick(type, isActive, isList) { + let { state } = this.state; + let transform = state.transform(); + const { document } = state; + + // Handle everything but list buttons. + if (type != 'unordered_list' && type != 'ordered_list') { + + if (isList) { + transform = transform + .setBlock(isActive ? DEFAULT_NODE : type) + .unwrapBlock('unordered_list') + .unwrapBlock('ordered_list'); + } + + else { + transform = transform + .setBlock(isActive ? DEFAULT_NODE : type); + } + } + + // Handle the extra wrapping required for list buttons. + else { + const isType = state.blocks.some((block) => { + return !!document.getClosest(block, parent => parent.type == type); + }); + + if (isList && isType) { + transform = transform + .setBlock(DEFAULT_NODE) + .unwrapBlock('unordered_list'); + } else if (isList) { + transform = transform + .unwrapBlock(type == 'unordered_list') + .wrapBlock(type); + } else { + transform = transform + .setBlock('list_item') + .wrapBlock(type); + } + } + + state = transform.apply(); + this.setState({ state }); + } + + /** + * When clicking a link, if the selection has a link in it, remove the link. + * Otherwise, add a new link with an href and text. + * + * @param {Event} e + */ + + handleInlineClick(type, isActive) { + let { state } = this.state; + + if (type === 'link') { + if (!state.isExpanded) return; + + if (isActive) { + state = state + .transform() + .unwrapInline('link') + .apply(); + } + + else { + const href = window.prompt('Enter the URL of the link:', 'http://www.'); + state = state + .transform() + .wrapInline({ + type: 'link', + data: { href } + }) + .collapseToEnd() + .apply(); + } + } + this.setState({ state }); + } + + + handleBlockTypeClick(type) { + let { state } = this.state; + + state = state + .transform() + .insertBlock({ + type: type, + isVoid: true + }) + .apply(); + + this.setState({ state }, this.focusAndAddParagraph); + } + + handlePluginClick(type, data) { + let { state } = this.state; + + state = state + .transform() + .insertInline({ + type: type, + data: data, + isVoid: true + }) + .collapseToEnd() + .insertBlock(DEFAULT_NODE) + .focus() + .apply(); + + this.setState({ state }); + } + + handleImageClick(mediaProxy) { + let { state } = this.state; + this.props.onAddMedia(mediaProxy); + + state = state + .transform() + .insertInline({ + type: 'mediaproxy', + isVoid: true, + data: { src: mediaProxy.path } + }) + .collapseToEnd() + .insertBlock(DEFAULT_NODE) + .focus() + .apply(); + + this.setState({ state }); + } + + focusAndAddParagraph() { + const { state } = this.state; + const blocks = state.document.getBlocks(); + const last = blocks.last(); + const normalized = state + .transform() + .focus() + .collapseToEndOf(last) + .splitBlock() + .setBlock(DEFAULT_NODE) + .apply({ + snapshot: false + }); + this.setState({ state:normalized }); + } + + + handleKeyDown(evt) { + if (evt.shiftKey && evt.key === 'Enter') { + this.blockEdit = true; + let { state } = this.state; + state = state + .transform() + .insertText(' \n') + .apply(); + + this.setState({ state }); + } + } + + renderBlockTypesMenu() { + const currentBlock = this.state.state.blocks.get(0); + const isOpen = (this.props.value !== undefined && currentBlock.isEmpty && currentBlock.type !== 'horizontal-rule'); + + return ( + + ); + } + + renderStylesMenu() { + const { state } = this.state; + const isOpen = !(state.isBlurred || state.isCollapsed); + + return ( + + ); + } + + render() { + return ( +
+ {this.renderStylesMenu()} + {this.renderBlockTypesMenu()} + +
+ ); + } +} + +export default VisualEditor; + +VisualEditor.propTypes = { + onChange: PropTypes.func.isRequired, + onAddMedia: PropTypes.func.isRequired, + getMedia: PropTypes.func.isRequired, + value: PropTypes.node, +}; diff --git a/src/components/Widgets/MarkdownControlElements/VisualEditor/schema.js b/src/components/Widgets/MarkdownControlElements/VisualEditor/schema.js new file mode 100644 index 00000000..ea00ade2 --- /dev/null +++ b/src/components/Widgets/MarkdownControlElements/VisualEditor/schema.js @@ -0,0 +1,65 @@ +import React from 'react'; +import Block from './Block'; +import styles from './index.css'; + +/* eslint react/prop-types: 0, react/no-multi-comp: 0 */ + +// Define the default node type. +export const DEFAULT_NODE = 'paragraph'; + +/** + * Define a schema. + * + * @type {Object} + */ + +export const SCHEMA = { + nodes: { + 'blockquote': (props) => {props.children}, + 'unordered_list': props =>
    {props.children}
, + 'header_one': props => {props.children}, + 'header_two': props => {props.children}, + 'header_three': props => {props.children}, + 'header_four': props => {props.children}, + 'header_five': props => {props.children}, + 'header_six': props => {props.children}, + 'list_item': props =>
  • {props.children}
  • , + 'paragraph': props => {props.children}, + 'hr': props => { + const { node, state } = props; + const isFocused = state.selection.hasEdgeIn(node); + const className = isFocused ? styles.active : null; + return ( +
    + ); + }, + 'link': (props) => { + const { data } = props.node; + const href = data.get('href'); + return {props.children}; + }, + 'image': (props) => { + const { node, state } = props; + const isFocused = state.selection.hasEdgeIn(node); + const className = isFocused ? styles.active : null; + const src = node.data.get('src'); + return ( + + ); + } + }, + marks: { + BOLD: { + fontWeight: 'bold' + }, + ITALIC: { + fontStyle: 'italic' + }, + CODE: { + fontFamily: 'monospace', + backgroundColor: '#eee', + padding: '3px', + borderRadius: '4px' + } + } +} diff --git a/src/components/Widgets/MarkdownControlElements/constants.js b/src/components/Widgets/MarkdownControlElements/constants.js new file mode 100644 index 00000000..74779111 --- /dev/null +++ b/src/components/Widgets/MarkdownControlElements/constants.js @@ -0,0 +1,13 @@ +export const emptyParagraphBlock = { + nodes: [ + { kind: 'block', + type: 'paragraph', + nodes: [{ + kind: 'text', + ranges: [{ + text: '' + }] + }] + } + ] +}; diff --git a/src/components/Widgets/MarkdownPreview.js b/src/components/Widgets/MarkdownPreview.js index 37fe084a..f9f0021c 100644 --- a/src/components/Widgets/MarkdownPreview.js +++ b/src/components/Widgets/MarkdownPreview.js @@ -1,17 +1,25 @@ import React, { PropTypes } from 'react'; -import CommonMark from 'commonmark'; -import ReactRenderer from 'commonmark-react-renderer'; - -const parser = new CommonMark.Parser(); -const renderer = new ReactRenderer(); +import MarkupIt from 'markup-it'; +import { getSyntaxes } from './richText'; export default class MarkdownPreview extends React.Component { + + constructor(props) { + super(props); + + const { markdown, html } = getSyntaxes(); + this.markdown = new MarkupIt(markdown); + this.html = new MarkupIt(html); + } render() { const { value } = this.props; if (value == null) { return null; } + const content = this.markdown.toContent(value); + const contentHtml = { __html: this.html.toText(content) }; - const ast = parser.parse(value); - return React.createElement.apply(React, ['div', {}].concat(renderer.render(ast))); + return ( +
    + ); } } diff --git a/src/components/Widgets/richText.js b/src/components/Widgets/richText.js new file mode 100644 index 00000000..08a8331c --- /dev/null +++ b/src/components/Widgets/richText.js @@ -0,0 +1,132 @@ +/* eslint react/prop-types: 0, react/no-multi-comp: 0 */ +import React from 'react'; +import { List, Map } from 'immutable'; +import MarkupIt from 'markup-it'; +import markdownSyntax from 'markup-it/syntaxes/markdown'; +import htmlSyntax from 'markup-it/syntaxes/html'; +import reInline from 'markup-it/syntaxes/markdown/re/inline'; +import { Icon } from '../UI'; + +/* + * All Rich text widgets (Markdown, for example) should use Slate for text editing and + * MarkupIt to convert between structured formats (Slate JSON, Markdown, HTML, etc.). + * This module Processes and provides Slate nodes and MarkupIt syntaxes augmented with plugins + */ + +let processedPlugins = List([]); + + +const nodes = {}; +let augmentedMarkdownSyntax = markdownSyntax; +let augmentedHTMLSyntax = htmlSyntax; + +function processEditorPlugins(plugins) { + // Since the plugin list is immutable, a simple comparisson is enough + // to determine whether we need to process again. + if (plugins === processedPlugins) return; + + plugins.forEach(plugin => { + const basicRule = MarkupIt.Rule(plugin.id).regExp(plugin.pattern, (state, match) => ( + { data: plugin.fromBlock(match) } + )); + + const markdownRule = basicRule.toText((state, token) => ( + plugin.toBlock(token.getData().toObject()) + '\n\n' + )); + + const htmlRule = basicRule.toText((state, token) => ( + plugin.toPreview(token.getData().toObject()) + )); + + const nodeRenderer = (props) => { + const { node, state } = props; + const isFocused = state.selection.hasEdgeIn(node); + const className = isFocused ? 'plugin active' : 'plugin'; + return ( +
    +
    +
    + { plugin.fields.map(field => `${field.label}: “${node.data.get(field.name)}”`) } +
    + + +
    + ); + }; + + augmentedMarkdownSyntax = augmentedMarkdownSyntax.addInlineRules(markdownRule); + augmentedHTMLSyntax = augmentedHTMLSyntax.addInlineRules(htmlRule); + nodes[plugin.id] = nodeRenderer; + }); + + processedPlugins = plugins; +} + +function processMediaProxyPlugins(getMedia) { + const mediaProxyRule = MarkupIt.Rule('mediaproxy').regExp(reInline.link, (state, match) => { + if (match[0].charAt(0) !== '!') { + // Return if this is not an image + return; + } + + var imgData = Map({ + alt: match[1], + src: match[2], + title: match[3] + }).filter(Boolean); + + return { + data: imgData + }; + }); + const mediaProxyMarkdownRule = mediaProxyRule.toText((state, token) => { + var data = token.getData(); + var alt = data.get('alt', ''); + var src = getMedia(data.get('src', '')); + var title = data.get('title', ''); + + if (title) { + return '![' + alt + '](' + src + ' "' + title + '")'; + } else { + return '![' + alt + '](' + src + ')'; + } + }); + const mediaProxyHTMLRule = mediaProxyRule.toText((state, token) => { + var data = token.getData(); + var alt = data.get('alt', ''); + var src = data.get('src', ''); + return `${alt}`; + }); + + nodes['mediaproxy'] = (props) => { + /* eslint react/prop-types: 0 */ + const { node, state } = props; + const isFocused = state.selection.hasEdgeIn(node); + const className = isFocused ? 'active' : null; + const src = node.data.get('src'); + return ( + + ); + }; + augmentedMarkdownSyntax = augmentedMarkdownSyntax.addInlineRules(mediaProxyMarkdownRule); + augmentedHTMLSyntax = augmentedHTMLSyntax.addInlineRules(mediaProxyHTMLRule); +} + +function getPlugins() { + return processedPlugins.map(plugin => ( + { id: plugin.id, icon: plugin.icon, fields: plugin.fields } + )).toArray(); +} + +function getNodes() { + return nodes; +} + +function getSyntaxes(getMedia) { + if (getMedia) { + processMediaProxyPlugins(getMedia); + } + return { markdown: augmentedMarkdownSyntax, html:augmentedHTMLSyntax }; +} + +export { processEditorPlugins, getNodes, getSyntaxes, getPlugins }; diff --git a/src/components/stories/Icon.js b/src/components/stories/Icon.js index eb36f842..50668a74 100644 --- a/src/components/stories/Icon.js +++ b/src/components/stories/Icon.js @@ -11,6 +11,36 @@ const style = { storiesOf('Icon', module) .add('Default View', () => (
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -45,7 +75,7 @@ storiesOf('Icon', module) - + @@ -63,7 +93,7 @@ storiesOf('Icon', module) - + diff --git a/src/index.js b/src/index.js index 508b38f0..13dc95ca 100644 --- a/src/index.js +++ b/src/index.js @@ -5,6 +5,7 @@ import { Router } from 'react-router'; import configureStore from './store/configureStore'; import routes from './routing/routes'; import history, { syncHistory } from './routing/history'; +import { initPluginAPI } from './plugins'; import 'file?name=index.html!../example/index.html'; import './index.css'; @@ -13,14 +14,18 @@ const store = configureStore(); // Create an enhanced history that syncs navigation events with the store syncHistory(store); +const Plugin = initPluginAPI(); + const el = document.createElement('div'); el.id = 'root'; document.body.appendChild(el); render(( - - {routes} - + + + {routes} + + ), el); diff --git a/src/plugins/index.js b/src/plugins/index.js new file mode 100644 index 00000000..485a0c4c --- /dev/null +++ b/src/plugins/index.js @@ -0,0 +1,58 @@ +import { Component, PropTypes, Children } from 'react'; +import { List, Record } from 'immutable'; +import _ from 'lodash'; + +const plugins = { editor: List() }; + +const catchesNothing = /.^/; +const EditorComponent = Record({ + id: null, + label: 'unnamed component', + icon: 'exclamation-triangle', + fields: [], + pattern: catchesNothing, + fromBlock: function(match) { return {}; }, + toBlock: function(attributes) { return 'Plugin'; }, + toPreview: function(attributes) { return 'Plugin'; } +}); + +function CMS() { + this.registerEditorComponent = (config) => { + const configObj = new EditorComponent({ + id: config.id || config.label.replace(/[^A-Z0-9]+/ig, '_'), + label: config.label, + icon: config.icon, + fields: config.fields, + pattern: config.pattern, + fromBlock: _.isFunction(config.fromBlock) ? config.fromBlock.bind(null) : null, + toBlock: _.isFunction(config.toBlock) ? config.toBlock.bind(null) : null, + toPreview: _.isFunction(config.toPreview) ? config.toPreview.bind(null) : config.toBlock.bind(null) + }); + + plugins.editor = plugins.editor.push(configObj); + }; +} + + +class Plugin extends Component { + getChildContext() { + return { plugins: plugins }; + } + + render() { + return Children.only(this.props.children); + } +} + +Plugin.propTypes = { + children: PropTypes.element.isRequired +}; +Plugin.childContextTypes = { + plugins: PropTypes.object +}; + + +export const initPluginAPI = () => { + window.CMS = new CMS(); + return Plugin; +}; diff --git a/src/reducers/editor.js b/src/reducers/editor.js new file mode 100644 index 00000000..4917befb --- /dev/null +++ b/src/reducers/editor.js @@ -0,0 +1,13 @@ +import { Map } from 'immutable'; +import { SWITCH_VISUAL_MODE } from '../actions/editor'; + +const editor = (state = Map({ useVisualMode: true }), action) => { + switch (action.type) { + case SWITCH_VISUAL_MODE: + return state.setIn(['useVisualMode'], action.payload); + default: + return state; + } +}; + +export default editor; diff --git a/src/reducers/index.js b/src/reducers/index.js index ecdac295..f7199111 100644 --- a/src/reducers/index.js +++ b/src/reducers/index.js @@ -1,5 +1,6 @@ import auth from './auth'; import config from './config'; +import editor from './editor'; import entries, * as fromEntries from './entries'; import entryDraft from './entryDraft'; import collections from './collections'; @@ -9,6 +10,7 @@ const reducers = { auth, config, collections, + editor, entries, entryDraft, medias