├── .babelrc
├── .eslintrc
├── .gitignore
├── .npmignore
├── .travis.yml
├── CHANGES.md
├── README.md
├── demo
├── demo.js
├── fixture.js
└── gremlin-prosemirror.js
├── gh-pages.sh
├── index.html
├── jsconfig.json
├── karma.conf.js
├── package.json
├── src
├── components
│ ├── add-cover.js
│ ├── add-fold.js
│ ├── app.css
│ ├── app.js
│ ├── attribution-editor.css
│ ├── attribution-editor.js
│ ├── attribution-view.js
│ ├── button-confirm.js
│ ├── credit-add.js
│ ├── credit-editor.js
│ ├── dropdown-group.js
│ ├── editable-feature-flags.css
│ ├── editable-menu.css
│ ├── editable.css
│ ├── editable.js
│ ├── icons.js
│ ├── image-editor.js
│ ├── image.css
│ ├── image.js
│ ├── modal.css
│ ├── modal.js
│ ├── nav-item-confirm.js
│ ├── placeholder.js
│ ├── rebass-theme.js
│ ├── textarea-autosize.css
│ ├── textarea-autosize.js
│ ├── widget-cta-view.js
│ ├── widget-cta.js
│ ├── widget-edit.js
│ ├── widget-iframe.js
│ ├── widget-unsupported.js
│ ├── widget-view.js
│ └── widget.js
├── convert
│ ├── determine-fold.js
│ ├── doc-to-grid.js
│ ├── grid-to-doc.js
│ ├── space-content.js
│ └── types.js
├── ed.js
├── inputrules
│ ├── ed-input-rules.js
│ ├── ed-keymap.js
│ ├── input-code.js
│ └── ios-double-space.js
├── menu
│ ├── ed-menu.js
│ ├── menu-image.js
│ ├── menu-link.js
│ ├── menu-media.js
│ └── menu-prompt.js
├── plugins
│ ├── commands-interface.js
│ ├── content-hints.js
│ ├── fixed-menu-hack.js
│ ├── iframe-info.js
│ ├── placeholder.js
│ ├── share-url.js
│ └── store-ref.js
├── schema
│ ├── block-meta.js
│ ├── ed-schema.js
│ ├── media.css
│ └── media.js
├── store
│ └── ed-store.js
└── util
│ ├── browser.js
│ ├── drop.js
│ ├── encode.js
│ ├── lodash.js
│ ├── pm.js
│ └── url.js
├── targets
├── web-demo.html
└── web.html
├── test
├── .eslintrc
├── components
│ ├── image.js
│ └── widget-cta.js
├── convert
│ ├── determine-fold.js
│ ├── doc-to-grid.js
│ └── grid-to-doc.js
├── ed.js
├── index.html
├── index.js
├── plugins
│ ├── placeholder.js
│ ├── widget-flagged.js
│ ├── widget-iframe.js
│ └── widget.js
└── schema
│ └── block-meta.js
└── webpack.config.js
/.babelrc:
--------------------------------------------------------------------------------
1 | {
2 | "presets": ["es2015"]
3 | }
4 |
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "parser": "babel-eslint",
3 | "ecmaFeatures": { "modules": true },
4 | "env": {
5 | "browser": true,
6 | "node": true,
7 | "es6": true
8 | },
9 | "rules": {
10 | "no-multiple-empty-lines": [2, {"max": 2, "maxEOF": 1}],
11 | "comma-dangle": [2, "always-multiline"],
12 | "no-console": 2,
13 | "camelcase": 0
14 | },
15 | "plugins": ["react"],
16 | "extends": ["standard", "plugin:react/recommended"]
17 | }
18 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | logs
2 | *.log*
3 | node_modules
4 | dist
5 | .DS_Store
6 |
7 | *.orig
8 |
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | logs
2 | *.log*
3 | node_modules
4 | !dist/node_modules/
5 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: node_js
2 | node_js:
3 | - 7.4
4 | before_install:
5 | - export CHROME_BIN=chromium-browser
6 | - export DISPLAY=:99.0
7 | - sh -e /etc/init.d/xvfb start
8 | script: npm test
9 | before_deploy: npm run build
10 | deploy:
11 | provider: npm
12 | skip_cleanup: true
13 | email: forrest@sembiki.com
14 | api_key:
15 | secure: KwIWPYT125xqr8CWMuMadU5gkh183Klp2lRuyH73q5h0nVH+OzwcxOVWirdCKX6UA3d2cT4fs28OaH58apvlDkqrDyE8j3YENFi7W5fyWr1lQlgNqD4pEIrBXQF1OP9A0yUDE9pZebQ+PiMvQ665qkl2GEKFaYjI/57vDO0jKaDIiGihrAQR1hRCIWGcWxTigq6vagmcYHVS/8udzqqxce6Cx9nJhHzHTc2ARPND0c/h6jySzZIzZCG781j0v0TWwzew20ibIzWhRrqq0l0lDFZD0usRXTzH8Bc6mDPX13jKvXJfm138iX5K/rRoL7lnBi9KTXMANNh/tR++tK4LJDr2226eapOFg6z1mhMAwhSXiDSf/NSbJtQvSKT9vCri4cDglKBqgE9H2PBrgH0FhPYmSzTcD59LQqq3yt5Sa1xKRmjidrmltbK+yjfPShr3huS2Vhj4aFGyIVxVw+YSFwUhTIg9/e6S42REkxS7zIvbNuRHBtlUSpB5ZdqaM7qMcavU8gsm38JRSqxgZR0XcCo5rIrOfG+A80B3xsGxyGf0+jjVv5WUAITDmcPw0GTe5G0H/yOf94FG1YCNLjel4NZUn3eBTghrYtH3WM95D6ua7kSowXs7OTWHLCAWgKk79lKekVhznYHtMyG82Rk9VJfJlav6/l1d85/n3enqymM=
16 | on:
17 | tags: true
18 | repo: the-grid/ed
19 | after_script: bash ./gh-pages.sh
20 | env:
21 | global:
22 | - GH_REF: github.com/the-grid/ed.git
23 | - secure: Pz7NfvRl0OWA1Jmwzjdzt5Ug8TCVw8x44JbmniBhiMooh7/hFAVN9b6RvCWPx/7eqf+uQXiyaFfG9UcXQNwLo9cEkvwWlEyhSW/k7sMMyddl3rOK8zf9PfmoNU9K6Q0t3YPvYOAXTe2Ma43d/4OEJY1JgbtlooDldjhHqQqIiFuPxUhk7rfLdtpfI4mijVt7wjSmY7pPg2jtddRfNFExAk0p4KZOSSQlrn6R9tdIMeSALRfo7oulKheGq0V5CLTaX5OlRojeI5UIrGPJpIk4QbYaN19u6opGetlLzQ1qqyiSs8pDL8XXV55GPyPjB8m4iEJXnAjIDgqc5LTtDdhRpmlti+23xX5GKIto9/uj4TU/knouuAs54lcXgwuyBT+G8MLDsED3Xzpxmw8JmvpKiiilRwn58bT1SZ2BHkJdvB/qnzQQ7G4726OuAo0rsMR3S8eRk3ysnf4Vva+rHtA26zuYeIZlP0GAkZCjs45r56fJVS0hvLqPANBrhZETdxBKOyRo64KAft0ql3oQUpdXmuwY4IbhKDKtYDjlQe3J262wLBnSo878RFWoB6YTvVxojhOv98nc2yuTcUjX8n72fDtnceSFkj4x4pMRmg6a+eAwN0Qq28BwlMm+H1913AGQ3wsVNakwQ5CSWU8C5LuacUd0HOV3q2PfhcroVnWNmhc=
24 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | `npm start`
2 |
3 | # ed
4 |
5 | [](https://travis-ci.org/the-grid/ed)
6 |
7 | Using [ProseMirror](http://prosemirror.net/) with data from [the Grid API](http://developer.thegrid.io/)
8 |
9 | Demo: [the-grid.github.io/ed/](https://the-grid.github.io/ed/), [with fixture](https://the-grid.github.io/ed/#fixture)
10 |
11 | The demo shows translating from ProseMirror to the the Grid API JSON and back.
12 |
13 | ## purpose
14 |
15 | ProseMirror provides a high-level schema-based interface for interacting with `contenteditable`, taking care of that pain. Ed is focused on:
16 |
17 | * Schema to translate between the Grid API data and ProseMirror doc type
18 | * Coordinating widgets (block editors specialized by type) ([example](https://github.com/the-grid/ced))
19 |
20 | # use
21 |
22 | ## Using as a React ⚛ component
23 |
24 | Ed exposes [a React component](./src/components/app.js) by default.
25 |
26 | ``` jsx
27 | import Ed from '@the-grid/ed'
28 |
29 | export default class PostEditor extends React.Component {
30 | render() {
31 | return (
32 |
33 | )
34 | }
35 | }
36 | ```
37 |
38 | ## Using as a stand-alone library in iframe or similar
39 |
40 | Including `dist/build.js` in your page exposes `window.TheGridEd`
41 |
42 | ``` html
43 |
44 | ```
45 |
46 | There are `{mountApp, unmountApp}` helper methods
47 | available to use like this:
48 |
49 | ``` js
50 | var container = document.querySelector('#ed')
51 | window.TheGridEd.mountApp(container, {
52 | // REQUIRED -- Content array from post
53 | initialContent: [],
54 | // OPTIONAL (default true) enable or disable the default menu
55 | menuBar: true,
56 | // REQUIRED -- Hit on every change
57 | onChange: function () {
58 | /* App can show "unsaved changes" in UI */
59 | },
60 | // REQUIRED
61 | onShareFile: function (index) {
62 | /* App triggers native file picker */
63 | /* App calls ed.insertPlaceholders(index, count) and gets array of ids back */
64 | /* App uploads files and sets status on placeholder blocks with ed.updateProgress */
65 | /* On upload / measurement finishing, app replaces placeholder blocks with ed.setContent */
66 | },
67 | // REQUIRED
68 | onRequestCoverUpload: function (id) {
69 | /* Similar to onShareFile, but hit with block id instead of index */
70 | /* App uploads files and sets status on blocks with ed.updateProgress */
71 | /* Once upload is complete, app hits ed.setCoverSrc */
72 | },
73 | // REQUIRED
74 | onShareUrl: function ({block, url}) {
75 | /* Ed made the placeholder with block id */
76 | /* App shares url with given block id */
77 | /* App updates status on placeholder blocks with ed.updateProgress */
78 | /* On share / measurement finishing, app replaces placeholder blocks with ed.setContent */
79 | },
80 | // REQUIRED
81 | onPlaceholderCancel: function (id) {
82 | /* Ed removed the placeholder if you call ed.getContent() now */
83 | /* App should cancel the share or upload */
84 | },
85 | // OPTIONAL
86 | onRequestLink: function (value) {
87 | /*
88 | If defined, Ed will _not_ show prompt for link
89 | If selection is url-like, value will be the selected string
90 | App can then call `ed.execCommand('link:toggle', {href, title})`
91 | Note: If that is called while command 'link:toggle' is 'active', it will remove the link, not replace it
92 | */
93 | },
94 | // OPTIONAL
95 | onDropFiles: function (index, files) {
96 | /* App calls ed.insertPlaceholders(index, files.length) and gets array of ids back */
97 | /* App uploads files and sets status on placeholder blocks with ed.updateProgress */
98 | /* On upload / measurement finishing, app replaces placeholder blocks with ed.setContent */
99 | },
100 | // OPTIONAL
101 | onDropFileOnBlock: function (id, file) {
102 | /* App uploads files and sets status on block with ed.updateProgress */
103 | /* Once upload is complete, app hits ed.setCoverSrc */
104 | },
105 | // OPTIONAL
106 | onMount: function (mounted) {
107 | /* Called once PM and widgets are mounted */
108 | window.ed = mounted
109 | },
110 | // OPTIONAL
111 | onCommandsChanged: function (commands) {
112 | /* Object with commandName keys and one of inactive, active, disabled */
113 | },
114 | // OPTIONAL -- imgflo image proxy config
115 | imgfloConfig: {
116 | server: 'https://imgflo.herokuapp.com/',
117 | key: 'key',
118 | secret: 'secret'
119 | },
120 | // OPTIONAL -- where iframe widgets live relative to app (or absolute)
121 | widgetPath: './node_modules/',
122 | // OPTIONAL -- site-wide settings to allow cover filter, crop, overlay; default true
123 | coverPrefs: {
124 | filter: false,
125 | crop: true,
126 | overlay: true
127 | },
128 | // OPTIONAL -- site or user flags to reduce functionality
129 | featureFlags: {
130 | edCta: false,
131 | edEmbed: false
132 | }
133 | })
134 |
135 | // Returns array of inserted placeholder ids
136 | ed.insertPlaceholders(index, count)
137 |
138 | // Update placeholder metadata
139 | // {status (string), progress (number 0-100), failed (boolean)}
140 | // metadata argument with {progress: null} will remove the progress bar
141 | ed.updateProgress(id, metadata)
142 |
143 | // Once block cover upload completes
144 | // `cover` is object with {src, width, height}
145 | ed.setCover(id, cover)
146 |
147 | // For placeholder or media block with uploading cover
148 | // `src` should be blob: or data: url of a
149 | // sized preview of the local image
150 | ed.setCoverPreview(id, src)
151 |
152 | // Returns content array
153 | // Expensive, so best to debounce and not call this on every change
154 | // Above the fold block is index 0, and starred
155 | ed.getContent()
156 |
157 | // Only inserts/updates placeholder blocks and converts placeholder blocks to media
158 | ed.setContent(contentArray)
159 |
160 | // Returns true if command applies successfully with current selection
161 | ed.execCommand(commandName)
162 | ```
163 |
164 | Demo: [./demo/demo.js](./demo/demo.js)
165 |
166 | ## commands
167 |
168 | With `onCommandsChanged` prop, app will get an object containing these commandName keys.
169 | Values will be one of these strings: `inactive`, `active`, `disabled`, `flagged`.
170 |
171 | Apps can apply formatting / editing commands with `ed.execCommand(commandName)`
172 |
173 | Special case: `ed.execCommand('link:toggle', {href, title})` (title optional) to set link of current selection.
174 |
175 | Supported `commandName` keys:
176 |
177 | ```
178 | strong:toggle
179 | em:toggle
180 | link:toggle
181 | paragraph:make
182 | heading:make1
183 | heading:make2
184 | heading:make3
185 | bullet_list:wrap
186 | ordered_list:wrap
187 | horizontal_rule:insert
188 | lift
189 | undo
190 | redo
191 | ed_upload_image
192 | ed_add_code
193 | ed_add_location
194 | ed_add_userhtml
195 | ed_add_cta
196 | ed_add_quote
197 | ```
198 |
199 | # dev
200 |
201 | ## server
202 |
203 | `npm start` and open [http://localhost:8080/](http://localhost:8080/)
204 |
205 | In development mode, webpack builds and serves the targets in memory from /webpack/
206 |
207 | Changes will trigger a browser refresh.
208 |
209 | ## plugins
210 |
211 | [Plugins](./src/plugins) are ES2015 classes with 2 required methods:
212 |
213 | * `constructor (ed) {}` gets a reference to the main `ed`, where you can
214 | * listen to PM events: `ed.pm.on('draw', ...)`
215 | * and set up UI: `ed.pluginContainer.appendChild(...)`
216 | * `teardown () {}` where all listeners and UI should be removed
217 |
218 | ## widgets
219 |
220 | Widgets are mini-editors built to edit specific media types
221 |
222 | ### iframe
223 |
224 | Run in iframe and communicate via postMessage
225 |
226 | Example: [ced - widget for code editing](https://github.com/the-grid/ced)
227 |
228 | ### native
229 |
230 | Example: WIP
231 |
232 | ## styling
233 |
234 | 1. Primary: [Rebass](http://jxnblk.com/rebass/) defaults and [rebass-theme](./src/components/rebass-theme.js) for global overrides
235 | 2. Secondary: inlined JS `style` objects ([example](./src/components/textarea-autosize.js))
236 | 3. Deprecating: `require('./component-name.css')` style includes, but needed for some responsive hacks and ProseMirror overrides
237 |
238 | ## code style
239 |
240 | Feross [standard](https://github.com/feross/standard#rules) checked by ESLint with `npm test` or `npm run lint`
241 |
242 | * no unneeded semicolons
243 | * no trailing spaces
244 | * single quotes
245 |
246 | To automatically fix easy stuff like trailing whitespace: `npm run lintfix`
247 |
248 | ## test
249 |
250 | `npm test`
251 |
252 | [Karma is set up](./karma.conf.js) to run tests in local Chrome and Firefox.
253 |
254 | Tests will also run in mobile platforms via [BrowserStack](https://www.browserstack.com/), if you have these environment variables set up:
255 |
256 | ```
257 | BROWSERSTACK_USERNAME
258 | BROWSERSTACK_ACCESSKEY
259 | ```
260 |
261 | ## build
262 |
263 | `npm run build`
264 |
265 | Outputs minified dist/ed.js and copies widgets defined in [package.json](./package.json).
266 |
267 | ## deploying
268 |
269 | `npm version patch` - style tweaks, hot bug fixes
270 |
271 | `npm version minor` - adding features, backwards-compatible changes
272 |
273 | `npm version major` - removing features, non-backwards-compatible changes
274 |
275 | These shortcuts will run tests, tag, change package version, and push changes and tags to GH.
276 |
277 | Travis will then publish new tags to [npm](https://www.npmjs.com/package/@the-grid/ed)
278 | and build the demo to publish to [gh-pages](https://the-grid.github.io/ed/).
--------------------------------------------------------------------------------
/demo/demo.js:
--------------------------------------------------------------------------------
1 | /* eslint no-console: 0 */
2 |
3 | import {mountApp, unmountApp} from '../src/ed'
4 | import fixture from './fixture'
5 | import Gremlins from 'gremlins.js/src/main'
6 | import gremlinProsemirror from './gremlin-prosemirror'
7 |
8 | let ed
9 | const fixtureContent = fixture.content
10 |
11 | let apiJSON = document.querySelector('#debug-api')
12 |
13 | // ProseMirror setup
14 | function setup (options) {
15 | const container = document.querySelector('#app')
16 |
17 | if (ed) {
18 | unmountApp(container)
19 | ed = null
20 | }
21 | if (options.initialContent) {
22 | apiJSON.value = JSON.stringify(options.initialContent, null, 2)
23 | }
24 | const props =
25 | { initialContent: (options.initialContent || []),
26 | onChange: () => { console.log('onChange') },
27 | onMount: function (mounted) {
28 | ed = mounted
29 | console.log(ed)
30 | window.ed = ed
31 | },
32 | onShareFile: onShareFileDemo,
33 | onShareUrl: onShareUrlDemo,
34 | onRequestCoverUpload: onRequestCoverUploadDemo,
35 | onPlaceholderCancel: onPlaceholderCancelDemo,
36 | onCommandsChanged: function (commands) {
37 | // console.log(commands)
38 | },
39 | onDropFiles: onDropFilesDemo,
40 | onDropFileOnBlock: onDropFileOnBlockDemo,
41 | // onRequestLink: function (value) {
42 | // console.log('onRequestLink', value)
43 | // },
44 | imgfloConfig: null,
45 | widgetPath: './node_modules/',
46 | coverPrefs: { filter: false },
47 | menuBar: true,
48 | featureFlags: {
49 | edCta: false,
50 | edEmbed: false,
51 | },
52 | }
53 | mountApp(container, props)
54 |
55 | // Only for fixture demo
56 | initializePlaceholderMetadata(props.initialContent)
57 | }
58 | const initialContent = (window.location.hash === '#fixture' ? fixtureContent : [])
59 | setup({initialContent})
60 |
61 | // onShareFile upload demo
62 | let input
63 | function onShareFileDemo (index) {
64 | console.log('onShareFile: app triggers native picker', index)
65 |
66 | // Remove old input from DOM
67 | if (input && input.parentNode) {
68 | input.parentNode.removeChild(input)
69 | }
70 | input = document.createElement('input')
71 | input.type = 'file'
72 | input.multiple = true
73 | input.accept = 'image/*'
74 | input.onchange = makeInputOnChange(index)
75 | input.style.display = 'none'
76 | document.body.appendChild(input)
77 | input.click()
78 | }
79 | function makeInputOnChange (index) {
80 | return function (event) {
81 | event.stopPropagation()
82 | const input = event.target
83 | const files = input.files
84 | if (!files || !files.length) return
85 | filesUploadSim(index, files)
86 | }
87 | }
88 |
89 | function filesUploadSim (index, files) {
90 | // Make placeholder blocks
91 | let names = []
92 | for (let i = 0, len = files.length; i < len; i++) {
93 | const file = files[i]
94 | const name = file.name.substr(0, file.name.indexOf('.'))
95 | names.push(name)
96 | }
97 |
98 | // Insert placeholder blocks into content
99 | console.log(`app calls ed.insertPlaceholders(${index}, ${files.length}) and gets array of ids`)
100 | const ids = ed.insertPlaceholders(index, files.length)
101 |
102 | for (let i = 0, len = files.length; i < len; i++) {
103 | const file = files[i]
104 | const url = URL.createObjectURL(file)
105 | const id = ids[i]
106 | ed.setCoverPreview(id, url)
107 | }
108 |
109 | console.log('app uploads files now and calls `ed.updateProgress(id, meta)` with updates')
110 |
111 | simulateProgress(
112 | function (progress) {
113 | ids.forEach(function (id, index) {
114 | let status = `Uploading ${names[index]}`
115 | ed.updateProgress(id, {status, progress})
116 | })
117 | },
118 | function () {
119 | const updatedBlocks = ids.map(function (id, index) {
120 | ed.updateProgress(id, {progress: null})
121 | return (
122 | { id,
123 | type: 'image',
124 | metadata: {title: names[index]},
125 | }
126 | )
127 | })
128 | ed.setContent(updatedBlocks)
129 | }
130 | )
131 | }
132 |
133 | // File picker debug
134 | document.getElementById('upload').onclick = function () {
135 | ed.execCommand('ed_upload_image')
136 | }
137 |
138 | // onShareUrl demo
139 | function onShareUrlDemo (share) {
140 | const {block, url} = share
141 | console.log('onShareUrl: app shares url now and calls ed.updateProgress() with updates', share)
142 |
143 | simulateProgress(
144 | function (progress) {
145 | const status = `Sharing ${url}`
146 | ed.updateProgress(block, {status, progress})
147 | },
148 | function () {
149 | console.log('Share: mount block')
150 | ed.setContent([
151 | { id: block,
152 | type: 'article',
153 | metadata:
154 | { title: 'Shared article title',
155 | description: `Simulated share from ${url}`,
156 | },
157 | },
158 | ])
159 | window.setTimeout(function () {
160 | console.log('Share: mount block + cover')
161 | ed.setContent([
162 | { id: block,
163 | type: 'article',
164 | metadata:
165 | { title: 'Shared article title + cover',
166 | description: `Simulated share from ${url}`,
167 | },
168 | cover:
169 | { src: 'http://meemoo.org/images/meemoo-illo-by-jyri-pieniniemi-400.png',
170 | width: 400,
171 | height: 474,
172 | },
173 | },
174 | ])
175 | }, 1000)
176 | }
177 | )
178 | }
179 |
180 | function simulateProgress (progress, complete) {
181 | let percent = 0
182 | let animate = function () {
183 | percent += 0.5
184 | if (percent < 100) {
185 | // Loop animation
186 | requestAnimationFrame(animate)
187 | // Update placeholder status
188 | progress(percent)
189 | } else {
190 | // Change placeholder to article block
191 | complete()
192 | }
193 | }
194 | animate()
195 | }
196 |
197 | // Debug buttons
198 |
199 | // Toggle debug
200 | let showDebug = false
201 | let debug = document.getElementById('debug')
202 | let toggleDebug = document.getElementById('debug-toggle')
203 | toggleDebug.onclick = () => {
204 | if (showDebug) {
205 | debug.style.display = 'none'
206 | } else {
207 | debug.style.display = 'block'
208 | }
209 | showDebug = !showDebug
210 | }
211 |
212 | // Hydrate
213 | function APIToEditor () {
214 | let json
215 | try {
216 | json = JSON.parse(apiJSON.value)
217 | } catch (e) {
218 | return console.warn('bad json')
219 | }
220 | ed.setContent(json)
221 | }
222 | document.querySelector('#hydrate').onclick = APIToEditor
223 |
224 | // Dehydrate
225 | function EditorToAPI () {
226 | apiJSON.value = JSON.stringify(ed.getContent(), null, 2)
227 | }
228 | document.querySelector('#dehydrate').onclick = EditorToAPI
229 |
230 | // Simulate changes from API
231 | const bangOnContent = document.querySelector('#sim')
232 | let timeout
233 | let simulateUpdates = function () {
234 | // Loop
235 | timeout = setTimeout(simulateUpdates, 1000)
236 |
237 | let content = ed.getContent()
238 | // console.log(content[6].url)
239 | ed.setContent(content)
240 | }
241 | let toggleUpdates = function () {
242 | if (timeout) {
243 | clearTimeout(timeout)
244 | timeout = null
245 | bangOnContent.textContent = 'Sim changes from API ▶'
246 | } else {
247 | timeout = setTimeout(simulateUpdates, 500)
248 | bangOnContent.textContent = 'Sim changes from API ◼︎'
249 | }
250 | }
251 | bangOnContent.onclick = toggleUpdates
252 | bangOnContent.click()
253 |
254 | // Load full post
255 | function loadFixture () {
256 | window.location.hash = '#fixture'
257 | setup({initialContent: fixtureContent})
258 | }
259 | document.querySelector('#fixture').onclick = loadFixture
260 |
261 | function initializePlaceholderMetadata (content) {
262 | for (let i = 0, len = content.length; i < len; i++) {
263 | const block = content[i]
264 | if (!block || !block.id || !block.metadata) {
265 | continue
266 | }
267 |
268 | const {progress, status, failed} = block.metadata
269 | if (progress === undefined && status === undefined && failed === undefined) {
270 | continue
271 | }
272 | ed.updateProgress(block.id, {progress, status, failed})
273 |
274 | const {cover} = block
275 | if (cover && cover.src) {
276 | ed.setCoverPreview(block.id, cover.src)
277 | }
278 | }
279 | }
280 |
281 | function onPlaceholderCancelDemo (id) {
282 | console.log(`App would cancel the share or upload with id: ${id}`)
283 | }
284 |
285 | // Cover change
286 |
287 | function onRequestCoverUploadDemo (id) {
288 | console.log('onRequestCoverUpload: app triggers native picker', id)
289 |
290 | // Remove old input from DOM
291 | if (input && input.parentNode) {
292 | input.parentNode.removeChild(input)
293 | }
294 | input = document.createElement('input')
295 | input.type = 'file'
296 | input.multiple = false
297 | input.accept = 'image/*'
298 | input.onchange = makeRequestCoverUploadInputOnChange(id)
299 | input.style.display = 'none'
300 | document.body.appendChild(input)
301 | input.click()
302 | }
303 |
304 | function makeRequestCoverUploadInputOnChange (id) {
305 | return function (event) {
306 | event.stopPropagation()
307 |
308 | const file = input.files[0]
309 | const src = URL.createObjectURL(file)
310 | ed.setCoverPreview(id, src)
311 | ed.updateProgress(id, {failed: false})
312 |
313 | console.log('app uploads files now and calls ed.updateProgress with updates')
314 |
315 | simulateProgress(
316 | function (progress) {
317 | let status = 'Uploading...'
318 | ed.updateProgress(id, {status, progress})
319 | },
320 | function () {
321 | ed.updateProgress(id, {progress: null})
322 | // Apps should have dimensions from API
323 | // and should not need to load the image client-side
324 | const img = new Image()
325 | img.onload = function () {
326 | const {width, height} = img
327 | ed.setCover(id, {src, width, height})
328 | }
329 | img.src = src
330 | }
331 | )
332 | }
333 | }
334 |
335 | function onDropFilesDemo (index, files) {
336 | console.log('onDropFiles: files dropped')
337 | filesUploadSim(index, files)
338 | }
339 |
340 | function onDropFileOnBlockDemo (id, file) {
341 | console.log('onDropFileOnBlock: file dropped')
342 |
343 | const src = URL.createObjectURL(file)
344 | ed.setCoverPreview(id, src)
345 |
346 | console.log('app uploads files now and calls ed.updateProgress with updates')
347 |
348 | simulateProgress(
349 | function (progress) {
350 | let status = 'Uploading...'
351 | ed.updateProgress(id, {status, progress})
352 | },
353 | function () {
354 | // Apps should have dimensions from API
355 | // and should not need to load the image client-side
356 | const img = new Image()
357 | img.onload = function () {
358 | const {width, height} = img
359 | ed.setCover(id, {src, width, height})
360 | }
361 | img.src = src
362 | }
363 | )
364 | }
365 |
366 | /* Fuzz / Monkey / Gremlin.js testing */
367 |
368 | const fuzz = document.getElementById('fuzz')
369 | let fuzzer = null
370 | fuzz.addEventListener('click', function () {
371 | if (fuzzer) {
372 | fuzzer.stop()
373 | fuzzer = null
374 | } else {
375 | fuzzer = Gremlins
376 | .createHorde()
377 | // .allGremlins()
378 | .gremlin(gremlinProsemirror)
379 | fuzzer.unleash()
380 | }
381 | })
382 |
--------------------------------------------------------------------------------
/demo/gremlin-prosemirror.js:
--------------------------------------------------------------------------------
1 | import configurable from 'gremlins.js/src/utils/configurable'
2 |
3 | function randomChar () {
4 | return String.fromCharCode(0x00FF * Math.random())
5 | }
6 |
7 | function randomFromArray (arr) {
8 | return arr[Math.floor(Math.random() * arr.length)]
9 | }
10 |
11 |
12 | const config =
13 | { logger: null,
14 | randomizer: null,
15 | }
16 |
17 | function pmSelecter (pm) {
18 | const max = pm.doc.nodeSize
19 | const anchor = config.randomizer.natural({max})
20 | const head = config.randomizer.natural({max})
21 | try {
22 | pm.setTextSelection(anchor, head)
23 | } catch (e) {}
24 | }
25 |
26 | function pmSelecterCollapsed (pm) {
27 | const max = pm.doc.nodeSize
28 | const anchor = config.randomizer.natural({max})
29 | try {
30 | pm.setTextSelection(anchor)
31 | } catch (e) {}
32 | }
33 |
34 | function pmSelecterNode (pm) {
35 | const max = pm.doc.nodeSize
36 | const pos = config.randomizer.natural({max})
37 | try {
38 | pm.setNodeSelection(pos)
39 | } catch (e) {}
40 | }
41 |
42 | function pmFocuser (pm) {
43 | pm.focus()
44 | }
45 |
46 | function pmTyper (pm) {
47 | pm.tr.typeText(randomChar()).apply()
48 | }
49 |
50 | function pmSplitter (pm) {
51 | const max = pm.doc.nodeSize
52 | const pos = config.randomizer.natural({max})
53 | try {
54 | pm.tr.split(pos).apply()
55 | } catch (e) {}
56 | }
57 |
58 | function pmFormatter (pm) {
59 | const icons = document.body.querySelectorAll('.ProseMirror-icon')
60 | if (!icons.length) return
61 |
62 | const icon = randomFromArray(icons)
63 | const event = new MouseEvent('mousedown')
64 | icon.dispatchEvent(event)
65 | }
66 |
67 | const subgremlins =
68 | [ pmSelecter,
69 | pmSelecterCollapsed,
70 | pmSelecterNode,
71 | pmTyper,
72 | pmFocuser,
73 | pmSplitter,
74 | pmFormatter,
75 | ]
76 |
77 | function pmGremlin () {
78 | const {pm} = window.ed
79 | const gremlin = randomFromArray(subgremlins)
80 | gremlin(pm)
81 | }
82 |
83 | configurable(pmGremlin, config)
84 |
85 | export default pmGremlin
86 |
--------------------------------------------------------------------------------
/gh-pages.sh:
--------------------------------------------------------------------------------
1 | if [ "$TRAVIS_TAG" = "" ]
2 | then
3 | echo "Not a tag, not publishing"
4 | exit 0
5 | else
6 | echo "==> Building and publishing demo tag $TRAVIS_TAG <=="
7 | fi
8 |
9 | #!/bin/bash
10 | set -e # exit with nonzero exit code if anything fails
11 |
12 | # run our compile script, discussed above
13 | npm run builddemo
14 |
15 | # move demo stuff around
16 | mkdir dist/webpack
17 | mv dist/demo.js dist/webpack/demo.js
18 | mv dist/demo.map dist/webpack/demo.map
19 |
20 | # no need for jekyll in this demo (jekyll 3.3+ blocks node_modules)
21 | touch dist/.nojekyll
22 |
23 | # go to the build directory and create a *new* Git repo
24 | cd dist
25 | git init
26 |
27 | # inside this git repo we'll pretend to be a new user
28 | git config user.name "Travis CI"
29 | git config user.email "f.bot@forresto.com"
30 |
31 | # The first and only commit to this new Git repo contains all the
32 | # files present with the commit message "Deploy to GitHub Pages".
33 | git add .
34 | git commit -m "demo $TRAVIS_TAG to gh-pages"
35 |
36 | # Force push from the current repo's master branch to the remote
37 | # repo's gh-pages branch. (All previous history on the gh-pages branch
38 | # will be lost, since we are overwriting it.) We redirect any output to
39 | # /dev/null to hide any sensitive credential data that might otherwise be exposed.
40 | git push --force --quiet "https://${GH_TOKEN}@${GH_REF}" master:gh-pages > /dev/null 2>&1
--------------------------------------------------------------------------------
/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | the-grid/ed demo page
5 |
6 |
13 |
14 |
15 |
16 |
17 |
18 |
30 |
31 |
69 |
70 |
71 |
72 |
73 |