diff --git a/.eslintrc b/.eslintrc index 7b1047b..04c4e6b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,17 +1,17 @@ -{ "parser": "babel-eslint" -, "ecmaFeatures": { "modules": true } -, "env": - { "browser": true - , "node": true - , "es6": true - } -, "rules": - { "no-multiple-empty-lines": [2, {"max": 2, "maxEOF": 1}] - , "no-console": 2 - , "comma-style": [2, "first"] - , "camelcase": 0 - , "indent": 0 - } -, "plugins": ["react"] -, "extends": ["standard", "plugin:react/recommended"] -} \ No newline at end of file +{ + "parser": "babel-eslint", + "ecmaFeatures": { "modules": true }, + "env": { + "browser": true, + "node": true, + "es6": true + }, + "rules": { + "no-multiple-empty-lines": [2, {"max": 2, "maxEOF": 1}], + "comma-dangle": [2, "always-multiline"], + "no-console": 2, + "camelcase": 0 + }, + "plugins": ["react"], + "extends": ["standard", "plugin:react/recommended"] +} diff --git a/.gitignore b/.gitignore index 95ed199..6667f68 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ logs node_modules dist .DS_Store + +*.orig diff --git a/.travis.yml b/.travis.yml index 69ca89f..f86b46c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,6 @@ language: node_js node_js: -- 6.9 +- 7.4 before_install: - export CHROME_BIN=chromium-browser - export DISPLAY=:99.0 diff --git a/CHANGES.md b/CHANGES.md index 529ecf0..abb483c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,83 @@ ### dev +# 2.0.0 - 2017-01-25 + +* Some style tweaks +* All of these: + +#### 2.0.0-11 prerelease - 2017-01-24 + +* Merge `featureFlags` prop from 1.4.x +* ed.version (#306) +* Link input `type="url"` for mobile input keyboard +* Show attributions in inline view +* Show block type & link in inline view +* `hr` padding for easier selection + +#### 2.0.0-10 prerelease - 2017-01-21 + +* Node v7.4.0 + +#### 2.0.0-9 prerelease - 2017-01-21 + +* Clarify link placeholder +* iframe widget name + drag handle +* Click to select simplification +* Delete block wording & close modal +* No text select draggable widgets +* Enter in text area to close modal and menu +* Modal style +* Fix bug where clicking "Edit" would select node and jump away from pointer +* Default `coverPrefs` +* Fix blur: prevents delete by backspace from "under" modal + +#### 2.0.0-8 prerelease - 2017-01-19 + +* Merge v1 patch +* Fix bug that sometimes focused top of doc on clicking block "edit" + +#### 2.0.0-7 prerelease - 2017-01-19 + +* Blur editable on modal open. +* Don't allow typing over node selection. Fixes bug that media view can be typed over and deleted after clicking "edit." + +#### 2.0.0-6 prerelease - 2017-01-19 + +* Modal media block meta editing! :tada: +* Fixed menu hack _only_ on iOS + +#### 2.0.0-5 prerelease - 2017-01-10 + +* Fix gh-pages widget serving (.nojekyll) +* Fix link menu form position +* Fix file drops: on both media blocks & text + +#### 2.0.0-4 prerelease - 2017-01-10 + +* ProseMirror 0.17.0 +* URL modal attached to menu +* URL pre-filled if selected text is link-like (#288) + +#### 2.0.0-3 prerelease - 2017-01-05 + +* bump imgflo-url to ignore `blob:` URLs + +#### 2.0.0-2 prerelease - 2017-01-05 + +* fix `menuBar: false` + +#### 2.0.0-1 prerelease - 2017-01-05 + +* BREAKING -- `mountApp` is async because of a React change, and does not return `ed` + * `props.onMount` callback is called with `ed` instance + +#### 2.0.0-0 prerelease - 2017-01-02 + +* ProseMirror 0.16.0 -- big refactor to get up to date + * Redo all plugins & everything we have built on PM +* Widgets are inline: major simplification +* h1-h3 empty block placeholders + ### 1.4.2 - 2017-01-23 * `featureFlags: {edCta, edEmbed}` to match API diff --git a/README.md b/README.md index 09018af..345e725 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,7 @@ available to use like this: ``` js var container = document.querySelector('#ed') - var ed = window.TheGridEd.mountApp(container, { + window.TheGridEd.mountApp(container, { // REQUIRED -- Content array from post initialContent: [], // OPTIONAL (default true) enable or disable the default menu @@ -94,8 +94,9 @@ available to use like this: /* Once upload is complete, app hits ed.setCoverSrc */ }, // OPTIONAL - onMount: function () { + onMount: function (mounted) { /* Called once PM and widgets are mounted */ + window.ed = mounted }, // OPTIONAL onCommandsChanged: function (commands) { diff --git a/demo/demo.js b/demo/demo.js index 738858b..c97a57a 100644 --- a/demo/demo.js +++ b/demo/demo.js @@ -22,32 +22,31 @@ function setup (options) { apiJSON.value = JSON.stringify(options.initialContent, null, 2) } const props = - { initialContent: (options.initialContent || []) - , onChange: () => { console.log('change') } - , onMount: () => { console.log('mount') } - , onShareFile: onShareFileDemo - , onShareUrl: onShareUrlDemo - , onRequestCoverUpload: onRequestCoverUploadDemo - , onPlaceholderCancel: onPlaceholderCancelDemo - , onCommandsChanged: (commands) => {} - , onDropFiles: onDropFilesDemo - , onDropFileOnBlock: onDropFileOnBlockDemo - , imgfloConfig: null - , widgetPath: './node_modules/' - , coverPrefs: { filter: false } - , menuBar: true - , featureFlags: - { edCta: false - , edEmbed: false - } - , ref: - function (mounted) { - ed = mounted - console.log(ed) - window.ed = ed - } + { initialContent: (options.initialContent || []), + onChange: () => { console.log('onChange') }, + onMount: function (mounted) { + ed = mounted + console.log(ed) + window.ed = ed + }, + onShareFile: onShareFileDemo, + onShareUrl: onShareUrlDemo, + onRequestCoverUpload: onRequestCoverUploadDemo, + onPlaceholderCancel: onPlaceholderCancelDemo, + onCommandsChanged: function (commands) { + // console.log(commands) + }, + onDropFiles: onDropFilesDemo, + onDropFileOnBlock: onDropFileOnBlockDemo, + imgfloConfig: null, + widgetPath: './node_modules/', + coverPrefs: { filter: false }, + menuBar: true, + featureFlags: { + edCta: false, + edEmbed: false, + }, } - mountApp(container, props) // Only for fixture demo @@ -117,10 +116,10 @@ function filesUploadSim (index, files) { const updatedBlocks = ids.map(function (id, index) { ed.updateProgress(id, {progress: null}) return ( - { id - , type: 'image' - , metadata: {title: names[index]} - } + { id, + type: 'image', + metadata: {title: names[index]}, + } ) }) ed.setContent(updatedBlocks) @@ -146,29 +145,29 @@ function onShareUrlDemo (share) { function () { console.log('Share: mount block') ed.setContent([ - { id: block - , type: 'article' - , metadata: - { title: 'Shared article title' - , description: `Simulated share from ${url}` - } - } + { id: block, + type: 'article', + metadata: + { title: 'Shared article title', + description: `Simulated share from ${url}`, + }, + }, ]) window.setTimeout(function () { console.log('Share: mount block + cover') ed.setContent([ - { id: block - , type: 'article' - , metadata: - { title: 'Shared article title + cover' - , description: `Simulated share from ${url}` - } - , cover: - { src: 'http://meemoo.org/images/meemoo-illo-by-jyri-pieniniemi-400.png' - , width: 400 - , height: 474 - } - } + { id: block, + type: 'article', + metadata: + { title: 'Shared article title + cover', + description: `Simulated share from ${url}`, + }, + cover: + { src: 'http://meemoo.org/images/meemoo-illo-by-jyri-pieniniemi-400.png', + width: 400, + height: 474, + }, + }, ]) }, 1000) } diff --git a/demo/fixture.js b/demo/fixture.js index cee08c6..bafee16 100644 --- a/demo/fixture.js +++ b/demo/fixture.js @@ -1,5 +1,3 @@ -/* eslint quotes: [0], comma-style: [0] */ - import getHappyLittlePhrase from 'bob-ross-lipsum' let tweet = { @@ -24,26 +22,26 @@ let tweet = { [240, 243, 244], [17, 17, 20], [103, 117, 134], - [186, 188, 191] - ] - } + [186, 188, 191], + ], + }, }], 'related': [], 'publisher': { 'url': 'http://twitter.com', 'name': 'Twitter', 'favicon': 'https://abs.twimg.com/favicons/favicon.ico', - 'domain': 'twitter.com' + 'domain': 'twitter.com', }, 'keywords': ['jasonfried', 'thegridio', 'https', 'thegrid', 'dracula_x', 'conceptually', 'location', 'websites', 'sure', 'waynepelletier'], - // 'description': 'https://thegrid.io - "AI" website design. Conceptually feels very next level, an obvious, natural progression just waiting to happen.', + 'description': 'https://thegrid.io - "AI" website design. Conceptually feels very next level, an obvious, natural progression just waiting to happen.', 'inLanguage': 'English', '@type': 'Comment', 'app_links': [], '@context': 'http://schema.org', 'isBasedOnUrl': 'https://twitter.com/jasonfried/status/522492212144525312', - 'source': '926db660-ed6c-43f6-b838-56ac6a527034' - } + 'source': '926db660-ed6c-43f6-b838-56ac6a527034', + }, } let imageRaphael = { @@ -67,33 +65,33 @@ let imageRaphael = { [ 229, 212, - 218 + 218, ], [ 185, 162, - 160 + 160, ], [ 81, 70, - 64 + 64, ], [ 139, 118, - 116 - ] - ] - } - } + 116, + ], + ], + }, + }, ], 'related': [], 'publisher': { 'url': 'http://twitter.com', 'name': 'Twitter', 'favicon': 'https://abs.twimg.com/favicons/favicon.ico', - 'domain': 'twitter.com' + 'domain': 'twitter.com', }, 'keywords': [ 'raphael', @@ -105,7 +103,7 @@ let imageRaphael = { 'approx', 'deepforgery', '360k', - 'erbwqjyelg' + 'erbwqjyelg', ], 'description': 'StyleNet #NeuralArt, inspiration from @jeratt and style by Raphael.', 'inLanguage': 'en', @@ -116,12 +114,12 @@ let imageRaphael = { 'datePublished': null, 'starred': true, 'caption': 'StyleNet #NeuralArt, inspiration from @jeratt and style by Raphael.', - 'source': 'd6fddcde-0831-4058-83a8-110b03aab390' + 'source': 'd6fddcde-0831-4058-83a8-110b03aab390', }, 'caption': 'StyleNet #NeuralArt, inspiration from @jeratt and style by Raphael.', 'cover': { - 'src': 'https://pbs.twimg.com/media/CODJ8KXWoAAVGTP.jpg:large' - } + 'src': 'https://pbs.twimg.com/media/CODJ8KXWoAAVGTP.jpg:large', + }, } let videoTurtle = { @@ -137,15 +135,15 @@ let videoTurtle = { { 'name': 'Jon Nordby', 'url': 'http://www.youtube.com/channel/UCB9kP5NQGu0JLWa9UlkxklQ', - 'avatar': {} - } + 'avatar': {}, + }, ], 'related': [], 'publisher': { 'url': 'http://www.youtube.com/', 'name': 'YouTube', 'favicon': 'https://s.ytimg.com/yts/img/favicon-vfldLzJxy.ico', - 'domain': 'www.youtube.com' + 'domain': 'www.youtube.com', }, 'keywords': [ 'duration', @@ -157,7 +155,7 @@ let videoTurtle = { 'ravensbourne', 'mozfest', 'milling', - 'jens' + 'jens', ], 'description': getHappyLittlePhrase(), 'inLanguage': 'English', @@ -167,27 +165,27 @@ let videoTurtle = { 'url': 'vnd.youtube://www.youtube.com/watch?v=IMShMTn8yEU&feature=applinks', 'type': 'ios', 'app_store_id': '544007664', - 'app_name': 'YouTube' + 'app_name': 'YouTube', }, { 'url': 'http://www.youtube.com/watch?v=IMShMTn8yEU&feature=applinks', 'type': 'android', 'app_name': 'YouTube', - 'package': 'com.google.android.youtube' + 'package': 'com.google.android.youtube', }, { 'url': 'http://www.youtube.com/watch?v=IMShMTn8yEU&feature=applinks', - 'type': 'web' - } + 'type': 'web', + }, ], '@context': 'http://schema.org', 'isBasedOnUrl': 'https://www.youtube.com/watch?v=IMShMTn8yEU', 'title': getHappyLittlePhrase(), 'starred': false, - 'source': '77d2788d-d7c5-4fbe-9087-4328d9f12ddb' + 'source': '77d2788d-d7c5-4fbe-9087-4328d9f12ddb', }, 'video': { - 'src': 'https://cdn.embedly.com/widgets/media.html?src=http%3A%2F%2Fwww.youtube.com%2Fembed%2FIMShMTn8yEU%3Ffeature%3Doembed&url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DIMShMTn8yEU&image=http%3A%2F%2Fi.ytimg.com%2Fvi%2FIMShMTn8yEU%2Fhqdefault.jpg&key=b7d04c9b404c499eba89ee7072e1c4f7&type=text%2Fhtml&schema=youtube' + 'src': 'https://cdn.embedly.com/widgets/media.html?src=http%3A%2F%2Fwww.youtube.com%2Fembed%2FIMShMTn8yEU%3Ffeature%3Doembed&url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DIMShMTn8yEU&image=http%3A%2F%2Fi.ytimg.com%2Fvi%2FIMShMTn8yEU%2Fhqdefault.jpg&key=b7d04c9b404c499eba89ee7072e1c4f7&type=text%2Fhtml&schema=youtube', }, 'cover': { 'orientation': 'landscape', @@ -196,9 +194,9 @@ let videoTurtle = { 'width': 480, 'height': 360, 'faces': [], - 'colors': [[47, 47, 48], [213, 215, 206], [123, 119, 115], [149, 162, 164], [136, 144, 159]] + 'colors': [[47, 47, 48], [213, 215, 206], [123, 119, 115], [149, 162, 164], [136, 144, 159]], }, - 'title': 'Mozfest 2014 - Turtle Power: Mirobot and Flowhub penplotting' + 'title': 'Mozfest 2014 - Turtle Power: Mirobot and Flowhub penplotting', } let article = { @@ -218,14 +216,14 @@ let article = { 'url': 'http://www.wired.com/2015/01/orchestra-tiny-humming-robots-conduct-gestures/', 'thumbnail_height': 750, 'thumbnail_url': 'http://www.wired.com/wp-content/uploads/2015/01/IMG_5196.jpg', - 'thumbnail_width': 1000 - } + 'thumbnail_width': 1000, + }, ], 'publisher': { 'url': 'http://meemoo.org', 'name': 'Meemoo', 'favicon': 'http://meemoo.org/favicon.ico', - 'domain': 'meemoo.org' + 'domain': 'meemoo.org', }, 'keywords': [ 'mirobot', @@ -237,7 +235,7 @@ let article = { 'noflo', 'art', 'workshops', - 'graph' + 'graph', ], 'description': getHappyLittlePhrase(5), 'inLanguage': 'English', @@ -248,7 +246,7 @@ let article = { 'title': getHappyLittlePhrase(), 'starred': false, 'caption': "14 Jan 2015 by Vilson Vieira After working for about three years with Forrest we finally meet him on a meet up of The Grid team. During the first days we were preparing a workshop for MozFest's #ArtOfWeb track. The idea was to present a quick introduction to Flowhub/ NoFlo and how to use it to draw with Mirobot.", - 'source': 'ad25432f-11f9-4326-8b0c-edbffa7afbdc' + 'source': 'ad25432f-11f9-4326-8b0c-edbffa7afbdc', }, 'cover': { 'orientation': 'landscape', @@ -257,10 +255,10 @@ let article = { 'width': 1206, 'height': 511, 'faces': [], - 'colors': [[7, 8, 8], [177, 178, 177], [251, 4, 28], [14, 208, 125], [21, 106, 194]] + 'colors': [[7, 8, 8], [177, 178, 177], [251, 4, 28], [14, 208, 125], [21, 106, 194]], }, 'title': 'Turtle power to the people', - 'caption': "14 Jan 2015 by Vilson Vieira After working for about three years with Forrest we finally meet him on a meet up of The Grid team. During the first days we were preparing a workshop for MozFest's #ArtOfWeb track. The idea was to present a quick introduction to Flowhub/ NoFlo and how to use it to draw with Mirobot." + 'caption': "14 Jan 2015 by Vilson Vieira After working for about three years with Forrest we finally meet him on a meet up of The Grid team. During the first days we were preparing a workshop for MozFest's #ArtOfWeb track. The idea was to present a quick introduction to Flowhub/ NoFlo and how to use it to draw with Mirobot.", } let imageCole = { @@ -277,18 +275,18 @@ let imageCole = { '@type': 'Article', 'publisher': { 'domain': 'the-grid-user-content.s3-us-west-2.amazonaws.com', - 'name': 'The Grid' + 'name': 'The Grid', }, 'starred': true, 'source': '51642a12-50cf-4072-855c-d7d8294ba125', author: [{ name: 'Cole Rise', - url: 'https://cole.grid/' + url: 'https://cole.grid/', }], title: getHappyLittlePhrase(), coverPrefs: { - filter: false - } + filter: false, + }, }, 'cover': { 'orientation': 'landscape', @@ -302,9 +300,9 @@ let imageCole = { 'polygon': [[1219, 259], [1213, 254], [1190, 251], [1178, 264], [1151, 266], [1130, 275], [1113, 293], [1045, 293], [960, 282], [917, 282], [838, 292], [824, 302], [767, 311], [766, 322], [709, 318], [621, 341], [550, 384], [492, 410], [471, 424], [466, 434], [453, 434], [382, 469], [340, 469], [285, 446], [268, 449], [267, 456], [276, 457], [278, 480], [307, 484], [311, 571], [272, 576], [267, 607], [313, 604], [339, 621], [366, 624], [367, 658], [464, 641], [467, 635], [532, 619], [532, 606], [610, 598], [641, 591], [728, 587], [834, 571], [914, 569], [987, 578], [998, 585], [1025, 585], [1027, 591], [1050, 593], [1051, 599], [1083, 599], [1084, 611], [1119, 611], [1127, 624], [1144, 615], [1184, 615], [1192, 636], [1204, 630], [1206, 619], [1215, 614], [1215, 540], [1232, 534], [1232, 527], [1180, 527], [1180, 487], [1199, 469], [1190, 447], [1190, 406], [1197, 405], [1200, 367], [1216, 366], [1208, 341], [1208, 298], [1217, 295], [1223, 282]], 'center': [743, 433], 'radius': 522.01, - 'bounding_rect': [[267, 251], [1233, 659]] - } - } + 'bounding_rect': [[267, 251], [1233, 659]], + }, + }, } let code = { @@ -314,12 +312,12 @@ let code = { 'metadata': { 'title': 'code code', 'description': 'very sample code', - 'programmingLanguage': 'text/coffeescript' + 'programmingLanguage': 'text/coffeescript', }, 'src': null, 'text': '# Assignment:\nnumber = 42\nopposite = true\n\n# Conditions:\nnumber = -42 if opposite\n\n# Functions:\nsquare = (x) -> x * x\n\n# Arrays:\nlist = [1, 2, 3, 4, 5]\n\n# Objects:\nmath =\n root: Math.sqrt\n square: square\n cube: (x) -> x * square x\n\n# Splats:\nrace = (winner, runners...) ->\n print winner, runners\n\n# Existence:\nalert "I knew it!" if elvis?\n\n# Array comprehensions:\ncubes = (math.cube num for num in list)', 'length': 32, - 'measurementVersion': 5 + 'measurementVersion': 5, } let imageBeingD4 = { @@ -342,17 +340,17 @@ let imageBeingD4 = { 'title': getHappyLittlePhrase(), 'author': [ { - 'name': 'Gordon' - } + 'name': 'Gordon', + }, ], 'authors': [], 'publisher': { 'name': 'i.meemoo.me', 'domain': 'i.meemoo.me', 'url': null, - 'favicon': null + 'favicon': null, }, - 'user': '79922a3a-1dcb-4a43-9b62-e4f3af6ad4ca' + 'user': '79922a3a-1dcb-4a43-9b62-e4f3af6ad4ca', }, 'title': 'being d4', 'cover': { @@ -361,13 +359,13 @@ let imageBeingD4 = { [144, 126, 104], [50, 39, 37], [225, 208, 192], - [108, 90, 68] + [108, 90, 68], ], 'saliency': { 'polygon': [[1350, 300], [600, 200], [100, 250], [200, 600], [1250, 700]], 'center': [720, 480], 'radius': 700, - 'bounding_rect': [[100, 200], [1350, 700]] + 'bounding_rect': [[100, 200], [1350, 700]], }, 'src': 'http://i.meemoo.me/v1/in/Adx3TWdBQ3O3uJvyH2DP_being-d4.jpg', 'orientation': 'landscape', @@ -382,7 +380,7 @@ let imageBeingD4 = { 'width': 60.181420320681795, 'height': 60.181420320681795, 'neighbors': 11, - 'confidence': 8.715965329999982 + 'confidence': 8.715965329999982, }, { 'x': 367.5215516831421, @@ -390,7 +388,7 @@ let imageBeingD4 = { 'width': 54.27794792754907, 'height': 54.27794792754907, 'neighbors': 14, - 'confidence': 8.305308969999993 + 'confidence': 8.305308969999993, }, { 'x': 492.2391919725017, @@ -398,7 +396,7 @@ let imageBeingD4 = { 'width': 57.606293506030504, 'height': 57.606293506030504, 'neighbors': 12, - 'confidence': 7.161568390000004 + 'confidence': 7.161568390000004, }, { 'x': 486.83705737694504, @@ -406,7 +404,7 @@ let imageBeingD4 = { 'width': 50.952088047919155, 'height': 50.952088047919155, 'neighbors': 12, - 'confidence': 6.479253949999998 + 'confidence': 6.479253949999998, }, { 'x': 296.1169884519212, @@ -414,7 +412,7 @@ let imageBeingD4 = { 'width': 67.83546137450323, 'height': 67.83546137450323, 'neighbors': 13, - 'confidence': 6.356675570000005 + 'confidence': 6.356675570000005, }, { 'x': 646.9246222261988, @@ -422,7 +420,7 @@ let imageBeingD4 = { 'width': 59.64585507428254, 'height': 59.64585507428254, 'neighbors': 5, - 'confidence': 6.261280809999998 + 'confidence': 6.261280809999998, }, { 'x': 364.884811041297, @@ -430,7 +428,7 @@ let imageBeingD4 = { 'width': 66.86416232804767, 'height': 66.86416232804767, 'neighbors': 5, - 'confidence': 5.530725450000008 + 'confidence': 5.530725450000008, }, { 'x': 1224.101870760976, @@ -438,7 +436,7 @@ let imageBeingD4 = { 'width': 74.9664382734353, 'height': 74.9664382734353, 'neighbors': 5, - 'confidence': 5.0318680700000025 + 'confidence': 5.0318680700000025, }, { 'x': 958.9959594409227, @@ -446,7 +444,7 @@ let imageBeingD4 = { 'width': 59.64585507428254, 'height': 59.64585507428254, 'neighbors': 5, - 'confidence': 4.866107930000008 + 'confidence': 4.866107930000008, }, { 'x': 173.1109328273247, @@ -454,7 +452,7 @@ let imageBeingD4 = { 'width': 58.86220762496542, 'height': 58.86220762496542, 'neighbors': 9, - 'confidence': 4.741367449999998 + 'confidence': 4.741367449999998, }, { 'x': 462.6463818447289, @@ -462,7 +460,7 @@ let imageBeingD4 = { 'width': 61.23303015148604, 'height': 61.23303015148604, 'neighbors': 10, - 'confidence': 3.994080369999999 + 'confidence': 3.994080369999999, }, { 'x': 992.4931228509183, @@ -470,7 +468,7 @@ let imageBeingD4 = { 'width': 65.59088857569006, 'height': 65.59088857569006, 'neighbors': 7, - 'confidence': 3.941819850000007 + 'confidence': 3.941819850000007, }, { 'x': 594.709265637875, @@ -478,7 +476,7 @@ let imageBeingD4 = { 'width': 63.90080839651627, 'height': 63.90080839651627, 'neighbors': 5, - 'confidence': 1.0505017100000016 + 'confidence': 1.0505017100000016, }, { 'x': 1049.8796264067955, @@ -486,7 +484,7 @@ let imageBeingD4 = { 'width': 60.997264465222855, 'height': 60.997264465222855, 'neighbors': 4, - 'confidence': 0.9801972499999992 + 'confidence': 0.9801972499999992, }, { 'x': 1034.9838867187502, @@ -494,7 +492,7 @@ let imageBeingD4 = { 'width': 68.15576171875001, 'height': 68.15576171875001, 'neighbors': 1, - 'confidence': 0.57032285 + 'confidence': 0.57032285, }, { 'x': 245.90400995259034, @@ -502,7 +500,7 @@ let imageBeingD4 = { 'width': 40.877432949014576, 'height': 40.877432949014576, 'neighbors': 2, - 'confidence': -0.4849196699999989 + 'confidence': -0.4849196699999989, }, { 'x': 1049.9172332545004, @@ -510,7 +508,7 @@ let imageBeingD4 = { 'width': 61.197989614635624, 'height': 61.197989614635624, 'neighbors': 2, - 'confidence': -1.4843781100000037 + 'confidence': -1.4843781100000037, }, { 'x': 1092.2399786737249, @@ -518,7 +516,7 @@ let imageBeingD4 = { 'width': 76.4162095711182, 'height': 76.4162095711182, 'neighbors': 1, - 'confidence': -1.75232689 + 'confidence': -1.75232689, }, { 'x': 741.3058376715854, @@ -526,7 +524,7 @@ let imageBeingD4 = { 'width': 54.240217510521234, 'height': 54.240217510521234, 'neighbors': 1, - 'confidence': -2.690027709999997 + 'confidence': -2.690027709999997, }, { 'x': 867.1901815303999, @@ -534,10 +532,10 @@ let imageBeingD4 = { 'width': 48.39919881847384, 'height': 48.39919881847384, 'neighbors': 1, - 'confidence': -4.276220189999993 - } - ] - } + 'confidence': -4.276220189999993, + }, + ], + }, } let sharing = { @@ -546,35 +544,35 @@ let sharing = { metadata: { starred: true, status: 'Sharing... https://thegrid.io/#8', - progress: 67 - } + progress: 67, + }, } let location = { - "metadata": { - "geo": { - "latitude": 68.55260186877743, - "longitude": 22.666168212890625, - "zoom": 5 - }, - "address": "Enontekiö, Lappi, Finland", - starred: true + 'metadata': { + 'geo': { + 'latitude': 68.55260186877743, + 'longitude': 22.666168212890625, + 'zoom': 5, + }, + 'address': 'Enontekiö, Lappi, Finland', + starred: true, }, - "id": "uuid-loca-tion", - "html": "", - "type": "location" + 'id': 'uuid-loca-tion', + 'html': '', + 'type': 'location', } let userhtml = { - "type": "interactive", - "html": "", - "text": "

Hello 😬

\n

world 🌍

\n", - "metadata": { + 'type': 'interactive', + 'html': '', + 'text': "

Hello 😬

\n

world 🌍

\n", + 'metadata': { starred: true, - "widget": "userhtml", - "isBasedOnUrl": "https://the-grid.github.io/ed-userhtml/?g=eJyzyTC080jNyclX-DB_xhobfSCXy6bArjy_KCcFKNTTa6NfABTJzE1XyM_LyU9MsVVKzs8rzs9J1StPLMrTUE8rys9VyMzLycxLVddUUiguSrZVyigpKbDS18_JL0rNLcisSM3RS87P1Tc3AqHkxJJiJTsAKJAp3Q" + 'widget': 'userhtml', + 'isBasedOnUrl': 'https://the-grid.github.io/ed-userhtml/?g=eJyzyTC080jNyclX-DB_xhobfSCXy6bArjy_KCcFKNTTa6NfABTJzE1XyM_LyU9MsVVKzs8rzs9J1StPLMrTUE8rys9VyMzLycxLVddUUiguSrZVyigpKbDS18_JL0rNLcisSM3RS87P1Tc3AqHkxJJiJTsAKJAp3Q', }, - "id": "uuid-user-html" + 'id': 'uuid-user-html', } let cta = { @@ -583,9 +581,9 @@ let cta = { url: 'https://app.meemoo.org/', label: 'Try it now!', metadata: { - starred: true + starred: true, }, - id: 'uuid-cta' + id: 'uuid-cta', } let post = { @@ -594,98 +592,98 @@ let post = { 'id': 'abc-00000000-p', 'type': 'text', 'html': `

${getHappyLittlePhrase()}
Strong. Em. Both. Plain.

`, - 'metadata': {'starred': true} + 'metadata': {'starred': true}, }, { 'type': 'text', 'html': '

An unknown block type... important to keep place in doc flow:

', - 'metadata': {'starred': true} + 'metadata': {'starred': true}, }, { type: 'unsupported-boo', id: 'uuid-unsupported', metadata: { starred: true, - isBasedOnUrl: 'https://meemoo.org/' - } + isBasedOnUrl: 'https://meemoo.org/', + }, }, { 'type': 'text', 'html': '

An `hr` block divider:

', - 'metadata': {'starred': true} + 'metadata': {'starred': true}, }, { 'type': 'hr', 'html': '
', - 'metadata': {'starred': true} + 'metadata': {'starred': true}, }, { 'type': 'text', 'html': '

It\'s a link cta:

', - 'metadata': {'starred': true} + 'metadata': {'starred': true}, }, cta, { 'type': 'text', 'html': '

Quote from tweet:

', - 'metadata': {'starred': true} + 'metadata': {'starred': true}, }, tweet, { 'type': 'text', 'html': '

Look it\'s userhtml:

', - 'metadata': {'starred': true} + 'metadata': {'starred': true}, }, userhtml, { 'type': 'text', 'html': '

Here\'s a location in Lapland:

', - 'metadata': {'starred': true} + 'metadata': {'starred': true}, }, location, { 'type': 'text', 'html': '

Here\'s a normal image:

', - 'metadata': {'starred': true} + 'metadata': {'starred': true}, }, imageCole, { 'type': 'text', 'html': '

Here\'s an article block with no cover (you can upload an image with ... menu):

', - 'metadata': {'starred': true} + 'metadata': {'starred': true}, }, { 'id': '0000-article', 'type': 'article', 'html': '

article title

', - 'metadata': {'starred': true, 'title': 'article title', 'description': ''} + 'metadata': {'starred': true, 'title': 'article title', 'description': ''}, }, { 'type': 'text', 'html': '

Here\'s an article block with upload progress:

', - 'metadata': {'starred': true} + 'metadata': {'starred': true}, }, { 'id': '0000-article-progress', 'type': 'article', 'html': '

up

', - 'metadata': {starred: true, title: 'up', description: '', progress: 66} + 'metadata': {starred: true, title: 'up', description: '', progress: 66}, }, { 'type': 'text', 'html': '

Here\'s an article block with failed upload:

', - 'metadata': {'starred': true} + 'metadata': {'starred': true}, }, { 'id': '0000-article-failed', 'type': 'article', 'html': '

boo

', - 'metadata': {starred: true, title: 'boo', description: '', progress: 33, failed: true} + 'metadata': {starred: true, title: 'boo', description: '', progress: 33, failed: true}, }, { 'type': 'text', 'html': '

Here\'s an image block with unsalvageable cover:

', - 'metadata': {'starred': true} + 'metadata': {'starred': true}, }, { 'id': '0000-image-unsalvageable', @@ -693,98 +691,98 @@ let post = { 'html': '', 'metadata': {starred: true, caption: 'Sometimes images go away :-('}, 'cover': - { unsalvageable: true - , src: 'https://pbs.twimg.com/profile_images/674936695830282240/np255F6b_400x400.jpg' - } + { unsalvageable: true, + src: 'https://pbs.twimg.com/profile_images/674936695830282240/np255F6b_400x400.jpg', + }, }, { 'type': 'text', 'html': '

Placeholder with preview:

', - 'metadata': {'starred': true} + 'metadata': {'starred': true}, }, { 'id': '0000-placeholder-preview', 'type': 'placeholder', 'html': '', 'metadata': {starred: true, status: 'Uploading...', progress: 85}, - 'cover': {src: 'https://pbs.twimg.com/profile_images/674936695830282240/np255F6b_400x400.jpg'} + 'cover': {src: 'https://pbs.twimg.com/profile_images/674936695830282240/np255F6b_400x400.jpg'}, }, { 'type': 'text', 'html': '

Placeholder for URL share with progress:

', - 'metadata': {'starred': true} + 'metadata': {'starred': true}, }, sharing, { 'type': 'text', 'html': '

Placeholder failed:

', - 'metadata': {'starred': true} - }, - { id: '0000-failed' - , type: 'placeholder' - , metadata: - { starred: true - , status: 'Hmmm...' - , failed: true - } + 'metadata': {'starred': true}, + }, + { id: '0000-failed', + type: 'placeholder', + metadata: + { starred: true, + status: 'Hmmm...', + failed: true, + }, }, { 'id': 'abc-00000000-h1', 'type': 'h1', 'html': `

${getHappyLittlePhrase()}

`, - 'metadata': {} + 'metadata': {}, }, { 'id': 'abc-00000000-blockquote', 'type': 'blockquote', 'html': `
${getHappyLittlePhrase()}
`, - 'metadata': {} + 'metadata': {}, }, code, { 'id': 'abc-00000000-p2', 'type': 'p', - 'html': `

${getHappyLittlePhrase()}

` + 'html': `

${getHappyLittlePhrase()}

`, }, { 'id': 'uuid-broken-00', - 'type': undefined + 'type': undefined, }, imageRaphael, videoTurtle, { 'id': 'abc-00000000-h2', 'type': 'h2', - 'html': `

${getHappyLittlePhrase()}

` + 'html': `

${getHappyLittlePhrase()}

`, }, article, { 'id': 'abc-00000000-h3', 'type': 'h3', - 'html': `

${getHappyLittlePhrase()}

` + 'html': `

${getHappyLittlePhrase()}

`, }, imageBeingD4, { 'id': 'abc-00000000-02', 'type': 'text', - 'html': `

${getHappyLittlePhrase(2)}

` + 'html': `

${getHappyLittlePhrase(2)}

`, }, { 'id': 'abc-00000000-03', 'type': 'quote', - 'html': `

${getHappyLittlePhrase(3)}

` + 'html': `

${getHappyLittlePhrase(3)}

`, }, { 'id': 'abc-00000000-ol', 'type': 'list', - 'html': `
  1. ${getHappyLittlePhrase()}
  2. ${getHappyLittlePhrase()}
` + 'html': `
  1. ${getHappyLittlePhrase()}
  2. ${getHappyLittlePhrase()}
`, }, { 'id': 'abc-00000000-ul', 'type': 'list', - 'html': `` - } - ] + 'html': ``, + }, + ], } diff --git a/demo/gremlin-prosemirror.js b/demo/gremlin-prosemirror.js index 5c7e5fc..e83d8c3 100644 --- a/demo/gremlin-prosemirror.js +++ b/demo/gremlin-prosemirror.js @@ -10,8 +10,8 @@ function randomFromArray (arr) { const config = - { logger: null - , randomizer: null + { logger: null, + randomizer: null, } function pmSelecter (pm) { @@ -65,13 +65,13 @@ function pmFormatter (pm) { } const subgremlins = - [ pmSelecter - , pmSelecterCollapsed - , pmSelecterNode - , pmTyper - , pmFocuser - , pmSplitter - , pmFormatter + [ pmSelecter, + pmSelecterCollapsed, + pmSelecterNode, + pmTyper, + pmFocuser, + pmSplitter, + pmFormatter, ] function pmGremlin () { diff --git a/gh-pages.sh b/gh-pages.sh index 33ed16c..d423151 100644 --- a/gh-pages.sh +++ b/gh-pages.sh @@ -17,6 +17,9 @@ mkdir dist/webpack mv dist/demo.js dist/webpack/demo.js mv dist/demo.map dist/webpack/demo.map +# no need for jekyll in this demo (jekyll 3.3+ blocks node_modules) +touch dist/.nojekyll + # go to the build directory and create a *new* Git repo cd dist git init diff --git a/package.json b/package.json index 73dd7b1..9a53fcc 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "@the-grid/ed", "author": "Forrest Oliphant, The Grid", "license": "MIT", - "version": "1.4.2", + "version": "2.0.0-11", "description": "the grid api with prosemirror", "main": "dist/ed.js", "scripts": { @@ -31,10 +31,23 @@ "@the-grid/ced": "0.1.3", "@the-grid/ed-location": "2.0.1", "@the-grid/ed-userhtml": "0.3.0", + "crel": "3.0.0", "he": "1.1.0", "imgflo-url": "1.2.0", "lodash": "4.17.4", - "prosemirror": "0.10.1", + "prosemirror-commands": "0.17.1", + "prosemirror-dropcursor": "0.17.2", + "prosemirror-example-setup": "0.17.0", + "prosemirror-history": "0.17.0", + "prosemirror-inputrules": "0.17.0", + "prosemirror-keymap": "0.17.0", + "prosemirror-menu": "0.17.0", + "prosemirror-model": "0.17.0", + "prosemirror-schema-basic": "0.17.0", + "prosemirror-schema-list": "0.17.0", + "prosemirror-state": "0.17.0", + "prosemirror-transform": "0.17.0", + "prosemirror-view": "0.17.2", "react": "15.4.2", "react-dom": "15.4.2", "rebass": "0.3.3", diff --git a/src/components/add-cover.js b/src/components/add-cover.js index 7af3eed..dd775b2 100644 --- a/src/components/add-cover.js +++ b/src/components/add-cover.js @@ -2,10 +2,10 @@ import React, {createElement as el} from 'react' import ButtonOutline from 'rebass/dist/ButtonOutline' export const buttonStyle = - { textTransform: 'uppercase' - , borderRadius: 4 - , padding: '10px 16px' - , margin: '0.25em 0.5em' + { textTransform: 'uppercase', + borderRadius: 4, + padding: '10px 16px', + margin: '0.25em 0.5em', } class AddCover extends React.Component { @@ -29,11 +29,11 @@ class AddCover extends React.Component { if (hasCover) return null return el(ButtonOutline - , { id: 'AddCover' - , style: buttonStyle - , onClick: this.boundAddImage - , rounded: true - } + , { id: 'AddCover', + style: buttonStyle, + onClick: this.boundAddImage, + rounded: true, + } , 'Add Image' ) } diff --git a/src/components/add-fold.js b/src/components/add-fold.js index 42e343b..12ab4bb 100644 --- a/src/components/add-fold.js +++ b/src/components/add-fold.js @@ -23,11 +23,11 @@ class AddFold extends React.Component { if (hasFold) return null return el(ButtonOutline - , { id: 'AddFold' - , style: buttonStyle - , onClick: this.boundAddFold - , rounded: true - } + , { id: 'AddFold', + style: buttonStyle, + onClick: this.boundAddFold, + rounded: true, + } , 'Make Full Post' ) } diff --git a/src/components/app.js b/src/components/app.js index 2563796..27085e3 100644 --- a/src/components/app.js +++ b/src/components/app.js @@ -6,10 +6,14 @@ import AddCover from './add-cover' import AddFold from './add-fold' import Editable from './editable' import rebassTheme from './rebass-theme' +import WidgetEdit from './widget-edit' +import Modal from './modal' import EdStore from '../store/ed-store' import {edCommands} from '../menu/ed-menu' +import {version as PACKAGE_VERSION} from '../../package.json' + export default class App extends React.Component { constructor (props) { @@ -32,7 +36,6 @@ export default class App extends React.Component { } const { initialContent - , onMount , onChange , onShareFile , onShareUrl @@ -42,25 +45,43 @@ export default class App extends React.Component { , onCommandsChanged } = props this._store = new EdStore( - { initialContent - , onMount - , onChange - , onShareFile - , onShareUrl - , onRequestCoverUpload - , onDropFiles - , onDropFileOnBlock - , onCommandsChanged + { initialContent, + onChange, + onShareFile, + onShareUrl, + onRequestCoverUpload, + onDropFiles, + onDropFileOnBlock, + onCommandsChanged, } ) this.routeChange = this._store.routeChange.bind(this._store) + + this._store.on('media.block.edit.open', (blockID) => { + // TODO expose prop for native editors? + this.setState({blockToEdit: blockID}) + this.blur() + }) + this.closeMediaBlockModal = () => { + this.setState({blockToEdit: null}) + } + this._store.on('media.block.edit.close', () => { + this.closeMediaBlockModal() + }) + + this.state = { + blockToEdit: null, + } } componentDidMount () { this.boundOnDragOver = this.onDragOver.bind(this) window.addEventListener('dragover', this.boundOnDragOver) this.boundOnDrop = this.onDrop.bind(this) window.addEventListener('drop', this.boundOnDrop) + if (this.props.onMount) { + this.props.onMount(this) + } } componentWillUnmount () { window.removeEventListener('dragover', this.boundOnDragOver) @@ -68,19 +89,19 @@ export default class App extends React.Component { } getChildContext () { const {imgfloConfig, featureFlags} = this.props - return ( - { imgfloConfig - , featureFlags - , rebass: rebassTheme - , store: this._store - } - ) + return ({ + imgfloConfig, + featureFlags, + rebass: rebassTheme, + store: this._store, + }) } render () { - return el('div' - , {className: 'Ed'} - , this.renderContent() - , this.renderHints() + return el('div', + {className: 'Ed'}, + this.renderContent(), + // this.renderHints(), + this.renderModal() ) } renderContent () { @@ -94,33 +115,50 @@ export default class App extends React.Component { , coverPrefs } = this.props return el('div' - , { className: 'Ed-Content' - , style: - { zIndex: 1 - } - } + , { className: 'Ed-Content', + style: + { zIndex: 1, + }, + } , el(Editable - , { initialContent - , menuBar - , onChange: this.routeChange - , onShareFile - , onShareUrl - , onCommandsChanged - , onDropFiles - , widgetPath - , coverPrefs - } + , { initialContent, + menuBar, + onChange: this.routeChange, + onShareFile, + onShareUrl, + onCommandsChanged, + onDropFiles, + widgetPath, + coverPrefs, + } ) ) } renderHints () { return el('div' - , { className: 'Ed-Hints' - } + , { className: 'Ed-Hints', + } , el(AddCover, {}) , el(AddFold, {}) ) } + renderModal () { + const {blockToEdit} = this.state + if (!blockToEdit) return + const initialBlock = this._store.getBlock(blockToEdit) + if (!initialBlock) return + const {coverPrefs} = this.props + + return el(Modal, + { + onClose: this.closeMediaBlockModal, + child: el(WidgetEdit, { + initialBlock, + coverPrefs, + }), + } + ) + } onDragOver (event) { // Listening to window event.preventDefault() @@ -141,7 +179,8 @@ export default class App extends React.Component { if (!item) { throw new Error('commandName not found') } - item.spec.run(this._store.pm) + const {state, dispatch} = this._store.pm.editor + item.spec.run(state, dispatch) } insertPlaceholders (index, count) { return this._store.insertPlaceholders(index, count) @@ -161,34 +200,42 @@ export default class App extends React.Component { indexOfFold () { return this._store.indexOfFold() } + blur () { + this.pm.editor.content.blur() + window.getSelection().removeAllRanges() + } get pm () { return this._store.pm } -} -App.childContextTypes = - { imgfloConfig: React.PropTypes.object - , store: React.PropTypes.object - , rebass: React.PropTypes.object - , featureFlags: React.PropTypes.object - } -App.propTypes = - { initialContent: React.PropTypes.array.isRequired - , onChange: React.PropTypes.func.isRequired - , onShareFile: React.PropTypes.func.isRequired - , onShareUrl: React.PropTypes.func.isRequired - , onDropFiles: React.PropTypes.func - , onCommandsChanged: React.PropTypes.func - , onRequestCoverUpload: React.PropTypes.func.isRequired - , imgfloConfig: React.PropTypes.object - , widgetPath: React.PropTypes.string - , coverPrefs: React.PropTypes.object - , menuBar: React.PropTypes.bool - , onMount: React.PropTypes.func - , onDropFileOnBlock: React.PropTypes.func - , featureFlags: React.PropTypes.object - } -App.defaultProps = - { widgetPath: './node_modules/' - , menuBar: true - , featureFlags: {} + get version () { + return PACKAGE_VERSION } +} +App.childContextTypes = { + imgfloConfig: React.PropTypes.object, + store: React.PropTypes.object, + rebass: React.PropTypes.object, + featureFlags: React.PropTypes.object, +} +App.propTypes = { + initialContent: React.PropTypes.array.isRequired, + onChange: React.PropTypes.func.isRequired, + onShareFile: React.PropTypes.func.isRequired, + onShareUrl: React.PropTypes.func.isRequired, + onDropFiles: React.PropTypes.func, + onCommandsChanged: React.PropTypes.func, + onRequestCoverUpload: React.PropTypes.func.isRequired, + imgfloConfig: React.PropTypes.object, + widgetPath: React.PropTypes.string, + coverPrefs: React.PropTypes.object, + menuBar: React.PropTypes.bool, + onMount: React.PropTypes.func, + onDropFileOnBlock: React.PropTypes.func, + featureFlags: React.PropTypes.object, +} +App.defaultProps = { + widgetPath: './node_modules/', + menuBar: true, + coverPrefs: {}, + featureFlags: {}, +} diff --git a/src/components/attribution-editor.css b/src/components/attribution-editor.css index 1eeb4b5..3f3c508 100644 --- a/src/components/attribution-editor.css +++ b/src/components/attribution-editor.css @@ -1,3 +1,7 @@ +.AttributionEditor { + margin-bottom: -1rem; +} + .AttributionEditor-title textarea { font-size: 24px !important; margin-top: 0 !important; diff --git a/src/components/attribution-editor.js b/src/components/attribution-editor.js index de81cb1..04d8c7d 100644 --- a/src/components/attribution-editor.js +++ b/src/components/attribution-editor.js @@ -25,9 +25,9 @@ class AttributionEditor extends React.Component { constructor (props) { super(props) this.state = - { block: props.initialBlock - , showDropIndicator: false - } + { block: props.initialBlock, + showDropIndicator: false, + } this.boundOnDragOver = this.onDragOver.bind(this) this.boundOnDragEnter = this.onDragEnter.bind(this) @@ -55,33 +55,33 @@ class AttributionEditor extends React.Component { const menus = renderMenus(type, schema, metadata, cover, this.boundOnChange, this.boundOnMoreClick, this.boundOnUploadRequest, this.boundOnCoverRemove, coverPrefs) return el('div' - , { className: `AttributionEditor AttributionEditor-${type}` - , style: widgetStyle - , onDragOver: this.boundOnDragOver - , onDragEnter: this.boundOnDragEnter - , onDragLeave: this.boundOnDragLeave - , onDrop: this.boundOnDrop - } + , { className: `AttributionEditor AttributionEditor-${type}`, + style: widgetStyle, + onDragOver: this.boundOnDragOver, + onDragEnter: this.boundOnDragEnter, + onDragLeave: this.boundOnDragLeave, + onDrop: this.boundOnDrop, + } , this.renderPlay() , this.renderCover() , this.renderUnsalvageable() , this.renderFailed() , this.renderProgress() , el('div' - , { className: 'AttributionEditor-metadata' - , style: - { position: 'relative' - } - } + , { className: 'AttributionEditor-metadata', + style: + { position: 'relative', + }, + } , renderFields(schema, metadata, this.boundOnChange, type, html) , el('div' - , { className: 'AttributionEditor-links' - , style: - { margin: '1em -1em 0' - , position: 'relative' - , top: 1 - } - } + , { className: 'AttributionEditor-links', + style: + { margin: '1em -1em 0', + position: 'relative', + top: 1, + }, + } , el(DropdownGroup, {menus}) ) ) @@ -105,7 +105,7 @@ class AttributionEditor extends React.Component { const {store} = this.context const preview = store.getCoverPreview(id) if (!cover && !preview) return - if (cover.unsalvageable) return + if (cover && cover.unsalvageable) return let src, width, height, title if (cover) { src = cover.src @@ -121,13 +121,13 @@ class AttributionEditor extends React.Component { if (!src) return let props = {src, width, height, title} return el('div' - , { className: 'AttributionEditor-cover' - , style: - { width: '100%' - , position: 'relative' - , marginBottom: '1rem' - } - } + , { className: 'AttributionEditor-cover', + style: + { width: '100%', + position: 'relative', + marginBottom: '1rem', + }, + } , el(Image, props) ) } @@ -138,11 +138,11 @@ class AttributionEditor extends React.Component { let upload = null if (this.canChangeCover()) { upload = el(Button - , { onClick: this.boundOnUploadRequest - , rounded: true - , color: 'error' - , backgroundColor: 'white' - } + , { onClick: this.boundOnUploadRequest, + rounded: true, + color: 'error', + backgroundColor: 'white', + } , 'Upload New Image' ) } @@ -163,18 +163,18 @@ class AttributionEditor extends React.Component { let upload = null if (this.canChangeCover()) { upload = el(Button - , { onClick: this.boundOnUploadRequest - , rounded: true - , color: 'error' - , backgroundColor: 'white' - } + , { onClick: this.boundOnUploadRequest, + rounded: true, + color: 'error', + backgroundColor: 'white', + } , 'Upload Image' ) } return el(Message , {theme: 'error'} - , 'Upload failed, please try again' + , 'Upload failed, please try again.' , el(Space, {auto: true}) , upload ) @@ -189,10 +189,10 @@ class AttributionEditor extends React.Component { const color = (failed === true ? 'error' : 'info') return el(Progress - , { value: progress / 100 - , style: {margin: '8px 0'} - , color - } + , { value: progress / 100, + style: {margin: '8px 0'}, + color, + } ) } renderPlay () { @@ -203,20 +203,20 @@ class AttributionEditor extends React.Component { return el('div' , { style: - { textAlign: 'right' - , position: 'relative' - , top: '-0.5rem' - } - } + { textAlign: 'right', + position: 'relative', + top: '-0.5rem', + }, + } , el('a' - , { href: block.metadata.isBasedOnUrl - , target: '_blank' - , rel: 'noreferrer noopener' - , style: - { textDecoration: 'inherit' - , textTransform: 'uppercase' - } - } + , { href: block.metadata.isBasedOnUrl, + target: '_blank', + rel: 'noreferrer noopener', + style: + { textDecoration: 'inherit', + textTransform: 'uppercase', + }, + } , type + ' ' , el(PlayIcon) ) @@ -228,23 +228,23 @@ class AttributionEditor extends React.Component { return el('div' , { style: - { position: 'absolute' - , display: 'flex' - , justifyContent: 'center' - , alignItems: 'center' - , top: 0 - , left: 0 - , width: '100%' - , height: '100%' - , textAlign: 'center' - , fontSize: 36 - , fontWeight: 600 - , color: '#0088EE' - , padding: '1rem' - , backgroundColor: 'rgba(255, 255, 255, 0.9)' - , border: '12px #0088EE solid' - } - } + { position: 'absolute', + display: 'flex', + justifyContent: 'center', + alignItems: 'center', + top: 0, + left: 0, + width: '100%', + height: '100%', + textAlign: 'center', + fontSize: 36, + fontWeight: 600, + color: '#0088EE', + padding: '1rem', + backgroundColor: 'rgba(255, 255, 255, 0.9)', + border: '12px #0088EE solid', + }, + } , 'Drop to replace this image' ) } @@ -301,9 +301,9 @@ class AttributionEditor extends React.Component { const {store} = this.context const {id} = this.props store.routeChange('MEDIA_BLOCK_DROP_FILE' - , { id - , file: event.dataTransfer.files[0] - } + , { id, + file: event.dataTransfer.files[0], + } ) this.setState({showDropIndicator: false}) } @@ -317,6 +317,7 @@ class AttributionEditor extends React.Component { switch (key) { case 'delete': store.routeChange('MEDIA_BLOCK_REMOVE', id) + store.trigger('media.block.edit.close') return case 'isBasedOnUrl': path = ['metadata', 'isBasedOnUrl'] @@ -351,15 +352,15 @@ class AttributionEditor extends React.Component { AttributionEditor.contextTypes = { store: React.PropTypes.object } AttributionEditor.childContextTypes = - { imgfloConfig: React.PropTypes.object - , rebass: React.PropTypes.object - , store: React.PropTypes.object - } +{ imgfloConfig: React.PropTypes.object, + rebass: React.PropTypes.object, + store: React.PropTypes.object, +} AttributionEditor.propTypes = - { initialBlock: React.PropTypes.object.isRequired - , id: React.PropTypes.string.isRequired - , coverPrefs: React.PropTypes.object.isRequired - } +{ initialBlock: React.PropTypes.object.isRequired, + id: React.PropTypes.string.isRequired, + coverPrefs: React.PropTypes.object.isRequired, +} export default React.createFactory(AttributionEditor) @@ -397,13 +398,13 @@ function renderFields (schema, metadata = {}, onChange, type, html) { function renderTextField (key, label, value, onChange, placeholder) { return el(TextareaAutosize - , { className: `AttributionEditor-${key}` - , placeholder: placeholder || `Enter ${key}` - , defaultValue: value - , key: key - , onChange: makeChange(['metadata', key], onChange) - , style: {width: '100%'} - } + , { className: `AttributionEditor-${key}`, + placeholder: placeholder || `Enter ${key}`, + defaultValue: value, + key: key, + onChange: makeChange(['metadata', key], onChange), + style: {width: '100%'}, + } ) } @@ -440,11 +441,11 @@ function renderMenus (type, schema, metadata = {}, cover, onChange, onMoreClick, } menus.push( el(CreditAdd - , { schema - , metadata - , label: '...' - , onClick: onMoreClick - } + , { schema, + metadata, + label: '...', + onClick: onMoreClick, + } ) ) return menus @@ -452,36 +453,36 @@ function renderMenus (type, schema, metadata = {}, cover, onChange, onMoreClick, function renderCreditEditor (onlyUrl, key, label, item, onChange, path) { return el(CreditEditor - , { className: `AttributionEditor-${key}` - , key: key - , label: label - , name: item.name - , url: item.url - , avatar: item.avatar - , path: path || [key] - , onChange - , onlyUrl - } + , { className: `AttributionEditor-${key}`, + key: key, + label: label, + name: item.name, + url: item.url, + avatar: item.avatar, + path: path || [key], + onChange, + onlyUrl, + } ) } function renderImageEditor (hasCover, allowCoverChange, allowCoverRemove, type, title, coverPrefs = {}, onChange, onUploadRequest, onCoverRemove, siteCoverPrefs) { const {filter, crop, overlay} = coverPrefs return el(ImageEditor - , { hasCover - , allowCoverChange - , allowCoverRemove - , title - , siteCoverPrefs - , filter - , crop - , overlay - , onChange - , onUploadRequest - , onCoverRemove - , type - , name: 'Image' - , label: 'Image' - } + , { hasCover, + allowCoverChange, + allowCoverRemove, + title, + siteCoverPrefs, + filter, + crop, + overlay, + onChange, + onUploadRequest, + onCoverRemove, + type, + name: 'Image', + label: 'Image', + } ) } diff --git a/src/components/attribution-view.js b/src/components/attribution-view.js new file mode 100644 index 0000000..893cbeb --- /dev/null +++ b/src/components/attribution-view.js @@ -0,0 +1,81 @@ +import React, {createElement as el} from 'react' +import imgflo from 'imgflo-url' +import Avatar from 'rebass/dist/Avatar' + +class AttributionView extends React.Component { + constructor (props) { + super(props) + this.renderAuthor = this.renderAuthor.bind(this) + } + render () { + const {metadata} = this.props + if (!metadata) return null + + return el('div', {}, + this.renderAuthors(), + this.renderPublisher(), + this.renderVia() + ) + } + renderAuthors () { + const {metadata} = this.props + const {author} = metadata + if (!author || !author.length) return null + return el('span', {}, + 'by ', + author.map(this.renderAuthor), + ) + } + renderAuthor (author) { + const {name, avatar} = author + const {imgfloConfig} = this.context + let avatarEl + if (avatar && avatar.src) { + let {src} = avatar + if (imgfloConfig) { + const params = { + input: src, + width: 24, + } + src = imgflo(imgfloConfig, 'passthrough', params) + } + avatarEl = el(Avatar, + { + src, + size: 24, + style: {verticalAlign: 'middle'}, + } + ) + } + return el('span', {}, + avatarEl, + ' ', + (name || 'Credit') + ) + } + renderPublisher () { + const {metadata} = this.props + const {publisher} = metadata + if (!publisher || !publisher.name) return null + return el('span', {}, + ' on ', + publisher.name + ) + } + renderVia () { + const {metadata} = this.props + const {via} = metadata + if (!via || !via.name) return null + return el('span', {}, + ' via ', + via.name + ) + } +} +AttributionView.propTypes = { + metadata: React.PropTypes.object, +} +AttributionView.contextTypes = + { imgfloConfig: React.PropTypes.object } + +export default React.createFactory(AttributionView) diff --git a/src/components/button-confirm.js b/src/components/button-confirm.js index a9afda9..8c52d4d 100644 --- a/src/components/button-confirm.js +++ b/src/components/button-confirm.js @@ -13,11 +13,11 @@ class ButtonConfirm extends React.Component { const {open} = this.state return el(ButtonOutline - , { children: (open ? confirm : label) - , onClick: (open ? onClick : this.boundOnConfirm) - , theme - , style - } + , { children: (open ? confirm : label), + onClick: (open ? onClick : this.boundOnConfirm), + theme, + style, + } ) } onConfirm () { @@ -25,10 +25,10 @@ class ButtonConfirm extends React.Component { } } ButtonConfirm.propTypes = - { confirm: React.PropTypes.string.isRequired - , label: React.PropTypes.string.isRequired - , theme: React.PropTypes.string - , style: React.PropTypes.object - , onClick: React.PropTypes.func.isRequired - } +{ confirm: React.PropTypes.string.isRequired, + label: React.PropTypes.string.isRequired, + theme: React.PropTypes.string, + style: React.PropTypes.object, + onClick: React.PropTypes.func.isRequired, +} export default React.createFactory(ButtonConfirm) diff --git a/src/components/credit-add.js b/src/components/credit-add.js index cf56257..1ba40fc 100644 --- a/src/components/credit-add.js +++ b/src/components/credit-add.js @@ -33,17 +33,20 @@ function makeLinks (schema, metadata = {}, onClick) { } function makeLink (key, label, onClick, confirm = false) { - const Component = (confirm ? NavItemConfirm : NavItem) - return el(Component - , { key - , children: label - , label - , confirm: (confirm ? 'Are you sure?' : null) - , theme: (confirm ? 'warning' : 'primary') - , style: { display: 'block' } - , onClick: makeClick(key, onClick) - } - ) + let Component = NavItem + let props = { + key, + children: label, + label, + style: { display: 'block' }, + onClick: makeClick(key, onClick), + } + if (confirm) { + Component = NavItemConfirm + props.confirm = 'Delete forever now.' + props.theme = 'warning' + } + return el(Component, props) } function makeClick (key, onClick) { diff --git a/src/components/credit-editor.js b/src/components/credit-editor.js index 10cdf41..f17767f 100644 --- a/src/components/credit-editor.js +++ b/src/components/credit-editor.js @@ -11,13 +11,13 @@ import ButtonConfirm from './button-confirm' export default function CreditEditor (props, context) { const {name, label, url, avatar, onChange, onlyUrl, path} = props - return el('div' - , { style: - { padding: '1rem' - , width: 360 - , maxWidth: '100%' - } - } + return el('div', { + style: { + padding: '1rem', + width: 360, + maxWidth: '100%', + }, + } , renderAvatar(avatar, context.imgfloConfig) , (onlyUrl ? '' : renderLabel(label)) , (onlyUrl @@ -34,29 +34,29 @@ function renderAvatar (avatar, imgfloConfig) { if (!avatar || !avatar.src) return let {src} = avatar if (imgfloConfig) { - const params = - { input: src - , width: 72 - } + const params = { + input: src, + width: 72, + } src = imgflo(imgfloConfig, 'passthrough', params) } return el(Avatar, - { key: 'avatar' - , style: {float: 'right'} - , src + { key: 'avatar', + style: {float: 'right'}, + src, } ) } function renderRemove (onChange, path) { return el(ButtonConfirm - , { onClick: makeRemove(onChange, path) - , style: {float: 'right'} - , theme: 'warning' - , title: 'delete attribution from block' - , label: 'Remove' - , confirm: 'Remove: Are you sure?' - } + , { onClick: makeRemove(onChange, path), + style: {float: 'right'}, + theme: 'warning', + title: 'delete attribution from block', + label: 'Remove', + confirm: 'Remove: Are you sure?', + } ) } @@ -69,9 +69,9 @@ function renderLabel (label) { function renderFields (name, url, avatar, onChange, path) { return ( - [ renderTextField('name', 'Name', name, onChange, path.concat(['name']), true) - , renderTextField('url', 'Link', url, onChange, path.concat(['url']), false, isUrlOrBlank, 'https...') - ] + [ renderTextField('name', 'Name', name, onChange, path.concat(['name']), true), + renderTextField('url', 'Link', url, onChange, path.concat(['url']), false, isUrlOrBlank, 'https...'), + ] ) } @@ -81,18 +81,18 @@ function renderBasedOnUrl (value, onChange, path) { function renderTextField (key, label, value, onChange, path, defaultFocus, validator, placeholder) { return el(TextareaAutosize - , { className: `AttributionEditor-${key}` - , label - , defaultValue: value - , defaultFocus - , key: key - , name: key - , multiLine: true - , style: {width: '100%'} - , onChange: makeChange(onChange, path) - , validator - , placeholder - } + , { className: `AttributionEditor-${key}`, + label, + defaultValue: value, + defaultFocus, + key: key, + name: key, + multiLine: true, + style: {width: '100%'}, + onChange: makeChange(onChange, path), + validator, + placeholder, + } ) } diff --git a/src/components/dropdown-group.js b/src/components/dropdown-group.js index b5bd8dd..a68fdca 100644 --- a/src/components/dropdown-group.js +++ b/src/components/dropdown-group.js @@ -1,3 +1,5 @@ +/* eslint react/no-find-dom-node: [0] */ + import React, {createElement as el} from 'react' import Menu from 'rebass/dist/Menu' @@ -18,20 +20,23 @@ class DropdownGroup extends React.Component { constructor (props) { super(props) this.state = { - openMenu: null + openMenu: null, } this.boundCloseMenu = this.closeMenu.bind(this) this.nodeRefCallback = (node) => { this.node = node } + this.onKeyDown = (event) => { + if (event.key === 'Enter') { + event.preventDefault() + event.stopPropagation() + this.setState({openMenu: null}) + } + } } componentDidMount () { // Click away to dismiss - const el = document.querySelector('.ProseMirror-content') - el.addEventListener('focus', this.boundCloseMenu) document.body.addEventListener('click', this.boundCloseMenu) } componentWillUnmount () { - const el = document.querySelector('.ProseMirror-content') - el.removeEventListener('focus', this.boundCloseMenu) document.body.removeEventListener('click', this.boundCloseMenu) } componentWillReceiveProps (nextProps) { @@ -64,12 +69,14 @@ class DropdownGroup extends React.Component { } } render () { - return el('div' - , { className: 'DropdownGroup' - , ref: this.nodeRefCallback - } - , this.renderButtons() - , this.renderMenu() + return el('div', + { + className: 'DropdownGroup', + ref: this.nodeRefCallback, + onKeyDown: this.onKeyDown, + }, + this.renderButtons(), + this.renderMenu() ) } renderButtons () { @@ -81,32 +88,31 @@ class DropdownGroup extends React.Component { // HACK const {name, label} = menus[i].props buttons.push( - el(ButtonOutline - , { key: `button${i}` - , onClick: this.makeOpenMenu(i) - , theme: (openMenu === i ? 'primary' : theme) - , inverted: false - , style: - { borderWidth: 0 - , boxShadow: 'none' - , outline: 'none' - } - , rounded: false - , title: `Edit ${label}` - } - , el('span' - , { style: - { maxWidth: '15rem' - , verticalAlign: 'middle' - , display: 'inline-block' - , whiteSpace: 'pre' - , overflow: 'hidden' - , textOverflow: 'ellipsis' - , textTransform: 'uppercase' - } - } - , (name || label) - ) + el(ButtonOutline, { + key: `button${i}`, + onClick: this.makeOpenMenu(i), + theme: (openMenu === i ? 'primary' : theme), + inverted: false, + style: { + borderWidth: 0, + boxShadow: 'none', + outline: 'none', + }, + rounded: false, + title: `Edit ${label}`, + }, + el('span', { + style: { + maxWidth: '15rem', + verticalAlign: 'middle', + display: 'inline-block', + whiteSpace: 'pre', + overflow: 'hidden', + textOverflow: 'ellipsis', + textTransform: 'uppercase', + }}, + (name || label) + ) ) ) } @@ -118,21 +124,18 @@ class DropdownGroup extends React.Component { if (openMenu == null) return - return el('div' - , { style: - { position: 'relative' } - } - , el(Menu - , { theme - , style: - { textAlign: 'left' - , position: 'absolute' - , top: -1 - , right: -1 - , zIndex: 100 - } - } - , menus[openMenu] + return el('div', {style: { position: 'relative' }}, + el(Menu, { + theme, + style: { + textAlign: 'left', + position: 'absolute', + top: -1, + right: -1, + zIndex: 100, + marginBottom: '5rem', + }, + }, menus[openMenu] ) ) } @@ -157,9 +160,9 @@ class DropdownGroup extends React.Component { } } DropdownGroup.propTypes = - { menus: React.PropTypes.array.isRequired - , theme: React.PropTypes.string - } +{ menus: React.PropTypes.array.isRequired, + theme: React.PropTypes.string, +} DropdownGroup.defaultProps = { theme: 'secondary' } export default React.createFactory(DropdownGroup) diff --git a/src/components/editable-feature-flags.css b/src/components/editable-feature-flags.css index bc5791d..bcd12a5 100644 --- a/src/components/editable-feature-flags.css +++ b/src/components/editable-feature-flags.css @@ -1,4 +1,5 @@ .FlaggedWidget > div { + position: relative; } .FlaggedWidget > div::after { content: 'Paid feature: will not show on site until upgrade.'; @@ -10,6 +11,7 @@ position: absolute; border: 2px solid red; padding: 2px 4px; + border-radius: 0 2px 0 0; } .ProseMirror-menu-dropdown-item .flaggedFeature { diff --git a/src/components/editable.css b/src/components/editable.css index d15b7c5..46d7d9c 100644 --- a/src/components/editable.css +++ b/src/components/editable.css @@ -65,31 +65,22 @@ .ProseMirror-content h1 { font-size: 250%; - line-height: 1.2 !important; + line-height: 1.2; } .ProseMirror-content h2 { font-size: 175%; - line-height: 1.3 !important; + line-height: 1.3; } .ProseMirror-content h3 { font-size: 125%; - line-height: 1.4 !important; + line-height: 1.4; } -.ProseMirror-content h1 * { - line-height: 1.2 !important; -} -.ProseMirror-content h2 * { - line-height: 1.3 !important; -} -.ProseMirror-content h3 * { - line-height: 1.4 !important; -} - -.ProseMirror-content p * { - font-size: 20px !important; +.ProseMirror-content p { + font-size: 100%; + line-height: 1.5; } .ProseMirror-content p:first-child, .ProseMirror-content h1:first-child, .ProseMirror-content h2:first-child, .ProseMirror-content h3:first-child, .ProseMirror-content h4:first-child, .ProseMirror-content h5:first-child, .ProseMirror-content h6:first-child { @@ -119,11 +110,21 @@ } .ProseMirror-prompt { - padding: 1em; + background-color: white; + border: 1px silver solid; + + position: absolute; + padding: 0.5em; +} + +.ProseMirror-prompt input { + padding: .5em; + display: block; + margin-bottom: 0.5em; } -.ProseMirror-prompt input[type='text']{ - padding: .5em 1em; +.ProseMirror-prompt h5 { + margin: 0 0 0.5em 0; } .ProseMirror-prompt .ProseMirror-prompt-close { @@ -133,6 +134,9 @@ .ProseMirror-content hr { border-width: 2px 0 0 0; + /* Make it easier to tap */ + padding-bottom: 1rem; + margin-bottom: 0.75rem; } /* Placeholder text hacks */ @@ -143,13 +147,25 @@ content: 'Title'; opacity: 0.2; position: absolute; - bottom: 0; + top: 0; +} +.ProseMirror-content > h2.empty::after { + content: 'Section'; + opacity: 0.2; + position: absolute; + top: 0; +} +.ProseMirror-content > h3.empty::after { + content: 'Subsection'; + opacity: 0.2; + position: absolute; + top: 0; } .ProseMirror-content > p.empty::after { content: '¶'; opacity: 0.2; position: absolute; - bottom: 0; + top: 0; } diff --git a/src/components/editable.js b/src/components/editable.js index 778daee..371c0ae 100644 --- a/src/components/editable.js +++ b/src/components/editable.js @@ -1,26 +1,30 @@ +require('prosemirror-menu/style/menu.css') +require('prosemirror-view/style/prosemirror.css') require('./editable.css') require('./editable-menu.css') require('./editable-feature-flags.css') import React, {createElement as el} from 'react' -import {ProseMirror} from 'prosemirror/dist/edit/main' -import {Plugin} from 'prosemirror/dist/edit/plugin' +import {EditorState, Plugin, NodeSelection} from 'prosemirror-state' +import {history as pluginHistory} from 'prosemirror-history' +// import {dropCursor as pluginDropCursor} from 'prosemirror-dropcursor' -import {menuBar as pluginMenuBar} from 'prosemirror/dist/menu' -import {patchMenuWithFeatureFlags} from '../menu/ed-menu' +import {MenuBarEditorView} from 'prosemirror-menu' +import {edMenuPlugin, edMenuEmptyPlugin, patchMenuWithFeatureFlags} from '../menu/ed-menu' import GridToDoc from '../convert/grid-to-doc' -import EdKeymap from '../inputrules/ed-keymap' -import EdSchemaFull from '../schema/ed-schema-full' -import EdInputRules from '../inputrules/ed-input-rules' +import EdSchema from '../schema/ed-schema' +import {MediaNodeView} from '../schema/media' +import {edInputRules, edBaseKeymap, edKeymap} from '../inputrules/ed-input-rules' import {posToIndex} from '../util/pm' +import {isDropFileEvent} from '../util/drop' -import PluginWidget from '../plugins/widget.js' import PluginShareUrl from '../plugins/share-url' -import PluginContentHints from '../plugins/content-hints' +// import PluginContentHints from '../plugins/content-hints' import PluginPlaceholder from '../plugins/placeholder' import PluginFixedMenuHack from '../plugins/fixed-menu-hack' import PluginCommandsInterface from '../plugins/commands-interface' +import {PluginStoreRef} from '../plugins/store-ref' class Editable extends React.Component { @@ -32,14 +36,11 @@ class Editable extends React.Component { throw new Error('Can not setState of Editable') } render () { - return el('div' - , { className: 'Editable' - , style: - { position: 'relative' /* So widgets can position selves */ - } - } - , el('div', {className: 'Editable-Mirror', ref: 'mirror'}) - , el('div', {className: 'Editable-Plugins', ref: 'plugins'}) + return el('div', { + className: 'Editable', + }, + el('div', {className: 'Editable-Mirror', ref: 'mirror'}), + el('div', {className: 'Editable-Plugins', ref: 'plugins'}) ) } componentDidMount () { @@ -50,103 +51,129 @@ class Editable extends React.Component { , onCommandsChanged , widgetPath , coverPrefs } = this.props - const {store, featureFlags} = this.context - - // PM setup - let pmOptions = - { place: mirror - , autoInput: true - // , commands: commands - , doc: GridToDoc(initialContent) - , schema: EdSchemaFull - , plugins: [ EdInputRules ] - } - - let edPluginClasses = - [ PluginWidget - , PluginShareUrl - , PluginContentHints - , PluginPlaceholder - ] - + const {store, imgfloConfig, featureFlags} = this.context + + let edPlugins = [ + pluginHistory(), + edInputRules, + edKeymap, + edBaseKeymap, + ] + + let edPluginClasses = [ + PluginStoreRef, + PluginShareUrl, + PluginPlaceholder, + ] + + patchMenuWithFeatureFlags(featureFlags) if (menuBar) { - const menuContent = patchMenuWithFeatureFlags(featureFlags) - let menu = pluginMenuBar.config( - { float: false - , content: menuContent - } - ) - pmOptions.plugins.push(menu) + edPlugins.push(edMenuPlugin) edPluginClasses.push(PluginFixedMenuHack) + } else { + edPlugins.push(edMenuEmptyPlugin) } if (onCommandsChanged) { edPluginClasses.push(PluginCommandsInterface) } - const pluginOptions = - { ed: store - , editableView: this - , container: plugins - , widgetPath - , featureFlags - , coverPrefs - } + const pluginProps = { + ed: store, + editableView: this, + elMirror: mirror, + elPlugins: plugins, + widgetPath, + coverPrefs, + } edPluginClasses.forEach(function (plugin) { - const p = new Plugin(plugin, pluginOptions) - pmOptions.plugins.push(p) + // FIXME least knowledge per plugin + plugin.edStuff = pluginProps + const p = new Plugin(plugin) + edPlugins.push(p) }) - this.pm = new ProseMirror(pmOptions) - this.pm.ed = store - - this.pm.on.change.add(() => { - onChange('EDITABLE_CHANGE', this.pm) + const state = EditorState.create({ + schema: EdSchema, + doc: GridToDoc(initialContent), + plugins: edPlugins, + ed: store, }) - this.pm.on.domDrop.add(this.boundOnDrop) + let view - this.pm.addKeymap(EdKeymap) + const applyTransaction = (transaction) => { + view.updateState(view.editor.state.apply(transaction)) + if (transaction.steps.length) { + onChange('EDITABLE_CHANGE', this.pm) + } + } + + // PM setup + let pmOptions = + { state, + autoInput: true, + spellcheck: true, + dispatchTransaction: applyTransaction, + handleClickOn: function (_view, _pos, node) { return node.type.name === 'media' }, + nodeViews: { + media: (node, view, getPos) => { + return new MediaNodeView(node, view, getPos, store, imgfloConfig, coverPrefs, widgetPath, featureFlags) + }, + }, + editable: function (state) { return true }, + attributes: { class: 'ProseMirror-content' }, + handleDOMEvents: { + drop: this.boundOnDrop, + }, + // Don't type over node selection + handleTextInput: function (view, from, to, text) { + if (view.state.selection instanceof NodeSelection) { + return true + } + }, + } + view = this.pm = new MenuBarEditorView(mirror, pmOptions) + this.pm.ed = store onChange('EDITABLE_INITIALIZE', this) } componentWillUnmount () { - // this.pm.off('change') - // this.pm.off('ed.plugin.url') - // this.pm.off('ed.menu.file') - // this.pm.off('drop', this.boundOnDrop) - const pluginKeys = Object.keys(this.pm.plugin) - pluginKeys.forEach((key) => this.pm.plugin[key].detach()) + this.pm.editor.destroy() } - onDrop (event) { - if (!event.dataTransfer || !event.dataTransfer.files || !event.dataTransfer.files.length) return + onDrop (editor, event) { + if (!isDropFileEvent(event)) return const {onDropFiles} = this.props if (!onDropFiles) return - const pos = this.pm.posAtCoords({left: event.clientX, top: event.clientY}) + const {pos} = this.pm.editor.posAtCoords({left: event.clientX, top: event.clientY}) if (pos == null) return - const index = posToIndex(this.pm.doc, pos) + const index = posToIndex(editor.state.doc, pos) if (index == null) return event.preventDefault() event.stopPropagation() onDropFiles(index, event.dataTransfer.files) } } -Editable.contextTypes = - { store: React.PropTypes.object - , featureFlags: React.PropTypes.object - } -Editable.propTypes = - { initialContent: React.PropTypes.array.isRequired - , menuBar: React.PropTypes.bool - , onChange: React.PropTypes.func.isRequired - , onShareFile: React.PropTypes.func - , onShareUrl: React.PropTypes.func - , onDropFiles: React.PropTypes.func - , onEditableInit: React.PropTypes.func - , onCommandsChanged: React.PropTypes.func - , widgetPath: React.PropTypes.string - , coverPrefs: React.PropTypes.object - } +Editable.contextTypes = { + store: React.PropTypes.object, + imgfloConfig: React.PropTypes.object, + featureFlags: React.PropTypes.object, +} +Editable.propTypes = { + initialContent: React.PropTypes.array.isRequired, + menuBar: React.PropTypes.bool, + onChange: React.PropTypes.func.isRequired, + onShareFile: React.PropTypes.func, + onShareUrl: React.PropTypes.func, + onDropFiles: React.PropTypes.func, + onEditableInit: React.PropTypes.func, + onCommandsChanged: React.PropTypes.func, + widgetPath: React.PropTypes.string, + coverPrefs: React.PropTypes.object, +} +Editable.defaultProps = { + coverPrefs: {}, +} export default React.createFactory(Editable) diff --git a/src/components/icons.js b/src/components/icons.js index 7594f8c..cdb6fb2 100644 --- a/src/components/icons.js +++ b/src/components/icons.js @@ -2,37 +2,42 @@ import React, {createElement as el} from 'react' +// paths from https://github.com/jxnblk/geomicons-open/blob/master/src/play.js +const PATHS = { + play: 'M4 4 L28 16 L4 28 z', + link: 'M0 16 A8 8 0 0 1 8 8 L14 8 A8 8 0 0 1 22 16 L18 16 A4 4 0 0 0 14 12 L8 12 A4 4 0 0 0 4 16 A4 4 0 0 0 8 20 L10 24 L8 24 A8 8 0 0 1 0 16z M22 8 L24 8 A8 8 0 0 1 32 16 A8 8 0 0 1 24 24 L18 24 A8 8 0 0 1 10 16 L14 16 A4 4 0 0 0 18 20 L24 20 A4 4 0 0 0 28 16 A4 4 0 0 0 24 12z', +} + -// path from https://github.com/jxnblk/geomicons-open/blob/master/src/play.js -export function Play (props) { - const {fill, width, height} = props +export default function Icon (props) { + const {fill, width, height, icon} = props return el('svg' - , { viewBox: '0 0 32 32' - , fill - , width - , height - } + , { viewBox: '0 0 32 32', + fill, + width, + height, + style: {verticalAlign: 'middle'}, + } , el('path' - , { d: 'M4 4 L28 16 L4 28 z' - } + , { d: PATHS[icon], + } ) ) } -Play.defaultProps = - { fill: 'currentColor' - , width: '0.9em' - , height: '0.9em' - } -Play.propTypes = - { fill: React.PropTypes.string - , width: React.PropTypes.oneOfType( - [ React.PropTypes.string - , React.PropTypes.number - ] - ) - , height: React.PropTypes.oneOfType( - [ React.PropTypes.string - , React.PropTypes.number - ] - ) - } +Icon.defaultProps = { + fill: 'currentColor', + width: '1em', + height: '1em', +} +Icon.propTypes = { + icon: React.PropTypes.string.isRequired, + fill: React.PropTypes.string, + width: React.PropTypes.oneOfType([ + React.PropTypes.string, + React.PropTypes.number, + ]), + height: React.PropTypes.oneOfType([ + React.PropTypes.string, + React.PropTypes.number, + ]), +} diff --git a/src/components/image-editor.js b/src/components/image-editor.js index d8c943b..2069cd8 100644 --- a/src/components/image-editor.js +++ b/src/components/image-editor.js @@ -15,7 +15,7 @@ export default function ImageEditor (props, context) { , overlay , onChange , onUploadRequest - , onCoverRemove + , onCoverRemove, } = props let toggles = null @@ -30,11 +30,11 @@ export default function ImageEditor (props, context) { return el('div' , { style: - { padding: '1rem' - , width: 288 - , maxWidth: '100%' - } - } + { padding: '1rem', + width: 288, + maxWidth: '100%', + }, + } , toggles , (allowCoverChange ? renderUploadButton(onUploadRequest) : null) , (allowCoverRemove ? renderRemoveButton(onCoverRemove) : null) @@ -44,15 +44,15 @@ export default function ImageEditor (props, context) { function renderToggle (key, label, value, onChange, path, siteAllow) { const readOnly = (siteAllow === false) return el(Checkbox - , { key - , label: label + (readOnly ? ' (off site-wide)' : '') - , name: key - , checked: (siteAllow !== false && value !== false) - , style: (readOnly ? {opacity: 0.5} : {}) - , readOnly - , disabled: readOnly - , onChange: makeChange(path, onChange, true) - } + , { key, + label: label + (readOnly ? ' (off site-wide)' : ''), + name: key, + checked: (siteAllow !== false && value !== false), + style: (readOnly ? {opacity: 0.5} : {}), + readOnly, + disabled: readOnly, + onChange: makeChange(path, onChange, true), + } ) } @@ -65,24 +65,24 @@ function makeChange (path, onChange, checked = false) { function renderUploadButton (onClick) { return el(ButtonOutline - , { onClick - , theme: 'warning' - , style: { width: '100%' } - } + , { onClick, + theme: 'warning', + style: { width: '100%' }, + } , 'Upload New Image' ) } function renderRemoveButton (onClick) { return el(ButtonConfirm - , { onClick - , label: 'Remove Image' - , confirm: 'Remove Image: Are you sure?' - , theme: 'warning' - , style: - { width: '100%' - , marginTop: '0.5rem' - } - } + , { onClick, + label: 'Remove Image', + confirm: 'Remove Image: Are you sure?', + theme: 'warning', + style: + { width: '100%', + marginTop: '0.5rem', + }, + } ) } diff --git a/src/components/image.js b/src/components/image.js index 4901c22..c7abaaa 100644 --- a/src/components/image.js +++ b/src/components/image.js @@ -14,8 +14,8 @@ export default function Image (props, context) { const {width, height} = props if (context && context.imgfloConfig) { const params = - { input: src - , width: getSize(width, height) + { input: src, + width: getSize(width, height), } src = imgflo(context.imgfloConfig, 'passthrough', params) } @@ -24,21 +24,18 @@ export default function Image (props, context) { ) } Image.contextTypes = { - imgfloConfig: React.PropTypes.object + imgfloConfig: React.PropTypes.object, } -// Proxy via imgflo with width multiple of 360 +// Proxy via imgflo with width multiple of 72 function getSize (width, height) { - let size = width || 360 - if (width && (width >= 360)) { - size = 360 + let size = width || 216 + if (width && (width >= 216)) { + size = 216 } - if (width && (width >= 720)) { - size = 720 - } - if (height && height > width && (width >= 360)) { - size = 360 + if (height && height > width && (width >= 144)) { + size = 144 } return size } diff --git a/src/components/media.js b/src/components/media.js deleted file mode 100644 index b59ef59..0000000 --- a/src/components/media.js +++ /dev/null @@ -1,97 +0,0 @@ -import React, {createElement as el} from 'react' -import _ from '../util/lodash' - -import Placeholder from './placeholder' -import AttributionEditor from './attribution-editor' -import WidgetCta from './widget-cta' -import WidgetUnsupported from './widget-unsupported' -import rebassTheme from './rebass-theme' - -const Components = - { placeholder: Placeholder - , cta: WidgetCta - , image: AttributionEditor - , video: AttributionEditor - , article: AttributionEditor - , interactive: AttributionEditor - , quote: AttributionEditor - , unsupported: WidgetUnsupported - } - - -class Media extends React.Component { - constructor (props) { - super(props) - - const {initialBlock} = props - const {id} = initialBlock - this.state = {id, initialBlock} - - const {store} = props - this.boundUpdateBlock = this.updateBlock.bind(this) - this.boundUpdateBlockAll = this.updateBlockAll.bind(this) - store.on('media.update.id', this.boundUpdateBlock) - store.on('media.update', this.boundUpdateBlockAll) - } - componentWillUnmount () { - const {store} = this.props - store.off('media.update.id', this.boundUpdateBlock) - store.off('media.update', this.boundUpdateBlockAll) - } - getChildContext () { - return ( - { imgfloConfig: (this.context.imgfloConfig || this.props.imgfloConfig) - , store: (this.context.store || this.props.store) - , rebass: rebassTheme - } - ) - } - render () { - const {coverPrefs} = this.props - const {initialBlock, id} = this.state - const {type} = initialBlock - let Component = Components[type] || Components.unsupported - return el(Component - , { initialBlock - , id - , coverPrefs - } - ) - } - updateBlock (updateId) { - const {id} = this.state - if (id !== updateId) { - return - } - const {store} = this.props - const initialBlock = store.getBlock(id) - if (!initialBlock) return - this.setState({initialBlock}) - } - updateBlockAll () { - const {store} = this.props - const {id, initialBlock} = this.state - const block = store.getBlock(id) - if (!block) return - // Needed to speed up ed.setContent - // and not render every block every time - if (_.isEqual(initialBlock, block)) return - this.setState({initialBlock: block}) - } -} -Media.contextTypes = - { imgfloConfig: React.PropTypes.object - , store: React.PropTypes.object - } -Media.childContextTypes = - { imgfloConfig: React.PropTypes.object - , rebass: React.PropTypes.object - , store: React.PropTypes.object - } -Media.propTypes = - { initialBlock: React.PropTypes.object.isRequired - , store: React.PropTypes.object.isRequired - , coverPrefs: React.PropTypes.object.isRequired - , imgfloConfig: React.PropTypes.object - } -export default React.createFactory(Media) diff --git a/src/components/modal.js b/src/components/modal.js new file mode 100644 index 0000000..c0a334d --- /dev/null +++ b/src/components/modal.js @@ -0,0 +1,71 @@ +import React, {createElement as el} from 'react' + +import ButtonOutline from 'rebass/dist/ButtonOutline' +import {pseudoFixedStyle} from '../util/browser' +function stopPropagation (event) { event.stopPropagation() } + + +class Modal extends React.Component { + constructor (props) { + super(props) + + this.onKeyDown = (event) => { + if (event.key === 'Enter') { + event.preventDefault() + event.stopPropagation() + props.onClose() + } + } + } + render () { + const {onClose, child} = this.props + + let bgStyle = pseudoFixedStyle() + bgStyle.backgroundColor = 'rgba(128,128,128,0.8)' + bgStyle.zIndex = 4 + bgStyle.overflowY = 'auto' + + return el('div', + { + className: 'Modal-bg', + style: bgStyle, + onClick: onClose, + onKeyDown: this.onKeyDown, + }, + el('div', + { + className: 'Modal-container', + style: { + padding: '1rem', + backgroundColor: 'white', + maxWidth: 720, + margin: '1rem auto 4rem', + border: '1px solid silver', + borderRadius: 2, + }, + onClick: stopPropagation, + }, + el('div', + { + style: { + textAlign: 'right', + marginBottom: '1rem', + }, + }, + el(ButtonOutline, + { + onClick: onClose, + }, + 'Close' + ), + ), + child + ) + ) + } +} +Modal.propTypes = { + onClose: React.PropTypes.func, + child: React.PropTypes.node, +} +export default React.createFactory(Modal) diff --git a/src/components/nav-item-confirm.js b/src/components/nav-item-confirm.js index cc2e58a..5073aff 100644 --- a/src/components/nav-item-confirm.js +++ b/src/components/nav-item-confirm.js @@ -12,23 +12,22 @@ class NavItemConfirm extends React.Component { const {confirm, label, theme, style, onClick} = this.props const {open} = this.state - return el(NavItem - , { children: (open ? confirm : label) - , onClick: (open ? onClick : this.boundOnConfirm) - , theme - , style - } - ) + return el(NavItem, { + children: (open ? confirm : label), + onClick: (open ? onClick : this.boundOnConfirm), + theme, + style, + }) } onConfirm () { this.setState({open: true}) } } NavItemConfirm.propTypes = - { confirm: React.PropTypes.string.isRequired - , label: React.PropTypes.string.isRequired - , theme: React.PropTypes.string - , style: React.PropTypes.object - , onClick: React.PropTypes.func.isRequired - } +{ confirm: React.PropTypes.string.isRequired, + label: React.PropTypes.string.isRequired, + theme: React.PropTypes.string, + style: React.PropTypes.object, + onClick: React.PropTypes.func.isRequired, +} export default React.createFactory(NavItemConfirm) diff --git a/src/components/placeholder.js b/src/components/placeholder.js index d002736..072581f 100644 --- a/src/components/placeholder.js +++ b/src/components/placeholder.js @@ -17,12 +17,12 @@ export default function Placeholder (props, context) { const theme = (failed === true ? 'error' : 'info') return el('div' - , { className: `Placeholder Placeholder-${theme}` - } + , { className: `Placeholder Placeholder-${theme}`, + } , el(Message - , { theme - , style: {marginBottom: 0} - } + , { theme, + style: {marginBottom: 0}, + } , el('span', {className: 'Placeholder-status'}, status) , makePreview(id, store) , el(Space @@ -43,13 +43,13 @@ function makePreview (id, store) { if (!preview) return return el('div' , { style: - { width: 96 - , height: 72 - , display: 'inline-block' - , margin: '0px 16px' - , overflow: 'hidden' - } - } + { width: 96, + height: 72, + display: 'inline-block', + margin: '0px 16px', + overflow: 'hidden', + }, + } , el(Image, {src: preview}) ) } @@ -57,10 +57,10 @@ function makePreview (id, store) { function makeProgress (progress, color) { if (progress == null) return return el(Progress - , { value: progress / 100 - , style: {marginTop: 16} - , color - } + , { value: progress / 100, + style: {marginTop: 16}, + color, + } ) } diff --git a/src/components/rebass-theme.js b/src/components/rebass-theme.js index 04031e2..1168cc8 100644 --- a/src/components/rebass-theme.js +++ b/src/components/rebass-theme.js @@ -5,43 +5,30 @@ export const sans = '-apple-system, ".SFNSText-Regular", "San Francisco", "Robot export const colors = rebassDefaults.colors -export const widgetStyle = - { padding: '1rem 1rem 0' - , background: '#fff' - , border: '1px solid #ddd' - , borderRadius: 2 - , position: 'relative' - } - -export const widgetLeftStyle = - { paddingLeft: '1rem' - , borderLeft: '1px solid #ddd' - , background: '#fff' - } const theme = - { name: 'Ed Theme' - , fontFamily: sans - , colors: rebassDefaults.colors - , Base: - { fontFamily: sans - } - , Button: - { fontFamily: sans - } - , ButtonOutline: - { fontFamily: sans - , boxShadow: 'inset 0 0 0 1px #ddd' - } - , NavItem: - { fontFamily: sans - } - , Panel: - { fontFamily: sans - } - , Message: - { fontFamily: sans - } + { name: 'Ed Theme', + fontFamily: sans, + colors: rebassDefaults.colors, + Base: + { fontFamily: sans, + }, + Button: + { fontFamily: sans, + }, + ButtonOutline: + { fontFamily: sans, + boxShadow: 'inset 0 0 0 1px #ddd', + }, + NavItem: + { fontFamily: sans, + }, + Panel: + { fontFamily: sans, + }, + Message: + { fontFamily: sans, + }, } export default theme diff --git a/src/components/textarea-autosize.js b/src/components/textarea-autosize.js index bdcc45e..b3b5151 100644 --- a/src/components/textarea-autosize.js +++ b/src/components/textarea-autosize.js @@ -4,30 +4,30 @@ import React, {createElement as el} from 'react' import {sans, colors} from './rebass-theme' const containerStyle = - { fontFamily: sans - , fontSize: 12 + { fontFamily: sans, + fontSize: 12, } const labelStyle = {} const labelStyleError = - { color: colors.error + { color: colors.error, } const areaStyle = - { fontFamily: sans - , minHeight: '1.5rem' - , display: 'block' - , width: '100%' - , padding: 0 - , resize: 'none' - , color: 'inherit' - , border: 0 - , borderBottom: '1px dotted rgba(0, 136, 238, .2)' - , borderRadius: 0 - , outline: 'none' - , overflow: 'hidden' - , marginBottom: '0.75rem' + { fontFamily: sans, + minHeight: '1.5rem', + display: 'block', + width: '100%', + padding: 0, + resize: 'none', + color: 'inherit', + border: 0, + borderBottom: '1px dotted rgba(0, 136, 238, .2)', + borderRadius: 0, + outline: 'none', + overflow: 'hidden', + marginBottom: '0.75rem', } @@ -35,21 +35,27 @@ class TextareaAutosize extends React.Component { constructor (props) { super(props) this.boundResize = this.resize.bind(this) + this.boundDebounceResize = this.debounceResize.bind(this) this.boundOnChange = this.onChange.bind(this) this.boundOnKeyDown = this.onKeyDown.bind(this) this.state = - { value: props.defaultValue - , valid: true - } + { value: props.defaultValue, + valid: true, + } } componentDidMount () { - this.boundResize() + this.boundDebounceResize() if (this.props.defaultFocus === true) { this.refs.textarea.focus() } } componentDidUpdate () { - this.boundResize() + this.boundDebounceResize() + } + componentWillUnmount () { + if (this.debounce) { + clearTimeout(this.debounce) + } } componentWillReceiveProps (props) { const {defaultValue, validator} = props @@ -69,28 +75,26 @@ class TextareaAutosize extends React.Component { autoCapitalize = 'none' } - return el('div' - , { className: `TextareaAutosize ${this.props.className}` - , style: containerStyle - } - , el('label' - , { style: (valid ? labelStyle : labelStyleError) - } - , label - , this.renderLink() - , el('textarea' - , { ref: 'textarea' - , style: areaStyle - , value: value || '' - , placeholder - , inputMode - , autoCapitalize - , onChange: this.boundOnChange - , rows: 1 - , onFocus: this.boundResize - , onKeyDown: this.boundOnKeyDown - } - ) + return el('div', + { + className: `TextareaAutosize ${this.props.className}`, + style: containerStyle, + }, + el('label', + {style: (valid ? labelStyle : labelStyleError)}, + label, + this.renderLink(), + el('textarea', { + ref: 'textarea', + style: areaStyle, + value: value || '', + placeholder, + inputMode, + autoCapitalize, + onChange: this.boundOnChange, + rows: 1, + onKeyDown: this.boundOnKeyDown, + }) ) ) } @@ -103,14 +107,14 @@ class TextareaAutosize extends React.Component { return el('span', {}, ' - must be a valid url (http...)') } return el('a' - , { href: value - , target: '_blank' - , rel: 'noreferrer noopener' - , style: - { marginLeft: '0.5rem' - , textDecoration: 'none' - } - } + , { href: value, + target: '_blank', + rel: 'noreferrer noopener', + style: + { marginLeft: '0.5rem', + textDecoration: 'none', + }, + } , 'open' ) } @@ -119,6 +123,12 @@ class TextareaAutosize extends React.Component { textarea.style.height = 'auto' textarea.style.height = textarea.scrollHeight + 'px' } + debounceResize () { + if (this.debounce) { + clearTimeout(this.debounce) + } + this.debounce = setTimeout(this.boundResize, 100) + } onKeyDown (event) { if (this.props.onKeyDown) { this.props.onKeyDown(event) @@ -146,22 +156,22 @@ class TextareaAutosize extends React.Component { this.boundResize() } } -TextareaAutosize.propTypes = - { className: React.PropTypes.string - , defaultValue: React.PropTypes.string - , defaultFocus: React.PropTypes.bool - , label: React.PropTypes.string - , placeholder: React.PropTypes.string - , inputMode: React.PropTypes.string - , autoCapitalize: React.PropTypes.string - , onChange: React.PropTypes.func - , onKeyDown: React.PropTypes.func - , multiline: React.PropTypes.bool - , validator: React.PropTypes.func - } -TextareaAutosize.defaultProps = - { multiline: false - , inputMode: '' - , autoCapitalize: 'sentences' - } +TextareaAutosize.propTypes = { + className: React.PropTypes.string, + defaultValue: React.PropTypes.string, + defaultFocus: React.PropTypes.bool, + label: React.PropTypes.string, + placeholder: React.PropTypes.string, + inputMode: React.PropTypes.string, + autoCapitalize: React.PropTypes.string, + onChange: React.PropTypes.func, + onKeyDown: React.PropTypes.func, + multiline: React.PropTypes.bool, + validator: React.PropTypes.func, +} +TextareaAutosize.defaultProps = { + multiline: false, + inputMode: '', + autoCapitalize: 'sentences', +} export default React.createFactory(TextareaAutosize) diff --git a/src/components/widget-cta-view.js b/src/components/widget-cta-view.js new file mode 100644 index 0000000..1709327 --- /dev/null +++ b/src/components/widget-cta-view.js @@ -0,0 +1,55 @@ +// If we need cover support for cta widget later, +// don't add it here. + +import React, {createElement as el} from 'react' +import Button from 'rebass/dist/Button' +import ButtonOutline from 'rebass/dist/ButtonOutline' + + +class WidgetCtaView extends React.Component { + render () { + const {initialBlock} = this.props + const {label, url} = initialBlock + + return el('div', + { + className: 'WidgetView WidgetView-cta', + style: { + border: '1px solid silver', + borderRadius: 2, + padding: '1rem', + backgroundColor: 'white', + }, + }, + el(Button, + { + style: { + fontSize: '200%', + padding: '1.5rem', + marginBottom: '1rem', + display: 'block', + }, + title: url, + big: true, + onClick: this.props.triggerEdit, + }, + label || 'label', + ), + el(ButtonOutline, + { + onClick: this.props.triggerEdit, + }, + 'Edit' + ) + ) + } +} +WidgetCtaView.propTypes = +{ initialBlock: React.PropTypes.object.isRequired, + id: React.PropTypes.string.isRequired, + triggerEdit: React.PropTypes.func, +} +WidgetCtaView.contextTypes = + { store: React.PropTypes.object } + +export default React.createFactory(WidgetCtaView) diff --git a/src/components/widget-cta.js b/src/components/widget-cta.js index 526fc3a..f7582df 100644 --- a/src/components/widget-cta.js +++ b/src/components/widget-cta.js @@ -43,8 +43,8 @@ class WidgetCta extends React.Component { this.toggleImport = () => { const {showImport} = this.state this.setState( - { showImport: !showImport - , importStatus: '' + { showImport: !showImport, + importStatus: '', } ) } @@ -68,38 +68,38 @@ class WidgetCta extends React.Component { const {label, url, canFrame} = this.state return el('div' - , { className: 'WidgetCta' - } + , { className: 'WidgetCta', + } , el('div' - , { className: 'WidgetCta-metadata' - , style: widgetLeftStyle - } + , { className: 'WidgetCta-metadata', + style: widgetLeftStyle, + } , el(TextareaAutosize - , { label: 'Label' - , key: 'label' - , placeholder: 'Sign up now!' - , defaultValue: label - , onChange: this.changeLabel - , style: {width: '100%'} - } + , { label: 'Label', + key: 'label', + placeholder: 'Sign up now!', + defaultValue: label, + onChange: this.changeLabel, + style: {width: '100%'}, + } ) , el(TextareaAutosize - , { label: 'Link' - , key: 'url' - , placeholder: 'https://...' - , defaultValue: url - , onChange: this.changeUrl - , validator: isUrlOrBlank - , style: {width: '100%'} - } + , { label: 'Link', + key: 'url', + placeholder: 'https://...', + defaultValue: url, + onChange: this.changeUrl, + validator: isUrlOrBlank, + style: {width: '100%'}, + } ) , el(Checkbox - , { key: 'canFrame' - , label: 'Link can open in frame' - , name: 'canFrame' - , checked: (canFrame === true) - , onChange: this.changeModal - } + , { key: 'canFrame', + label: 'Link can open in frame', + name: 'canFrame', + checked: (canFrame === true), + onChange: this.changeModal, + } ) , this.renderImport() ) @@ -128,11 +128,11 @@ class WidgetCta extends React.Component { return el('form' , { onSubmit: this.boundImportHTML } , el(TextareaAutosize - , { label: 'HTML' - , defaultValue: '' - , defaultFocus: true - , placeholder: '