Browse Source

Merge pull request #3813 from artf/dev

Merge dev
pull/3968/head
Artur Arseniev 5 years ago
committed by GitHub
parent
commit
f9172febf9
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 36
      .github/ISSUE_TEMPLATE/01_bug_report.md
  2. 28
      .github/ISSUE_TEMPLATE/02_feature_request.md
  3. 63
      .github/ISSUE_TEMPLATE/1.bug_report.yml
  4. 11
      .github/ISSUE_TEMPLATE/config.yml
  5. 1
      .github/config.yml
  6. 2
      dist/css/grapes.min.css
  7. 4
      dist/grapes.min.js
  8. 2
      dist/grapes.min.js.map
  9. 9
      docs/.vuepress/components/DemoViewer.vue
  10. 2
      docs/.vuepress/config.js
  11. 1
      docs/api.js
  12. 70
      docs/api/asset.md
  13. 217
      docs/api/assets.md
  14. 190
      docs/api/editor.md
  15. 82
      docs/api/modal_dialog.md
  16. 435
      docs/modules/Assets.md
  17. 2
      docs/modules/Components.md
  18. 127
      docs/modules/Modal.md
  19. 14
      package.json
  20. 44
      src/asset_manager/config/config.js
  21. 277
      src/asset_manager/index.js
  22. 75
      src/asset_manager/model/Asset.js
  23. 18
      src/asset_manager/model/AssetImage.js
  24. 10
      src/asset_manager/view/AssetImageView.js
  25. 6
      src/asset_manager/view/AssetView.js
  26. 20
      src/asset_manager/view/FileUploader.js
  27. 7
      src/canvas/view/CanvasView.js
  28. 76
      src/commands/view/OpenAssets.js
  29. 5
      src/common/module.js
  30. 16
      src/dom_components/view/ComponentImageView.js
  31. 6
      src/editor/config/config.js
  32. 14
      src/editor/index.js
  33. 3
      src/modal_dialog/config/config.js
  34. 99
      src/modal_dialog/index.js
  35. 1
      src/modal_dialog/model/Modal.js
  36. 8
      src/modal_dialog/view/ModalView.js
  37. 3
      src/style_manager/index.js
  38. 22
      src/style_manager/view/PropertyFileView.js
  39. 6
      src/styles/scss/_gjs_canvas.scss
  40. 1
      src/styles/scss/_gjs_variables.scss
  41. 29
      src/trait_manager/index.js
  42. 18
      src/trait_manager/view/TraitsView.js
  43. 2
      src/utils/dom.js
  44. 5
      src/utils/mixins.js
  45. 29
      test/specs/grapesjs/headless.js
  46. 708
      yarn.lock

36
.github/ISSUE_TEMPLATE/01_bug_report.md

@ -1,36 +0,0 @@
---
name: 🐜 Bug report
about: If something is not working as excepted
title: 'BUG: '
---
## WARNING
READ and FOLLOW next 3 steps, then REMOVE them before posting the issue
1. Follow CONTRIBUTING Guidelines https://github.com/artf/grapesjs/blob/master/CONTRIBUTING.md
2. Do a quick SEARCH first, to see if someone else didn't open the same issue
3. All relative statements/questions have to be filled/answered, otherwise, the issue might be CLOSED
4. JSFiddle Starter template https://jsfiddle.net/szLp8h4n
5. CodeSandbox Starter template https://codesandbox.io/s/1r0w2pk1vl
**Version:**
*You can get the version by typing `grapesjs.version` into the console*
**Are you able to reproduce the bug from the demo?**
- [ ] Yes
- [ ] No
**What is the expected behavior?**
*Describe the bug **detailed***
**What is the current behavior?**
*Describe the bug **detailed***
**Are you able to attach screenshots, screencasts or a live demo?**
- [ ] Yes (attach)
- [ ] No

28
.github/ISSUE_TEMPLATE/02_feature_request.md

@ -1,28 +0,0 @@
---
name: 🚀 Feature request
about: If you have a feature request
title: 'FEAT: '
---
## WARNING
READ and FOLLOW next 3 steps, then REMOVE them before posting the issue
1. Follow CONTRIBUTING Guidelines https://github.com/artf/grapesjs/blob/master/CONTRIBUTING.md
2. Do a quick SEARCH first, to see if someone else didn't open the same issue
3. All relative statements/questions have to be filled/answered, otherwise, the issue might be CLOSED
4. JSFiddle Starter template https://jsfiddle.net/szLp8h4n
5. CodeSandbox Starter template https://codesandbox.io/s/1r0w2pk1vl
**What are you trying to add to GrapesJS?**
*Describe your feature request **detailed***
**Is there an alternative at the latest version?**
- [ ] Yes (describe the alternative)
- [ ] No
**Is this related to an issue?**
- [ ] Yes (Give a link to the issue)
- [ ] No

63
.github/ISSUE_TEMPLATE/1.bug_report.yml

@ -0,0 +1,63 @@
name: 🐞 Bug report
description: Create a bug report for GrapesJS.
title: "BUG: "
labels: []
body:
- type: markdown
attributes:
value: |
In order to understand and fix the issue, we ask you to fill correctly all the statements/questions.
**If you don't indicate a reproducible demo with relative steps to reproduce the bug, the issue might be CLOSED.**
Note: before creating a bug issue, search in GitHub Issues to check if a similar bug was already reported.
- type: checkboxes
attributes:
label: GrapesJS version
description: |
As the bug you're facing might be already fixed, we ask you to ensure to use the latest version available [![npm](https://img.shields.io/npm/v/grapesjs.svg)](https://www.npmjs.com/package/grapesjs).
options:
- label: I confirm to use the latest version of GrapesJS
required: true
- type: input
attributes:
label: What browser are you using?
placeholder: ex. Chrome v91
validations:
required: true
- type: input
attributes:
label: Reproducible demo link
description: |
Use one of these starter templates to create your demo: [JSFiddle](https://jsfiddle.net/szLp8h4n) - [CodeSandbox](https://codesandbox.io/s/1r0w2pk1vl).
You can also indicate one of our offical demos if the bug is reproducible there.
validations:
required: true
- type: textarea
attributes:
label: Describe the bug
description: |
Indicate, step by step, how to reproduce the bug, what is the expected behavior and which is the current one.
If you're also able to create a video of the issue, that would be extremely helpful.
value: |
**How to reproduce the bug?**
1. ...
2. ...
**What is the expected behavior?**
...
**What is the current behavior?**
...
If is necessary to execute some code in order to reproduce the bug, paste it here below:
```js
// your code here
```
validations:
required: true
- type: checkboxes
attributes:
label: Code of Conduct
description: By submitting this issue, you agree to follow our [Code of Conduct](https://github.com/artf/grapesjs/blob/dev/CODE_OF_CONDUCT.md)
options:
- label: I agree to follow this project's Code of Conduct
required: true

11
.github/ISSUE_TEMPLATE/config.yml

@ -0,0 +1,11 @@
blank_issues_enabled: false
contact_links:
- name: 🚀 Feature Request
url: https://github.com/artf/grapesjs/discussions/new?category=ideas
about: 'Suggest any ideas you have using our discussion forums.'
- name: 🙏 Help
url: https://github.com/artf/grapesjs/discussions/new?category=q-a
about: 'If you have a question or need help, ask a question on the discussion forums.'
- name: 📢 Show and tell
url: https://github.com/artf/grapesjs/discussions/new?category=show-and-tell
about: "Have something nice to say or share about GrapesJS? We'd love to hear it!"

1
.github/config.yml

@ -1 +0,0 @@
blank_issues_enabled: false

2
dist/css/grapes.min.css

File diff suppressed because one or more lines are too long

4
dist/grapes.min.js

File diff suppressed because one or more lines are too long

2
dist/grapes.min.js.map

File diff suppressed because one or more lines are too long

9
docs/.vuepress/components/DemoViewer.vue

@ -22,11 +22,16 @@ export default {
type: String,
default: '300',
},
darkcode: {
type: Boolean,
default: false,
},
},
computed: {
src() {
const { value, user } = this;
return `//jsfiddle.net/${user}/${value}/embedded/js,html,css,result`;
const { value, user, darkcode } = this;
const dcStr = darkcode ? '/dark/?menuColor=fff&fontColor=333&accentColor=e67891' : '';
return `//jsfiddle.net/${user}/${value}/embedded/js,html,css,result${dcStr}`;
}
}
}

2
docs/.vuepress/config.js

@ -61,6 +61,7 @@ module.exports = {
['/api/i18n', 'I18n'],
['/api/canvas', 'Canvas'],
['/api/assets', 'Asset Manager'],
['/api/asset', `${subDivider}Asset`],
['/api/block_manager', 'Block Manager'],
['/api/block', `${subDivider}Block`],
['/api/commands', 'Commands'],
@ -99,6 +100,7 @@ module.exports = {
['/modules/Traits', 'Traits'],
['/modules/Style-manager', 'Style Manager'],
['/modules/Storage', 'Storage Manager'],
['/modules/Modal', 'Modal'],
['/modules/Plugins', 'Plugins'],
]
}, {

1
docs/api.js

@ -13,6 +13,7 @@ async function generateDocs () {
await Promise.all([
['editor/index.js', 'editor.md'],
['asset_manager/index.js', 'assets.md'],
['asset_manager/model/Asset.js', 'asset.md'],
['block_manager/index.js', 'block_manager.md'],
['block_manager/model/Block.js', 'block.md'],
['commands/index.js', 'commands.md'],

70
docs/api/asset.md

@ -0,0 +1,70 @@
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
## Asset
### Properties
* `type` **[String][1]** Asset type, eg. 'image'.
* `src` **[String][1]** Asset URL, eg. '[https://.../image.png][2]'.
### getType
Get asset type.
#### Examples
```javascript
// Asset: { src: 'https://.../image.png', type: 'image' }
asset.getType(); // -> 'image'
```
Returns **[String][1]**
### getSrc
Get asset URL.
#### Examples
```javascript
// Asset: { src: 'https://.../image.png' }
asset.getSrc(); // -> 'https://.../image.png'
```
Returns **[String][1]**
### getFilename
Get filename of the asset (based on `src`).
#### Examples
```javascript
// Asset: { src: 'https://.../image.png' }
asset.getFilename(); // -> 'image.png'
// Asset: { src: 'https://.../image' }
asset.getFilename(); // -> 'image'
```
Returns **[String][1]**
### getExtension
Get extension of the asset (based on `src`).
#### Examples
```javascript
// Asset: { src: 'https://.../image.png' }
asset.getExtension(); // -> 'png'
// Asset: { src: 'https://.../image' }
asset.getExtension(); // -> ''
```
Returns **[String][1]**
[1]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[2]: https://.../image.png

217
docs/api/assets.md

@ -18,18 +18,85 @@ Once the editor is instantiated you can use its API. Before using these methods
const assetManager = editor.AssetManager;
```
* [add][2]
* [get][3]
* [getAll][4]
* [getAllVisible][5]
* [remove][6]
* [store][7]
* [load][8]
* [getContainer][9]
* [getAssetsEl][10]
* [addType][11]
* [getType][12]
* [getTypes][13]
## Available Events
* `asset:open` - Asset Manager opened.
* `asset:close` - Asset Manager closed.
* `asset:remove` - Asset removed. The [Asset] is passed as an argument to the callback.
* `asset:update` - Asset updated. The updated [Asset] and the object containing changes are passed as arguments to the callback.
* `asset:upload:start` - Before the upload is started.
* `asset:upload:end` - After the upload is ended.
* `asset:upload:error` - On any error in upload, passes the error as an argument.
* `asset:upload:response` - On upload response, passes the result as an argument.
* `asset` - Catch-all event for all the events mentioned above. An object containing all the available data about the triggered event is passed as an argument to the callback.
* `asset:custom` - Event for handling custom Asset Manager UI.
## Methods
* [open][2]
* [close][3]
* [isOpen][4]
* [add][5]
* [get][6]
* [getAll][7]
* [getAllVisible][8]
* [remove][9]
* [store][10]
* [load][11]
* [getContainer][12]
[Asset]: asset.html
## open
Open the asset manager.
### Parameters
* `options` **[Object][13]?** Options for the asset manager. (optional, default `{}`)
* `options.types` **[Array][14]<[String][15]>** Types of assets to show. (optional, default `['image']`)
* `options.select` **[Function][16]?** Type of operation to perform on asset selection. If not specified, nothing will happen.
### Examples
```javascript
assetManager.open({
select(asset, complete) {
const selected = editor.getSelected();
if (selected && selected.is('image')) {
selected.addAttributes({ src: asset.getSrc() });
// The default AssetManager UI will trigger `select(asset, false)` on asset click
// and `select(asset, true)` on double-click
complete && assetManager.close();
}
}
});
// with your custom types (you should have assets with those types declared)
assetManager.open({ types: ['doc'], ... });
```
## close
Close the asset manager.
### Examples
```javascript
assetManager.close();
```
## isOpen
Checks if the asset manager is open
### Examples
```javascript
assetManager.isOpen(); // true | false
```
Returns **[Boolean][17]**
## add
@ -37,75 +104,75 @@ Add new asset/s to the collection. URLs are supposed to be unique
### Parameters
* `asset` **([string][14] | [Object][15] | [Array][16]<[string][14]> | [Array][16]<[Object][15]>)** URL strings or an objects representing the resource.
* `opts` **[Object][15]?** Options (optional, default `{}`)
* `asset` **([String][15] | [Object][13] | [Array][14]<[String][15]> | [Array][14]<[Object][13]>)** URL strings or an objects representing the resource.
* `opts` **[Object][13]?** Options (optional, default `{}`)
### Examples
```javascript
// In case of strings, would be interpreted as images
// As strings
assetManager.add('http://img.jpg');
assetManager.add(['http://img.jpg', './path/to/img.png']);
// Using objects you could indicate the type and other meta informations
// Using objects you can indicate the type and other meta informations
assetManager.add({
// type: 'image', // image is default
src: 'http://img.jpg',
//type: 'image', //image is default
height: 300,
width: 200,
});
assetManager.add([{
src: 'http://img.jpg',
},{
src: './path/to/img.png',
}]);
assetManager.add([{ src: 'img2.jpg' }, { src: 'img2.png' }]);
```
Returns **Model**
Returns **[Asset]**
## get
Returns the asset by URL
Return asset by URL
### Parameters
* `src` **[string][14]** URL of the asset
* `src` **[String][15]** URL of the asset
### Examples
```javascript
var asset = assetManager.get('http://img.jpg');
const asset = assetManager.get('http://img.jpg');
```
Returns **[Object][15]** Object representing the asset
Returns **([Asset] | null)**
## getAll
Return the global collection, containing all the assets
Returns **Collection**
Returns **Collection<[Asset]>**
## getAllVisible
Return the visible collection, which contains assets actually rendered
Returns **Collection**
Returns **Collection<[Asset]>**
## remove
Remove the asset by its URL
Remove asset
### Parameters
* `src` **[string][14]** URL of the asset
* `asset` **([String][15] | [Asset])** Asset or asset URL
* `opts`
### Examples
```javascript
assetManager.remove('http://img.jpg');
const removed = assetManager.remove('http://img.jpg');
// or by passing the Asset
const asset = assetManager.get('http://img.jpg');
assetManager.remove(asset);
```
Returns **this**
Returns **[Asset]** Removed asset
## store
@ -121,7 +188,7 @@ Store assets data to the selected storage
var assets = assetManager.store();
```
Returns **[Object][15]** Data to store
Returns **[Object][13]** Data to store
## load
@ -130,7 +197,7 @@ The fetched data will be added to the collection.
### Parameters
* `data` **[Object][15]** Object of data to load (optional, default `{}`)
* `data` **[Object][13]** Object of data to load (optional, default `{}`)
### Examples
@ -140,7 +207,7 @@ var assets = assetManager.load({
})
```
Returns **[Object][15]** Loaded assets
Returns **[Object][13]** Loaded assets
## getContainer
@ -148,12 +215,6 @@ Return the Asset Manager Container
Returns **[HTMLElement][18]**
## getAssetsEl
Get assets element container
Returns **[HTMLElement][18]**
## render
Render assets
@ -161,7 +222,7 @@ Render assets
### Parameters
* `assts`
* `assets` **[array][16]** Assets to render, without the argument will render all global assets
* `assets` **[array][14]** Assets to render, without the argument will render all global assets
### Examples
@ -178,78 +239,38 @@ assetManager.render(assets.filter(
Returns **[HTMLElement][18]**
## addType
Add new type. If you want to get more about type definition we suggest to read the [module's page][19]
### Parameters
* `id` **[string][14]** Type ID
* `definition` **[Object][15]** Definition of the type. Each definition contains
`model` (business logic), `view` (presentation logic)
and `isType` function which recognize the type of the
passed entity
### Examples
```javascript
assetManager.addType('my-type', {
model: {},
view: {},
isType: (value) => {},
})
```
## getType
Get type
### Parameters
* `id` **[string][14]** Type ID
Returns **[Object][15]** Type definition
## getTypes
Get types
Returns **[Array][16]**
[1]: https://github.com/artf/grapesjs/blob/master/src/asset_manager/config/config.js
[2]: #add
[2]: #open
[3]: #get
[3]: #close
[4]: #getall
[4]: #isopen
[5]: #getallvisible
[5]: #add
[6]: #remove
[6]: #get
[7]: #store
[7]: #getall
[8]: #load
[8]: #getallvisible
[9]: #getcontainer
[9]: #remove
[10]: #getassetsel
[10]: #store
[11]: #addtype
[11]: #load
[12]: #gettype
[12]: #getcontainer
[13]: #gettypes
[13]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[14]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[14]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[15]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[15]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[16]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[16]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function
[17]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[18]: https://developer.mozilla.org/docs/Web/HTML/Element
[19]: /modules/Assets.html

190
docs/api/editor.md

@ -50,15 +50,6 @@ editor.on('EVENT-NAME', (some, argument) => {
* `block:drag` - Dragging block, the block's model and the drag event are passed as arguments
* `block:drag:stop` - Dragging of the block is stopped. As agruments for the callback you get, the dropped component model (if dropped successfully) and the model of the block
### Assets
* `asset:add` - New asset added
* `asset:remove` - Asset removed
* `asset:upload:start` - Before the upload is started
* `asset:upload:end` - After the upload is ended
* `asset:upload:error` - On any error in upload, passes the error as an argument
* `asset:upload:response` - On upload response, passes the result as an argument
### Keymaps
* `keymap:add` - New keymap added. The new keyamp object is passed as an argument
@ -108,11 +99,6 @@ By changing `result.content` you're able to customize what is dropped
* `rte:enable` - RTE enabled. The view, on which RTE is enabled, is passed as an argument
* `rte:disable` - RTE disabled. The view, on which RTE is disabled, is passed as an argument
### Modal
* `modal:open` - Modal is opened
* `modal:close` - Modal is closed
### Commands
* `run:{commandName}` - Triggered when some command is called to run (eg. editor.runCommand('preview'))
@ -123,17 +109,25 @@ By changing `result.content` you're able to customize what is dropped
* `run` - Triggered on run of any command. The id and the result are passed as arguments to the callback
* `stop` - Triggered on stop of any command. The id and the result are passed as arguments to the callback
### Assets
Check the [Assets][2] module.
### Modal
Check the [Modal][3] module.
### Devices
Check the [Devices][2] module.
Check the [Devices][4] module.
### Parser
Check the [Parser][3] module.
Check the [Parser][5] module.
### Pages
Check the [Pages][4] module.
Check the [Pages][6] module.
### General
@ -149,7 +143,7 @@ Returns configuration object
### Parameters
* `prop` **[string][5]?** Property name
* `prop` **[string][7]?** Property name
Returns **any** Returns the configuration object or
the value of the specified property
@ -160,12 +154,12 @@ Returns HTML built inside canvas
### Parameters
* `opts` **[Object][6]** Options (optional, default `{}`)
* `opts` **[Object][8]** Options (optional, default `{}`)
* `opts.component` **Component?** Return the HTML of a specific Component
* `opts.cleanId` **[Boolean][7]** Remove unnecessary IDs (eg. those created automatically) (optional, default `false`)
* `opts.cleanId` **[Boolean][9]** Remove unnecessary IDs (eg. those created automatically) (optional, default `false`)
Returns **[string][5]** HTML string
Returns **[string][7]** HTML string
## getCss
@ -173,13 +167,13 @@ Returns CSS built inside canvas
### Parameters
* `opts` **[Object][6]** Options (optional, default `{}`)
* `opts` **[Object][8]** Options (optional, default `{}`)
* `opts.component` **Component?** Return the CSS of a specific Component
* `opts.json` **[Boolean][7]** Return an array of CssRules instead of the CSS string (optional, default `false`)
* `opts.avoidProtected` **[Boolean][7]** Don't include protected CSS (optional, default `false`)
* `opts.json` **[Boolean][9]** Return an array of CssRules instead of the CSS string (optional, default `false`)
* `opts.avoidProtected` **[Boolean][9]** Don't include protected CSS (optional, default `false`)
Returns **([String][5] | [Array][8]\<CssRule>)** CSS string or array of CssRules
Returns **([String][7] | [Array][10]\<CssRule>)** CSS string or array of CssRules
## getJs
@ -187,11 +181,11 @@ Returns JS of all components
### Parameters
* `opts` **[Object][6]** Options (optional, default `{}`)
* `opts` **[Object][8]** Options (optional, default `{}`)
* `opts.component` **Component?** Get the JS of a specific component
Returns **[String][5]** JS string
Returns **[String][7]** JS string
## getComponents
@ -211,8 +205,8 @@ Set components inside editor's canvas. This method overrides actual components
### Parameters
* `components` **([Array][8]<[Object][6]> | [Object][6] | [string][5])** HTML string or components model
* `opt` **[Object][6]** the options object to be used by the \[setComponents][em#setComponents][9] method (optional, default `{}`)
* `components` **([Array][10]<[Object][8]> | [Object][8] | [string][7])** HTML string or components model
* `opt` **[Object][8]** the options object to be used by the \[setComponents][em#setComponents][11] method (optional, default `{}`)
### Examples
@ -234,10 +228,10 @@ Add components
### Parameters
* `components` **([Array][8]<[Object][6]> | [Object][6] | [string][5])** HTML string or components model
* `opts` **[Object][6]** Options
* `components` **([Array][10]<[Object][8]> | [Object][8] | [string][7])** HTML string or components model
* `opts` **[Object][8]** Options
* `opts.avoidUpdateStyle` **[Boolean][7]** If the HTML string contains styles,
* `opts.avoidUpdateStyle` **[Boolean][9]** If the HTML string contains styles,
by default, they will be created and, if already exist, updated. When this option
is true, styles already created will not be updated. (optional, default `false`)
@ -253,13 +247,13 @@ editor.addComponents({
});
```
Returns **[Array][8]\<Component>**
Returns **[Array][10]\<Component>**
## getStyle
Returns style in JSON format object
Returns **[Object][6]**
Returns **[Object][8]**
## setStyle
@ -267,7 +261,7 @@ Set style inside editor's canvas. This method overrides actual style
### Parameters
* `style` **([Array][8]<[Object][6]> | [Object][6] | [string][5])** CSS string or style model
* `style` **([Array][10]<[Object][8]> | [Object][8] | [string][7])** CSS string or style model
* `opt` (optional, default `{}`)
### Examples
@ -289,7 +283,7 @@ Add styles to the editor
### Parameters
* `style` **([Array][8]<[Object][6]> | [Object][6] | [string][5])** CSS string or style model
* `style` **([Array][10]<[Object][8]> | [Object][8] | [string][7])** CSS string or style model
* `opts` (optional, default `{}`)
### Examples
@ -298,7 +292,7 @@ Add styles to the editor
editor.addStyle('.cls{color: red}');
```
Returns **[Array][8]\<CssRule>** Array of created CssRule instances
Returns **[Array][10]\<CssRule>** Array of created CssRule instances
## getSelected
@ -310,7 +304,7 @@ Returns **Model**
Returns an array of all selected components
Returns **[Array][8]**
Returns **[Array][10]**
## getSelectedToStyle
@ -328,10 +322,10 @@ Select a component
### Parameters
* `el` **(Component | [HTMLElement][10])** Component to select
* `opts` **[Object][6]?** Options
* `el` **(Component | [HTMLElement][12])** Component to select
* `opts` **[Object][8]?** Options
* `opts.scroll` **[Boolean][7]?** Scroll canvas to the selected element
* `opts.scroll` **[Boolean][9]?** Scroll canvas to the selected element
### Examples
@ -350,7 +344,7 @@ Add component to selection
### Parameters
* `el` **(Component | [HTMLElement][10] | [Array][8])** Component to select
* `el` **(Component | [HTMLElement][12] | [Array][10])** Component to select
### Examples
@ -366,7 +360,7 @@ Remove component from selection
### Parameters
* `el` **(Component | [HTMLElement][10] | [Array][8])** Component to select
* `el` **(Component | [HTMLElement][12] | [Array][10])** Component to select
### Examples
@ -382,7 +376,7 @@ Toggle component selection
### Parameters
* `el` **(Component | [HTMLElement][10] | [Array][8])** Component to select
* `el` **(Component | [HTMLElement][12] | [Array][10])** Component to select
### Examples
@ -414,7 +408,7 @@ change the canvas to the proper width
### Parameters
* `name` **[string][5]** Name of the device
* `name` **[string][7]** Name of the device
### Examples
@ -436,7 +430,7 @@ console.log(device);
// 'Tablet'
```
Returns **[string][5]** Device name
Returns **[string][7]** Device name
## runCommand
@ -444,8 +438,8 @@ Execute command
### Parameters
* `id` **[string][5]** Command ID
* `options` **[Object][6]** Custom options (optional, default `{}`)
* `id` **[string][7]** Command ID
* `options` **[Object][8]** Custom options (optional, default `{}`)
### Examples
@ -461,8 +455,8 @@ Stop the command if stop method was provided
### Parameters
* `id` **[string][5]** Command ID
* `options` **[Object][6]** Custom options (optional, default `{}`)
* `id` **[string][7]** Command ID
* `options` **[Object][8]** Custom options (optional, default `{}`)
### Examples
@ -478,9 +472,9 @@ Store data to the current storage
### Parameters
* `clb` **[Function][11]** Callback function
* `clb` **[Function][13]** Callback function
Returns **[Object][6]** Stored data
Returns **[Object][8]** Stored data
## storeData
@ -493,7 +487,7 @@ console.log(editor.storeData());
// { pages: [...], styles: [...], ... }
```
Returns **[Object][6]**
Returns **[Object][8]**
## load
@ -501,9 +495,9 @@ Load data from the current storage
### Parameters
* `clb` **[Function][11]** Callback function
* `clb` **[Function][13]** Callback function
Returns **[Object][6]** Stored data
Returns **[Object][8]** Stored data
## loadData
@ -511,7 +505,7 @@ Load data from the JSON data object
### Parameters
* `data` **[Object][6]** Data to load
* `data` **[Object][8]** Data to load
### Examples
@ -519,21 +513,21 @@ Load data from the JSON data object
editor.loadData({ pages: [...], styles: [...], ... })
```
Returns **[Object][6]** Loaded object
Returns **[Object][8]** Loaded object
## getContainer
Returns container element. The one which was indicated as 'container'
on init method
Returns **[HTMLElement][10]**
Returns **[HTMLElement][12]**
## getDirtyCount
Return the count of changes made to the content and not yet stored.
This count resets at any `store()`
Returns **[number][12]**
Returns **[number][14]**
## refresh
@ -546,9 +540,9 @@ refresh you'll get misleading position of tools
### Parameters
* `opts`
* `options` **[Object][6]?** Options
* `options` **[Object][8]?** Options
* `options.tools` **[Boolean][7]** Update the position of tools (eg. rich text editor, component highlighter, etc.) (optional, default `false`)
* `options.tools` **[Boolean][9]** Update the position of tools (eg. rich text editor, component highlighter, etc.) (optional, default `false`)
## setCustomRte
@ -556,7 +550,7 @@ Replace the built-in Rich Text Editor with a custom one.
### Parameters
* `obj` **[Object][6]** Custom RTE Interface
* `obj` **[Object][8]** Custom RTE Interface
### Examples
@ -596,7 +590,7 @@ custom parser, pass `null` as the argument
### Parameters
* `parser` **([Function][11] | null)** Parser function
* `parser` **([Function][13] | null)** Parser function
### Examples
@ -618,11 +612,11 @@ Returns **this**
## setDragMode
Change the global drag mode of components.
To get more about this feature read: [https://github.com/artf/grapesjs/issues/1936][13]
To get more about this feature read: [https://github.com/artf/grapesjs/issues/1936][15]
### Parameters
* `value` **[String][5]** Drag mode, options: 'absolute' | 'translate'
* `value` **[String][7]** Drag mode, options: 'absolute' | 'translate'
Returns **this**
@ -633,10 +627,10 @@ Trigger event log message
### Parameters
* `msg` **any** Message to log
* `opts` **[Object][6]** Custom options (optional, default `{}`)
* `opts` **[Object][8]** Custom options (optional, default `{}`)
* `opts.ns` **[String][5]** Namespace of the log (eg. to use in plugins) (optional, default `''`)
* `opts.level` **[String][5]** Level of the log, `debug`, `info`, `warning`, `error` (optional, default `'debug'`)
* `opts.ns` **[String][7]** Namespace of the log (eg. to use in plugins) (optional, default `''`)
* `opts.level` **[String][7]** Level of the log, `debug`, `info`, `warning`, `error` (optional, default `'debug'`)
### Examples
@ -658,11 +652,11 @@ Translate label
### Parameters
* `args` **...any**
* `key` **[String][5]** Label to translate
* `opts` **[Object][6]?** Options for the translation
* `key` **[String][7]** Label to translate
* `opts` **[Object][8]?** Options for the translation
* `opts.params` **[Object][6]?** Params for the translation
* `opts.noWarn` **[Boolean][7]?** Avoid warnings in case of missing resources
* `opts.params` **[Object][8]?** Params for the translation
* `opts.noWarn` **[Boolean][9]?** Avoid warnings in case of missing resources
### Examples
@ -674,7 +668,7 @@ editor.t('msg2', { params: { test: 'hello' } });
editor.t('msg2', { params: { test: 'hello' }, l: 'it' });
```
Returns **[String][5]**
Returns **[String][7]**
## on
@ -682,8 +676,8 @@ Attach event
### Parameters
* `event` **[string][5]** Event name
* `callback` **[Function][11]** Callback function
* `event` **[string][7]** Event name
* `callback` **[Function][13]** Callback function
Returns **this**
@ -693,8 +687,8 @@ Attach event and detach it after the first run
### Parameters
* `event` **[string][5]** Event name
* `callback` **[Function][11]** Callback function
* `event` **[string][7]** Event name
* `callback` **[Function][13]** Callback function
Returns **this**
@ -704,8 +698,8 @@ Detach event
### Parameters
* `event` **[string][5]** Event name
* `callback` **[Function][11]** Callback function
* `event` **[string][7]** Event name
* `callback` **[Function][13]** Callback function
Returns **this**
@ -715,7 +709,7 @@ Trigger event
### Parameters
* `event` **[string][5]** Event to trigger
* `event` **[string][7]** Event to trigger
Returns **this**
@ -727,7 +721,7 @@ Destroy the editor
Render editor
Returns **[HTMLElement][10]**
Returns **[HTMLElement][12]**
## html
@ -735,8 +729,8 @@ Print safe HTML by using ES6 tagged template strings.
### Parameters
* `literals` **[Array][8]<[String][5]>**
* `substs` **[Array][8]<[String][5]>**
* `literals` **[Array][10]<[String][7]>**
* `substs` **[Array][10]<[String][7]>**
### Examples
@ -747,30 +741,34 @@ const safeStr = '<b>Hello</b>';
const strHtml = editor.html`Escaped ${unsafeStr}, unescaped $${safeStr}`;
```
Returns **[String][5]**
Returns **[String][7]**
[1]: https://github.com/artf/grapesjs/blob/master/src/editor/config/config.js
[2]: /api/device_manager.html
[2]: /api/assets.html
[3]: /api/modal_dialog.html
[4]: /api/device_manager.html
[3]: /api/parser.html
[5]: /api/parser.html
[4]: /api/pages.html
[6]: /api/pages.html
[5]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[7]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[6]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[7]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[9]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[8]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[10]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[9]: em#setComponents
[11]: em#setComponents
[10]: https://developer.mozilla.org/docs/Web/HTML/Element
[12]: https://developer.mozilla.org/docs/Web/HTML/Element
[11]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function
[13]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Statements/function
[12]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number
[14]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Number
[13]: https://github.com/artf/grapesjs/issues/1936
[15]: https://github.com/artf/grapesjs/issues/1936

82
docs/api/modal_dialog.md

@ -18,6 +18,14 @@ Once the editor is instantiated you can use its API. Before using these methods
const modal = editor.Modal;
```
## Available Events
* `modal:open` - Modal is opened
* `modal:close` - Modal is closed
* `modal` - Event triggered on any change related to the modal. An object containing all the available data about the triggered event is passed as an argument to the callback.
## Methods
* [open][2]
* [close][3]
* [isOpen][4]
@ -40,12 +48,28 @@ Open the modal window
* `opts.content` **([String][12] | [HTMLElement][13])?** Content to set for the modal
* `opts.attributes` **[Object][11]?** Updates the modal wrapper with custom attributes
### Examples
```javascript
modal.open({
title: 'My title',
content: 'My content',
attributes: { class: 'my-class' },
});
```
Returns **this**
## close
Close the modal window
### Examples
```javascript
modal.close();
```
Returns **this**
## onceClose
@ -55,7 +79,15 @@ The callback will be called one only time
### Parameters
* `clb` **[Function][14]**
* `clb` **[Function][14]** Callback to call
### Examples
```javascript
modal.onceClose(() => {
console.log('The modal is closed');
});
```
Returns **this**
@ -66,7 +98,15 @@ The callback will be called one only time
### Parameters
* `clb` **[Function][14]**
* `clb` **[Function][14]** Callback to call
### Examples
```javascript
modal.onceOpen(() => {
console.log('The modal is opened');
});
```
Returns **this**
@ -74,6 +114,12 @@ Returns **this**
Checks if the modal window is open
### Examples
```javascript
modal.isOpen(); // true | false
```
Returns **[Boolean][15]**
## setTitle
@ -82,12 +128,17 @@ Set the title to the modal window
### Parameters
* `title` **[string][12]** Title
* `title` **([string][12] | [HTMLElement][13])** Title
### Examples
```javascript
modal.setTitle('New title');
// pass a string
modal.setTitle('Some title');
// or an HTMLElement
const el = document.createElement('div');
el.innerText = 'New title';
modal.setTitle(el);
```
Returns **this**
@ -96,7 +147,13 @@ Returns **this**
Returns the title of the modal window
Returns **[string][12]**
### Examples
```javascript
modal.getTitle();
```
Returns **([string][12] | [HTMLElement][13])**
## setContent
@ -109,7 +166,12 @@ Set the content of the modal window
### Examples
```javascript
modal.setContent('<div>Some HTML content</div>');
// pass a string
modal.setContent('Some content');
// or an HTMLElement
const el = document.createElement('div');
el.innerText = 'New content';
modal.setContent(el);
```
Returns **this**
@ -118,7 +180,13 @@ Returns **this**
Get the content of the modal window
Returns **[string][12]**
### Examples
```javascript
modal.getContent();
```
Returns **([string][12] | [HTMLElement][13])**
[1]: https://github.com/artf/grapesjs/blob/master/src/modal_dialog/config/config.js

435
docs/modules/Assets.md

@ -13,7 +13,7 @@ In this section, you will see how to setup and take the full advantage of built-
## Configuration
To change default configurations you'll have to pass `assetManager` property with the main configuration object
To change default configurations you'd need to pass the `assetManager` property with the main configuration object
```js
const editor = grapesjs.init({
@ -25,101 +25,13 @@ const editor = grapesjs.init({
});
```
You can update most of them later by using `getConfig` inside of the module
```js
const amConfig = editor.AssetManager.getConfig();
```
Below is a list of currently available options
```js
// Default assets
// eg. [
// 'https://...image1.png',
// 'https://...image2.png',
// {type: 'image', src: 'https://...image3.png', someOtherCustomProp: 1},
// ..
// ]
assets: [],
// Content to add where there is no assets to show
// eg. 'No <b>assets</b> here, drag to upload'
noAssets: '',
// Upload endpoint, set `false` to disable upload
// upload: 'https://endpoint/upload/assets',
// upload: false,
upload: 0,
// The name used in POST to pass uploaded files
uploadName: 'files',
// Custom headers to pass with the upload request
headers: {},
// Custom parameters to pass with the upload request, eg. csrf token
params: {},
// The credentials setting for the upload request, eg. 'include', 'omit'
credentials: 'include',
// Allow uploading multiple files per request.
// If disabled filename will not have '[]' appended
multiUpload: true,
// If true, tries to add automatically uploaded assets.
// To make it work the server should respond with a JSON containing assets
// in a data key, eg:
// {
// data: [
// 'https://.../image.png',
// ...
// {src: 'https://.../image2.png'},
// ...
// ]
// }
autoAdd: 1,
// Text on upload input
uploadText: 'Drop files here or click to upload',
// Label for the add button
addBtnText: 'Add image',
// Custom uploadFile function
// @example
// uploadFile: (e) => {
// var files = e.dataTransfer ? e.dataTransfer.files : e.target.files;
// // ...send somewhere
// }
uploadFile: '',
// Handle the image url submit from the built-in 'Add image' form
// @example
// handleAdd: (textFromInput) => {
// // some check...
// editor.AssetManager.add(textFromInput);
// }
handleAdd: '',
// Enable an upload dropzone on the entire editor (not document) when dragging
// files over it
dropzone: 1,
// Open the asset manager once files are been dropped via the dropzone
openAssetsOnDrop: 1,
// Any dropzone content to append inside dropzone element
dropzoneContent: '',
// Default title for the asset manager modal
modalTitle: 'Select Image',
```
Sometimes the code gets ahead of the docs, therefore we'd suggest to keep an eye at the current state of configurations by checking the dedicated source file [Asset Manager Config](https://github.com/artf/grapesjs/blob/dev/src/asset_manager/config/config.js)
Check the full list of available options here: [Asset Manager Config](https://github.com/artf/grapesjs/blob/master/src/asset_manager/config/config.js)
@ -140,7 +52,7 @@ const editor = grapesjs.init({
type: 'image',
src: 'http://placehold.it/350x250/459ba8/fff/image2.jpg',
height: 350,
width: 250,
width: 250,
name: 'displayName'
},
{
@ -165,6 +77,7 @@ The built-in Asset Manager modal is implemented and is showing up when requested
<img :src="$withBase('/assets-builtin-modal.png')">
<!--
Making the modal appear is registered with a command, so you can make it appear with this
```js
@ -181,16 +94,117 @@ editor.runCommand('open-assets', {
});
```
Now you should be able to change the image of the component.
-->
## Customization
## Uploading assets
The default Asset Manager includes also an easy to use, drag-and-drop uploader with a few UI helpers. The default uploader is already visible when you open the Asset Manager.
<img :src="$withBase('/assets-uploader.png')">
You can click on the uploader to select your files or just drag them directly from your computer to trigger the uploader. Obviously, before it will work you have to setup your server to receive your assets and specify the upload endpoint in your configuration
If you want to customize the Asset Manager after the initialization you have to use its [APIs](API-Asset-Manager)
```js
const editor = grapesjs.init({
...
assetManager: {
...
// Upload endpoint, set `false` to disable upload, default `false`
upload: 'https://endpoint/upload/assets',
// The name used in POST to pass uploaded files, default: `'files'`
uploadName: 'files',
...
},
...
});
```
### Listeners
If you want to execute an action before/after the uploading process (eg. loading animation) or even on response, you can make use of these listeners
```js
// The upload is started
editor.on('asset:upload:start', () => {
startAnimation();
});
// The upload is ended (completed or not)
editor.on('asset:upload:end', () => {
endAnimation();
});
// Error handling
editor.on('asset:upload:error', (err) => {
notifyError(err);
});
// Do something on response
editor.on('asset:upload:response', (response) => {
...
});
```
### Response
When the uploading is over, by default (via config parameter `autoAdd: 1`), the editor expects to receive a JSON of uploaded assets in a `data` key as a response and tries to add them to the main collection. The JSON might look like this:
```js
{
data: [
'https://.../image.png',
// ...
{
src: 'https://.../image2.png',
type: 'image',
height: 100,
width: 200,
},
// ...
]
}
```
<!-- Deprecated
### Setup Dropzone
There is another helper which improves the uploading of assets: A full-width editor dropzone.
<img :src="$withBase('/assets-full-dropzone.gif')">
All you have to do is to activate it and possibly set a custom content (you might also want to hide the default uploader)
```js
const editor = grapesjs.init({
...
assetManager: {
...,
dropzone: 1,
dropzoneContent: '<div class="dropzone-inner">Drop here your assets</div>'
}
});
``` -->
## Programmatic usage
If you need to manage your assets programmatically you have to use its [APIs][API-Asset-Manager]
```js
// Get the Asset Manager module first
@ -248,19 +262,124 @@ You can also mix arrays of assets
```js
am.render([...assets1, ...assets2, ...assets3]);
```
<!--
If you want to customize the asset manager container you can get its `HTMLElement`
```js
am.getContainer().insertAdjacentHTML('afterbegin', '<div><button type="button">Click</button></div>');
```
-->
In case you want to update or remove an asset, you can make use of this methods
```js
// Get the asset via its `src`
const asset = am.get('http://.../img.jpg');
// Update asset property
asset.set({ src: 'http://.../new-img.jpg' });
// Remove asset
am.remove(asset); // or via src, am.remove('http://.../new-img.jpg');
```
For more APIs methods check out the [API Reference](API-Asset-Manager)
### Custom select logic
::: warning
This section is referring to GrapesJS v0.17.26 or higher
:::
You can open the Asset Manager with your own select logic.
```js
am.open({
types: ['image'], // This is the default option
// Without select, nothing will happen on asset selection
select(asset, complete) {
const selected = editor.getSelected();
if (selected && selected.is('image')) {
selected.addAttributes({ src: asset.getSrc() });
// The default AssetManager UI will trigger `select(asset, false)`
// on asset click and `select(asset, true)` on double-click
complete && am.close();
}
}
});
```
## Customization
The default Asset Manager UI is great for simple things, but except the possibility to tweak some CSS style, adding more complex things like a search input, filters, etc. requires a replace of the defualt UI.
All you have to do is to indicate the editor your intent to use a custom UI and then subscribe to the `asset:custom` event that will give you all the information on any requested change.
```js
const editor = grapesjs.init({
// ...
assetManager: {
// ...
custom: true,
},
});
editor.on('asset:custom', props => {
// The `props` will contain all the information you need in order to update your UI.
// props.open (boolean) - Indicates if the Asset Manager is open
// props.assets (Array<Asset>) - Array of all assets
// props.types (Array<String>) - Array of asset types requested, eg. ['image'],
// props.close (Function) - A callback to close the Asset Manager
// props.remove (Function<Asset>) - A callback to remove an asset
// props.select (Function<Asset, boolean>) - A callback to select an asset
// props.container (HTMLElement) - The element where you should append your UI
// Here you would put the logic to render/update your UI.
});
```
Here an example of using custom Asset Manager with a Vue component.
<demo-viewer value="wbj4tmqk" height="500" darkcode/>
The example above is the right way if you need to replace the default UI, but as you might notice we append the mounted element to the container `props.container.appendChild(this.$el);`.
This is required as the Asset Manager, by default, is placed in the [Modal](/modules/Modal.html).
How to approach the case when your Asset Manager is a completely independent/external module (eg. should be showed in its own custom modal)? Not a problem, you can bind the Asset Manager state via `assetManager.custom.open`.
```js
const editor = grapesjs.init({
// ...
assetManager: {
// ...
custom: {
open(props) {
// `props` are the same used in `asset:custom` event
// ...
// Init and open your external Asset Manager
// ...
// IMPORTANT:
// When the external library is closed you have to comunicate
// this state back to the editor, otherwise GrapesJS will think
// the Asset Manager is still open.
// example: myAssetManager.on('close', () => props.close())
},
close(props) {
// Close the external Asset Manager
},
},
},
});
```
It's important to declare also the `close` function, the editor should be able to close the Asset Manager via `am.close()`.
<!--
### Define new Asset type
Generally speaking, images aren't the only asset you'll use, it could be a `video`, `svg-icon`, or any other kind of `document`. Each type of asset is applied in our templates/pages differently. If you need to change the image of the Component all you need is another `url` in `src` attribute. However In case of a `svg-icon`, its not the same, you might want to replace the element with a new `<svg>` content. Besides this you also have to deal with the presentation/preview of the asset inside the panel/modal. For example, showing a thumbnail for big images or the possibility to preview videos.
@ -414,9 +533,6 @@ am.addType('svg-icon', {
```
### Extend Asset Types
Extending asset types is basically the same as adding them, you can choose what type to extend and how.
@ -463,119 +579,7 @@ am.addType('image', {
}
},
})
```
## Uploading assets
Asset Manager includes an easy to use, drag-and-drop uploader with a few UI helpers. The default uploader is already visible when you open the Asset Manager.
<img :src="$withBase('/assets-uploader.png')">
You can click on the uploader to select your files or just drag them directly from your computer to trigger the uploader. Obviously, before it will work you have to setup your server to receive your assets and specify the upload endpoint in your configuration
```js
let editor = grapesjs.init({
...
assetManager: {
...
// Upload endpoint, set `false` to disable upload, default `false`
upload: 'https://endpoint/upload/assets',
// The name used in POST to pass uploaded files, default: `'files'`
uploadName: 'files',
...
},
...
});
```
### Listeners
If you want to execute an action before/after the uploading process (eg. loading animation) or even on response, you can make use of these listeners
```js
// The upload is started
editor.on('asset:upload:start', () => {
...
startAnimation();
});
// The upload is ended (completed or not)
editor.on('asset:upload:end', () => {
...
endAnimation();
});
// Error handling
editor.on('asset:upload:error', (err) => {
...
notifyError(err);
});
// Do something on response
editor.on('asset:upload:response', (response) => {
...
});
```
### Response
When the uploading is over, by default (via config parameter `autoAdd: 1`), the editor expects to receive a JSON blob of uploaded assets in a `data` key as a response and tries to add them to the main collection. The JSON might look like this:
```js
{
data: [
'https://.../image.png',
// ...
{
src: 'https://.../image2.png',
type: 'image',
height: 100,
width: 200,
},
// ...
]
}
```
### Setup Dropzone
There is another helper which improves the uploading of assets: A full-width editor dropzone.
<img :src="$withBase('/assets-full-dropzone.gif')">
All you have to do is to activate it and possibly set a custom content (you might also want to hide the default uploader)
```js
const editor = grapesjs.init({
...
assetManager: {
...,
dropzone: 1,
dropzoneContent: '<div class="dropzone-inner">Drop here your assets</div>'
}
});
```
``` -->
@ -583,12 +587,7 @@ const editor = grapesjs.init({
## Events
Currently available events you can listen to
For a complete list of available events, you can check it [here](/api/assets.html#available-events).
* `asset:add` - New asset added
* `asset:remove` - Asset removed
* `asset:upload:start` - Before the upload is started
* `asset:upload:end` - After the upload is ended
* `asset:upload:error` - On any error in upload, passes the error as an argument
* `asset:upload:response` - On upload response, passes the result as an argument
[API-Asset-Manager]: </api/assets.html>

2
docs/modules/Components.md

@ -4,7 +4,7 @@ title: Component Manager
# Component Manager
The Component is a base element of the template. It might be something simple and atomic like an image or a text box, but also complex structures, more probably composed by other components, like sections or pages. The concept of the component was made to allow the developer to bind different behaviors to different elements. For example, opening the Asset Manager on double click of the image is a custom behavior binded to that particular type of element.
The Component is a base element of the template. It might be something simple and atomic like an image or a text box, but also complex structures, more probably composed by other components, like sections or pages. The concept of the component was made to allow the developer to bind different behaviors to different elements. For example, opening the Asset Manager on double click of the image is a custom behavior bound to that particular type of element.
::: warning
This guide is referring to GrapesJS v0.15.8 or higher

127
docs/modules/Modal.md

@ -0,0 +1,127 @@
---
title: Modal
---
# Modal
The **Modal** module allows to easily display content in a dialog window.
::: warning
This guide is referring to GrapesJS v0.17.26 or higher
:::
[[toc]]
## Basic usage
You can easily display your content by calling a single API call.
```js
// Init editor
const editor = grapesjs.init({ ... });
// Open modal
const openModal = () => {
editor.Modal.open({
title: 'My title', // string | HTMLElement
content: 'My content', // string | HTMLElement
});
};
// Create a simple custom button that will open the modal
document.body.insertAdjacentHTML('afterbegin',`
<button onclick="openModal()">Open Modal</button>
`);
```
## Using API
By using other [available APIs](/api/modal_dialog.html) you have full control of the modal (eg. updating content/title, closing the modal, etc.).
Here are a few examples:
```js
const { Modal } = editor;
// Close the modal
Modal.close();
// Check if the modal is open
Modal.isOpen();
// Update title
Modal.setTitle('New title');
// Update content
Modal.setContent('New content');
// Execute one-time callback on modal close
Modal.onceClose(() => {
console.log('My last modal is closed');
});
```
## Customization
The modal can be fully customized and you have different available options.
The fastest and the easiest one is to use your specific CSS for the modal element. With a few lines of CSS your modal can be completely adapted to your choices.
```css
.gjs-mdl-dialog {
background-color: white;
color: #333;
}
```
In case you have to customize a specific modal differently, you can rely on your custom class attributes.
```js
editor.Modal.open({
title: 'My title',
content: 'My content',
attributes: {
class: 'my-small-modal',
},
});
```
```css
.my-small-modal .gjs-mdl-dialog {
max-width: 300px;
}
```
::: warning
Your custom CSS has to be loaded after the GrapesJS one.
:::
### Custom Modal
For more advanced usage, you can completely replace the default modal with one of your own. All you have to do is to indicate the editor your intent to use a custom modal and then subscribe to the `modal` event that will give you all the information on any requested change.
```js
const editor = grapesjs.init({
// ...
modal: { custom: true },
});
editor.on('modal', props => {
// The `props` will contain all the information you need in order to update your custom modal.
// props.open (boolean) - Indicates if the modal should be open
// props.title (Node) - Modal title
// props.content (Node) - Modal content
// props.attributes (Object) - Modal custom attributes (eg. class)
// props.close (Function) - A callback to use when you want to close the modal programmatically
// Here you would put the logic to control your modal.
});
```
Here an example of using the Bootstrap modal.
<demo-viewer value="x70amv3f" height="500" darkcode/>
## Events
For a complete list of available events, you can check it [here](/api/modal_dialog.html#available-events).

14
package.json

@ -1,7 +1,7 @@
{
"name": "grapesjs",
"description": "Free and Open Source Web Builder Framework",
"version": "0.17.25",
"version": "0.17.26",
"author": "Artur Arseniev",
"license": "BSD-3-Clause",
"homepage": "http://grapesjs.com",
@ -19,25 +19,24 @@
"backbone": "1.3.3",
"backbone-undo": "^0.2.5",
"cash-dom": "^2.3.9",
"codemirror": "^5.61.1",
"codemirror": "^5.63.0",
"codemirror-formatting": "^1.0.0",
"keymaster": "^1.6.2",
"promise-polyfill": "^8.1.3",
"spectrum-colorpicker": "^1.8.0",
"underscore": "^1.13.1"
},
"devDependencies": {
"@babel/cli": "^7.14.5",
"@babel/cli": "^7.15.7",
"@vuepress/plugin-google-analytics": "^1.8.2",
"documentation": "^13.2.5",
"eslint": "^7.28.0",
"grapesjs-cli": "^1.0.15",
"eslint": "^7.32.0",
"grapesjs-cli": "^2.0.0",
"husky": "^2.7.0",
"jest": "^24.9.0",
"lint-staged": "^8.2.1",
"npm-run-all": "^4.1.5",
"prettier": "^1.18.2",
"sass": "^1.34.1",
"sass": "^1.42.1",
"sinon": "^7.5.0",
"string-replace-loader": "^2.2.0",
"vuepress": "^1.8.2",
@ -104,7 +103,6 @@
"build": "npm run check && run-s build:*",
"build:js": "grapesjs-cli build --targets=\"> 1%, ie 11, safari 8, not dead\" --statsOutput=\"stats.json\" --localePath=\"src/i18n/locale\"",
"build:css": "sass src/styles/scss/main.scss dist/css/grapes.min.css --no-source-map --style=compressed --load-path=node_modules",
"build:locale": "rm -rf ./locale && node scripts/build-locale.js && babel locale -d locale --copy-files --no-comments",
"start": "run-p start:*",
"start:js": "grapesjs-cli serve",
"start:css": "npm run build:css -- --watch",

44
src/asset_manager/config/config.js

@ -47,14 +47,14 @@ export default {
// ...
// ]
// }
autoAdd: 1,
autoAdd: true,
// To upload your assets, the module uses Fetch API, with this option you
// overwrite it with something else.
// It should return a Promise
// @example
// customFetch: (url, options) => axios(url, { data: options.body }),
customFetch: '',
customFetch: null,
// Custom uploadFile function.
// Differently from the `customFetch` option, this gives a total control
@ -65,10 +65,10 @@ export default {
// var files = e.dataTransfer ? e.dataTransfer.files : e.target.files;
// // ...send somewhere
// }
uploadFile: '',
uploadFile: null,
// In the absence of 'uploadFile' or 'upload' assets will be embedded as Base64
embedAsBase64: 1,
embedAsBase64: true,
// Handle the image url submit from the built-in 'Add image' form
// @example
@ -76,21 +76,9 @@ export default {
// // some check...
// editor.AssetManager.add(textFromInput);
// }
handleAdd: '',
handleAdd: null,
// Enable an upload dropzone on the entire editor (not document) when dragging
// files over it
// If active the dropzone disable/hide the upload dropzone in asset modal,
// otherwise you will get double drops (#507)
dropzone: 0,
// Open the asset manager once files are been dropped via the dropzone
openAssetsOnDrop: 1,
// Any dropzone content to append inside dropzone element
dropzoneContent: '',
//method called before upload, on return false upload is canceled.
// Method called before upload, on return false upload is canceled.
// @example
// beforeUpload: (files) => {
// // logic...
@ -100,5 +88,23 @@ export default {
beforeUpload: null,
// Toggles visiblity of assets url input
showUrlInput: true
showUrlInput: true,
// Avoid rendering the default asset manager.
custom: false,
// WARNING: all the options below are considered DEPRECATED.
// ---------------------------------------------------------------
// Enable an upload dropzone on the entire editor (not document) when dragging
// files over it
// If active the dropzone disable/hide the upload dropzone in asset modal,
// otherwise you will get double drops (#507)
dropzone: false,
// Open the asset manager once files are been dropped via the dropzone
openAssetsOnDrop: 1,
// Any dropzone content to append inside dropzone element
dropzoneContent: ''
};

277
src/asset_manager/index.js

@ -14,6 +14,22 @@
* const assetManager = editor.AssetManager;
* ```
*
* ## Available Events
* * `asset:open` - Asset Manager opened.
* * `asset:close` - Asset Manager closed.
* * `asset:remove` - Asset removed. The [Asset] is passed as an argument to the callback.
* * `asset:update` - Asset updated. The updated [Asset] and the object containing changes are passed as arguments to the callback.
* * `asset:upload:start` - Before the upload is started.
* * `asset:upload:end` - After the upload is ended.
* * `asset:upload:error` - On any error in upload, passes the error as an argument.
* * `asset:upload:response` - On upload response, passes the result as an argument.
* * `asset` - Catch-all event for all the events mentioned above. An object containing all the available data about the triggered event is passed as an argument to the callback.
* * `asset:custom` - Event for handling custom Asset Manager UI.
*
* ## Methods
* * [open](#open)
* * [close](#close)
* * [isOpen](#isopen)
* * [add](#add)
* * [get](#get)
* * [getAll](#getall)
@ -22,56 +38,65 @@
* * [store](#store)
* * [load](#load)
* * [getContainer](#getcontainer)
* * [getAssetsEl](#getassetsel)
* * [addType](#addtype)
* * [getType](#gettype)
* * [getTypes](#gettypes)
*
* [Asset]: asset.html
*
* @module AssetManager
*/
import { debounce, isFunction } from 'underscore';
import Module from 'common/module';
import defaults from './config/config';
import Asset from './model/Assets';
import Assets from './model/Assets';
import AssetsView from './view/AssetsView';
import FileUpload from './view/FileUploader';
export const evAll = 'asset';
export const evPfx = `${evAll}:`;
export const evSelect = `${evPfx}select`;
export const evUpdate = `${evPfx}update`;
export const evAdd = `${evPfx}add`;
export const evRemove = `${evPfx}remove`;
export const evRemoveBefore = `${evRemove}:before`;
export const evCustom = `${evPfx}custom`;
export const evOpen = `${evPfx}open`;
export const evClose = `${evPfx}close`;
export default () => {
let c = {};
let assets, assetsVis, am, fu;
const assetCmd = 'open-assets';
return {
/**
* Name of the module
* @type {String}
* @private
*/
...Module,
name: 'AssetManager',
/**
* Mandatory for the storage manager
* @type {String}
* @private
*/
storageKey: 'assets',
getConfig() {
return c;
},
Asset,
/**
* Initialize module
* @param {Object} config Configurations
* @private
*/
init(config) {
c = config || {};
Assets,
for (let name in defaults) {
if (!(name in c)) c[name] = defaults[name];
}
events: {
all: evAll,
select: evSelect,
update: evUpdate,
add: evAdd,
remove: evRemove,
removeBefore: evRemoveBefore,
custom: evCustom,
open: evOpen,
close: evClose
},
init(config = {}) {
c = { ...defaults, ...config };
const ppfx = c.pStylePrefix;
const em = c.em;
const { em } = c;
this.config = c;
this.em = em;
if (ppfx) {
c.stylePrefix = ppfx + c.stylePrefix;
@ -80,43 +105,120 @@ export default () => {
// Global assets collection
assets = new Assets([]);
assetsVis = new Assets([]);
this.all = assets;
this.__initListen();
// Setup the sync between the global and public collections
assets.listenTo(assets, 'add', model => {
this.getAllVisible().add(model);
em && em.trigger('asset:add', model);
});
assets.on('add', model => this.getAllVisible().add(model));
assets.on('remove', model => this.getAllVisible().remove(model));
assets.listenTo(assets, 'remove', model => {
this.getAllVisible().remove(model);
em && em.trigger('asset:remove', model);
return this;
},
__propEv(ev, ...data) {
this.em.trigger(ev, ...data);
this.getAll().trigger(ev, ...data);
},
__onAllEvent: debounce(function() {
this.__trgCustom();
}),
__trgCustom() {
const bhv = this.__getBehaviour();
if (!bhv.container && !this.getConfig('custom').open) {
return;
}
this.em.trigger(this.events.custom, this.__customData());
},
__customData() {
const bhv = this.__getBehaviour();
return {
am: this,
open: this.isOpen(),
assets: this.getAll().models,
types: bhv.types || [],
container: bhv.container,
close: () => this.close(),
remove: (...args) => this.remove(...args),
select: (asset, complete) => {
const res = this.add(asset);
isFunction(bhv.select) && bhv.select(res, complete);
},
// extra
options: bhv.options || {}
};
},
/**
* Open the asset manager.
* @param {Object} [options] Options for the asset manager.
* @param {Array<String>} [options.types=['image']] Types of assets to show.
* @param {Function} [options.select] Type of operation to perform on asset selection. If not specified, nothing will happen.
* @example
* assetManager.open({
* select(asset, complete) {
* const selected = editor.getSelected();
* if (selected && selected.is('image')) {
* selected.addAttributes({ src: asset.getSrc() });
* // The default AssetManager UI will trigger `select(asset, false)` on asset click
* // and `select(asset, true)` on double-click
* complete && assetManager.close();
* }
* }
* });
* // with your custom types (you should have assets with those types declared)
* assetManager.open({ types: ['doc'], ... });
*/
open(options = {}) {
const cmd = this.em.get('Commands');
cmd.run(assetCmd, {
types: ['image'],
select: () => {},
...options
});
},
return this;
/**
* Close the asset manager.
* @example
* assetManager.close();
*/
close() {
const cmd = this.em.get('Commands');
cmd.stop(assetCmd);
},
/**
* Checks if the asset manager is open
* @returns {Boolean}
* @example
* assetManager.isOpen(); // true | false
*/
isOpen() {
const cmd = this.em.get('Commands');
return !!(cmd && cmd.isActive(assetCmd));
},
/**
* Add new asset/s to the collection. URLs are supposed to be unique
* @param {string|Object|Array<string>|Array<Object>} asset URL strings or an objects representing the resource.
* @param {String|Object|Array<String>|Array<Object>} asset URL strings or an objects representing the resource.
* @param {Object} [opts] Options
* @return {Model}
* @returns {[Asset]}
* @example
* // In case of strings, would be interpreted as images
* // As strings
* assetManager.add('http://img.jpg');
* assetManager.add(['http://img.jpg', './path/to/img.png']);
*
* // Using objects you could indicate the type and other meta informations
* // Using objects you can indicate the type and other meta informations
* assetManager.add({
* // type: 'image', // image is default
* src: 'http://img.jpg',
* //type: 'image', //image is default
* height: 300,
* width: 200,
* });
* assetManager.add([{
* src: 'http://img.jpg',
* },{
* src: './path/to/img.png',
* }]);
* assetManager.add([{ src: 'img2.jpg' }, { src: 'img2.png' }]);
*/
add(asset, opts = {}) {
// Put the model at the beginning
@ -128,19 +230,19 @@ export default () => {
},
/**
* Returns the asset by URL
* @param {string} src URL of the asset
* @return {Object} Object representing the asset
* Return asset by URL
* @param {String} src URL of the asset
* @returns {[Asset]|null}
* @example
* var asset = assetManager.get('http://img.jpg');
* const asset = assetManager.get('http://img.jpg');
*/
get(src) {
return assets.where({ src })[0];
return assets.where({ src })[0] || null;
},
/**
* Return the global collection, containing all the assets
* @return {Collection}
* @returns {Collection<[Asset]>}
*/
getAll() {
return assets;
@ -148,35 +250,36 @@ export default () => {
/**
* Return the visible collection, which contains assets actually rendered
* @return {Collection}
* @returns {Collection<[Asset]>}
*/
getAllVisible() {
return assetsVis;
},
/**
* Remove the asset by its URL
* @param {string} src URL of the asset
* @return {this}
* Remove asset
* @param {String|[Asset]} asset Asset or asset URL
* @returns {[Asset]} Removed asset
* @example
* assetManager.remove('http://img.jpg');
* const removed = assetManager.remove('http://img.jpg');
* // or by passing the Asset
* const asset = assetManager.get('http://img.jpg');
* assetManager.remove(asset);
*/
remove(src) {
var asset = this.get(src);
this.getAll().remove(asset);
return this;
remove(asset, opts) {
return this.__remove(asset, opts);
},
/**
* Store assets data to the selected storage
* @param {Boolean} noStore If true, won't store
* @return {Object} Data to store
* @returns {Object} Data to store
* @example
* var assets = assetManager.store();
*/
store(noStore) {
var obj = {};
var assets = JSON.stringify(this.getAll().toJSON());
const obj = {};
const assets = JSON.stringify(this.getAll().toJSON());
obj[this.storageKey] = assets;
if (!noStore && c.stm) c.stm.store(obj);
return obj;
@ -186,7 +289,7 @@ export default () => {
* Load data from the passed object.
* The fetched data will be added to the collection.
* @param {Object} data Object of data to load
* @return {Object} Loaded assets
* @returns {Object} Loaded assets
* @example
* var assets = assetManager.load({
* assets: [...]
@ -212,15 +315,17 @@ export default () => {
/**
* Return the Asset Manager Container
* @return {HTMLElement}
* @returns {HTMLElement}
*/
getContainer() {
return am.el;
const bhv = this.__getBehaviour();
return bhv.container || (am && am.el);
},
/**
* Get assets element container
* @return {HTMLElement}
* @returns {HTMLElement}
* @private
*/
getAssetsEl() {
return am.el.querySelector('[data-el=assets]');
@ -229,7 +334,7 @@ export default () => {
/**
* Render assets
* @param {array} assets Assets to render, without the argument will render all global assets
* @return {HTMLElement}
* @returns {HTMLElement}
* @example
* // Render all assets
* assetManager.render();
@ -241,17 +346,23 @@ export default () => {
* ));
*/
render(assts) {
if (this.getConfig('custom')) return;
const toRender = assts || this.getAll().models;
if (!am) {
const obj = {
collection: assetsVis, // Collection visible in asset manager
globalCollection: assets,
config: c
config: c,
module: this
};
fu = new FileUpload(obj);
obj.fu = fu;
am = new AssetsView(obj);
const el = am && am.el;
am = new AssetsView({
el,
...obj
});
am.render();
}
@ -266,6 +377,7 @@ export default () => {
* `model` (business logic), `view` (presentation logic)
* and `isType` function which recognize the type of the
* passed entity
* @private
* @example
* assetManager.addType('my-type', {
* model: {},
@ -280,7 +392,8 @@ export default () => {
/**
* Get type
* @param {string} id Type ID
* @return {Object} Type definition
* @returns {Object} Type definition
* @private
*/
getType(id) {
return this.getAll().getType(id);
@ -288,7 +401,8 @@ export default () => {
/**
* Get types
* @return {Array}
* @returns {Array}
* @private
*/
getTypes() {
return this.getAll().getTypes();
@ -306,6 +420,9 @@ export default () => {
onLoad() {
this.getAll().reset(c.assets);
const { em, events } = this;
em.on(`run:${assetCmd}`, () => this.__propEv(events.open));
em.on(`stop:${assetCmd}`, () => this.__propEv(events.close));
},
postRender(editorView) {
@ -348,12 +465,26 @@ export default () => {
c.onDblClick = func;
},
__behaviour(opts = {}) {
return (this._bhv = {
...(this._bhv || {}),
...opts
});
},
__getBehaviour(opts = {}) {
return this._bhv || {};
},
destroy() {
assets.stopListening();
assetsVis.stopListening();
assets.reset();
assetsVis.reset();
fu && fu.remove();
am && am.remove();
[assets, am, fu].forEach(i => (i = null));
this._bhv = {};
c = {};
}
};

75
src/asset_manager/model/Asset.js

@ -1,32 +1,75 @@
import Backbone from 'backbone';
import { result } from 'underscore';
import { Model } from 'common';
export default Backbone.Model.extend({
idAttribute: 'src',
/**
* @property {String} type Asset type, eg. 'image'.
* @property {String} src Asset URL, eg. 'https://.../image.png'.
*/
export default class Asset extends Model {
defaults() {
return {
type: '',
src: ''
};
}
/**
* Get asset type.
* @returns {String}
* @example
* // Asset: { src: 'https://.../image.png', type: 'image' }
* asset.getType(); // -> 'image'
* */
getType() {
return this.get('type');
}
defaults: {
type: '',
src: ''
},
/**
* Get asset URL.
* @returns {String}
* @example
* // Asset: { src: 'https://.../image.png' }
* asset.getSrc(); // -> 'https://.../image.png'
* */
getSrc() {
return this.get('src');
}
/**
* Get filename of the asset
* @return {string}
* @private
* Get filename of the asset (based on `src`).
* @returns {String}
* @example
* // Asset: { src: 'https://.../image.png' }
* asset.getFilename(); // -> 'image.png'
* // Asset: { src: 'https://.../image' }
* asset.getFilename(); // -> 'image'
* */
getFilename() {
return this.get('src')
.split('/')
.pop();
},
.pop()
.split('?')
.shift();
}
/**
* Get extension of the asset
* @return {string}
* @private
* Get extension of the asset (based on `src`).
* @returns {String}
* @example
* // Asset: { src: 'https://.../image.png' }
* asset.getExtension(); // -> 'png'
* // Asset: { src: 'https://.../image' }
* asset.getExtension(); // -> ''
* */
getExtension() {
return this.getFilename()
.split('.')
.pop();
}
});
}
Asset.prototype.idAttribute = 'src';
Asset.getDefaults = function() {
return result(this.prototype, 'defaults');
};

18
src/asset_manager/model/AssetImage.js

@ -1,11 +1,13 @@
import Asset from './Asset';
export default Asset.extend({
defaults: {
...Asset.prototype.defaults,
type: 'image',
unitDim: 'px',
height: 0,
width: 0
export default class AssetImage extends Asset {
defaults() {
return {
...Asset.getDefaults(),
type: 'image',
unitDim: 'px',
height: 0,
width: 0
};
}
});
}

10
src/asset_manager/view/AssetImageView.js

@ -43,12 +43,15 @@ export default AssetView.extend({
* */
onClick() {
const { model, pfx } = this;
const { select } = this.__getBhv();
const { onClick } = this.config;
const coll = this.collection;
coll.trigger('deselectAll');
this.$el.addClass(pfx + 'highlight');
if (isFunction(onClick)) {
if (isFunction(select)) {
select(model, false);
} else if (isFunction(onClick)) {
onClick(model);
} else {
this.updateTarget(coll.target);
@ -61,10 +64,13 @@ export default AssetView.extend({
* */
onDblClick() {
const { em, model } = this;
const { select } = this.__getBhv();
const { onDblClick } = this.config;
const { target, onSelect } = this.collection;
if (isFunction(onDblClick)) {
if (isFunction(select)) {
select(model, true);
} else if (isFunction(onDblClick)) {
onDblClick(model);
} else {
this.updateTarget(target);

6
src/asset_manager/view/AssetView.js

@ -17,6 +17,12 @@ export default Backbone.View.extend({
init && init(o);
},
__getBhv() {
const { em } = this;
const am = em && em.get('AssetManager');
return (am && am.__getBehaviour()) || {};
},
template() {
const pfx = this.pfx;
return `

20
src/asset_manager/view/FileUploader.js

@ -26,6 +26,7 @@ export default Backbone.View.extend(
initialize(opts = {}) {
this.options = opts;
const c = opts.config || {};
this.module = c.module;
this.config = c;
this.em = this.config.em;
this.pfx = c.stylePrefix || '';
@ -54,8 +55,8 @@ export default Backbone.View.extend(
* @private
*/
onUploadStart() {
const em = this.config.em;
em && em.trigger('asset:upload:start');
const { module } = this;
module && module.__propEv('asset:upload:start');
},
/**
@ -64,9 +65,8 @@ export default Backbone.View.extend(
* @private
*/
onUploadEnd(res) {
const { $el, config } = this;
const em = config.em;
em && em.trigger('asset:upload:end', res);
const { $el, module } = this;
module && module.__propEv('asset:upload:end', res);
const input = $el.find('input');
input && input.val('');
},
@ -77,10 +77,10 @@ export default Backbone.View.extend(
* @private
*/
onUploadError(err) {
const em = this.config.em;
const { module } = this;
console.error(err);
this.onUploadEnd(err);
em && em.trigger('asset:upload:error', err);
module && module.__propEv('asset:upload:error', err);
},
/**
@ -89,9 +89,7 @@ export default Backbone.View.extend(
* @private
*/
onUploadResponse(text, clb) {
const em = this.config.em;
const config = this.config;
const target = this.target;
const { module, config, target } = this;
let json;
try {
json = typeof text === 'string' ? JSON.parse(text) : text;
@ -99,7 +97,7 @@ export default Backbone.View.extend(
json = text;
}
em && em.trigger('asset:upload:response', json);
module && module.__propEv('asset:upload:response', json);
if (config.autoAdd && target) {
target.add(json.data, { at: 0 });

7
src/canvas/view/CanvasView.js

@ -6,7 +6,8 @@ import {
getElement,
getKeyChar,
isTextNode,
getElRect
getElRect,
getUiClass
} from 'utils/mixins';
import FramesView from './FramesView';
@ -353,7 +354,7 @@ export default Backbone.View.extend({
},
render() {
const { el, $el, ppfx, config } = this;
const { el, $el, ppfx, config, em } = this;
$el.html(this.template());
const $frames = $el.find('[data-frames]');
this.framesArea = $frames.get(0);
@ -387,7 +388,7 @@ export default Backbone.View.extend({
this.fixedOffsetEl = el.querySelector(`.${ppfx}offset-fixed-v`);
this.toolsGlobEl = el.querySelector(`.${ppfx}tools-gl`);
this.toolsEl = toolsEl;
this.el.className = this.className;
this.el.className = getUiClass(em, this.className);
this.ready = 1;
this._renderFrames();

76
src/commands/view/OpenAssets.js

@ -1,36 +1,76 @@
import { isFunction } from 'underscore';
import { createEl } from '../../utils/dom';
export default {
open(content) {
const { editor, title, config, am } = this;
const { custom } = config;
if (isFunction(custom.open)) {
return custom.open(am.__customData());
}
const { Modal } = editor;
Modal.open({ title, content }).onceClose(() => editor.stopCommand(this.id));
},
close() {
const { custom } = this.config;
if (isFunction(custom.close)) {
return custom.close(this.am.__customData());
}
const { Modal } = this.editor;
Modal && Modal.close();
},
run(editor, sender, opts = {}) {
const modal = editor.Modal;
const am = editor.AssetManager;
const config = am.getConfig();
const title = opts.modalTitle || editor.t('assetManager.modalTitle') || '';
const types = opts.types;
const accept = opts.accept;
const { types = [], accept, select } = opts;
this.title = opts.modalTitle || editor.t('assetManager.modalTitle') || '';
this.editor = editor;
this.config = config;
this.am = am;
am.setTarget(opts.target);
am.onClick(opts.onClick);
am.onDblClick(opts.onDblClick);
am.onSelect(opts.onSelect);
am.__behaviour({
select,
types,
options: opts
});
if (!this.rendered || types) {
let assets = am.getAll().filter(i => i);
if (config.custom) {
this.rendered = this.rendered || createEl('div');
this.rendered.className = `${config.stylePrefix}custom-wrp`;
am.__behaviour({ container: this.rendered });
am.__trgCustom();
} else {
if (!this.rendered || types) {
let assets = am.getAll().filter(i => i);
if (types && types.length) {
assets = assets.filter(a => types.indexOf(a.get('type')) !== -1);
}
if (types && types.length) {
assets = assets.filter(a => types.indexOf(a.get('type')) !== -1);
}
am.render(assets);
this.rendered = am.getContainer();
}
am.render(assets);
this.rendered = am.getContainer();
}
if (accept) {
const uploadEl = this.rendered.querySelector(
`input#${config.stylePrefix}uploadFile`
);
uploadEl && uploadEl.setAttribute('accept', accept);
if (accept) {
const uploadEl = this.rendered.querySelector(
`input#${config.stylePrefix}uploadFile`
);
uploadEl && uploadEl.setAttribute('accept', accept);
}
}
modal.open({ title, content: this.rendered });
this.open(this.rendered);
return this;
},
stop(editor) {
this.editor = editor;
this.close(this.rendered);
}
};

5
src/common/module.js

@ -13,7 +13,7 @@ export default {
getAllMap() {
return this.getAll().reduce((acc, i) => {
acc[i.get('id')] = i;
acc[i.get(i.idAttribute)] = i;
return acc;
}, {});
},
@ -46,8 +46,11 @@ export default {
const { em, events } = this;
const options = opts || coll;
em && events.all && em.trigger(events.all, { event, model, options });
this.__onAllEvent();
},
__onAllEvent() {},
_createId(len = 16) {
const all = this.getAll();
const ln = all.length + len;

16
src/dom_components/view/ComponentImageView.js

@ -63,17 +63,17 @@ export default ComponentView.extend({
onActive(ev) {
ev && ev.stopPropagation();
const { em, model } = this;
const ed = em && em.get('Editor');
const am = em && em.get('AssetManager');
if (ed && model.get('editable')) {
ed.runCommand('open-assets', {
if (am && model.get('editable')) {
am.open({
select(asset, complete) {
model.set({ src: asset.getSrc() });
complete && am.close();
},
target: model,
types: ['image'],
accept: 'image/*',
onSelect() {
ed.Modal.close();
ed.AssetManager.setTarget(null);
}
accept: 'image/*'
});
}
},

6
src/editor/config/config.js

@ -316,5 +316,9 @@ export default {
keepUnusedStyles: 0,
// TODO
multiFrames: 0
multiFrames: 0,
// Experimental: don't use.
// Avoid default UI styles
customUI: false
};

14
src/editor/index.js

@ -42,13 +42,6 @@
* * `block:drag:start` - Started dragging block, model of the block is passed as an argument
* * `block:drag` - Dragging block, the block's model and the drag event are passed as arguments
* * `block:drag:stop` - Dragging of the block is stopped. As agruments for the callback you get, the dropped component model (if dropped successfully) and the model of the block
* ### Assets
* * `asset:add` - New asset added
* * `asset:remove` - Asset removed
* * `asset:upload:start` - Before the upload is started
* * `asset:upload:end` - After the upload is ended
* * `asset:upload:error` - On any error in upload, passes the error as an argument
* * `asset:upload:response` - On upload response, passes the result as an argument
* ### Keymaps
* * `keymap:add` - New keymap added. The new keyamp object is passed as an argument
* * `keymap:remove` - Keymap removed. The removed keyamp object is passed as an argument
@ -85,9 +78,6 @@
* ### RTE
* * `rte:enable` - RTE enabled. The view, on which RTE is enabled, is passed as an argument
* * `rte:disable` - RTE disabled. The view, on which RTE is disabled, is passed as an argument
* ### Modal
* * `modal:open` - Modal is opened
* * `modal:close` - Modal is closed
* ### Commands
* * `run:{commandName}` - Triggered when some command is called to run (eg. editor.runCommand('preview'))
* * `stop:{commandName}` - Triggered when some command is called to stop (eg. editor.stopCommand('preview'))
@ -96,6 +86,10 @@
* * `abort:{commandName}` - Triggered when the command execution is aborted (`editor.on(`run:preview:before`, opts => opts.abort = 1);`)
* * `run` - Triggered on run of any command. The id and the result are passed as arguments to the callback
* * `stop` - Triggered on stop of any command. The id and the result are passed as arguments to the callback
* ### Assets
* Check the [Assets](/api/assets.html) module.
* ### Modal
* Check the [Modal](/api/modal_dialog.html) module.
* ### Devices
* Check the [Devices](/api/device_manager.html) module.
* ### Parser

3
src/modal_dialog/config/config.js

@ -7,6 +7,9 @@ export default {
backdrop: true,
// Avoid rendering the default modal.
custom: false,
/**
* Extend ModalView object (view/ModalView.js)
* @example

99
src/modal_dialog/index.js

@ -14,6 +14,12 @@
* const modal = editor.Modal;
* ```
*
* ## Available Events
* * `modal:open` - Modal is opened
* * `modal:close` - Modal is closed
* * `modal` - Event triggered on any change related to the modal. An object containing all the available data about the triggered event is passed as an argument to the callback.
*
* ## Methods
* * [open](#open)
* * [close](#close)
* * [isOpen](#isopen)
@ -27,6 +33,8 @@
* @module Modal
*/
import { debounce, isFunction, isString } from 'underscore';
import { createText } from '../utils/dom';
import defaults from './config/config';
import ModalM from './model/Modal';
import ModalView from './view/ModalView';
@ -69,13 +77,36 @@ export default () => {
model = new ModalM(c);
model.on('change:open', (m, enb) => triggerEvent(enb, em));
model.on(
'change',
debounce(() => {
const data = this._evData();
const { custom } = this.getConfig();
isFunction(custom) && custom(data);
em.trigger('modal', data);
})
);
return this;
},
_evData() {
const titl = this.getTitle();
const cnt = this.getContent();
const { open, attributes } = model.attributes;
return {
open,
attributes,
title: isString(titl) ? createText(titl) : titl,
content: isString(cnt) ? createText(cnt) : cnt.get ? cnt.get(0) : cnt,
close: () => this.close()
};
},
postRender(view) {
const el = view.model.getConfig().el || view.el;
this.render().appendTo(el);
const res = this.render();
res && res.appendTo(el);
},
/**
@ -84,19 +115,29 @@ export default () => {
* @param {String|HTMLElement} [opts.title] Title to set for the modal
* @param {String|HTMLElement} [opts.content] Content to set for the modal
* @param {Object} [opts.attributes] Updates the modal wrapper with custom attributes
* @return {this}
* @returns {this}
* @example
* modal.open({
* title: 'My title',
* content: 'My content',
* attributes: { class: 'my-class' },
* });
*/
open(opts = {}) {
const attr = opts.attributes || {};
opts.title && this.setTitle(opts.title);
opts.content && this.setContent(opts.content);
model.set('attributes', attr);
model.open();
modal && modal.updateAttr(opts.attributes);
modal && modal.updateAttr(attr);
return this;
},
/**
* Close the modal window
* @return {this}
* @returns {this}
* @example
* modal.close();
*/
close() {
model.close();
@ -106,8 +147,12 @@ export default () => {
/**
* Execute callback when the modal will be closed.
* The callback will be called one only time
* @param {Function} clb
* @param {Function} clb Callback to call
* @returns {this}
* @example
* modal.onceClose(() => {
* console.log('The modal is closed');
* });
*/
onceClose(clb) {
this.em.once('modal:close', clb);
@ -117,8 +162,12 @@ export default () => {
/**
* Execute callback when the modal will be opened.
* The callback will be called one only time
* @param {Function} clb
* @param {Function} clb Callback to call
* @returns {this}
* @example
* modal.onceOpen(() => {
* console.log('The modal is opened');
* });
*/
onceOpen(clb) {
this.em.once('modal:open', clb);
@ -127,7 +176,9 @@ export default () => {
/**
* Checks if the modal window is open
* @return {Boolean}
* @returns {Boolean}
* @example
* modal.isOpen(); // true | false
*/
isOpen() {
return !!model.get('open');
@ -135,10 +186,15 @@ export default () => {
/**
* Set the title to the modal window
* @param {string} title Title
* @return {this}
* @param {string | HTMLElement} title Title
* @returns {this}
* @example
* modal.setTitle('New title');
* // pass a string
* modal.setTitle('Some title');
* // or an HTMLElement
* const el = document.createElement('div');
* el.innerText = 'New title';
* modal.setTitle(el);
*/
setTitle(title) {
model.set('title', title);
@ -147,7 +203,9 @@ export default () => {
/**
* Returns the title of the modal window
* @return {string}
* @returns {string | HTMLElement}
* @example
* modal.getTitle();
*/
getTitle() {
return model.get('title');
@ -155,10 +213,15 @@ export default () => {
/**
* Set the content of the modal window
* @param {string|HTMLElement} content Content
* @return {this}
* @param {string | HTMLElement} content Content
* @returns {this}
* @example
* modal.setContent('<div>Some HTML content</div>');
* // pass a string
* modal.setContent('Some content');
* // or an HTMLElement
* const el = document.createElement('div');
* el.innerText = 'New content';
* modal.setContent(el);
*/
setContent(content) {
model.set('content', ' ');
@ -168,7 +231,9 @@ export default () => {
/**
* Get the content of the modal window
* @return {string}
* @returns {string | HTMLElement}
* @example
* modal.getContent();
*/
getContent() {
return model.get('content');
@ -198,9 +263,11 @@ export default () => {
* @private
*/
render() {
if (this.getConfig().custom) return;
const View = ModalView.extend(c.extend);
modal && modal.remove();
const el = modal && modal.el;
modal = new View({
el,
model,
config: c
});

1
src/modal_dialog/model/Modal.js

@ -5,6 +5,7 @@ export default class Modal extends Model {
return {
title: '',
content: '',
attributes: {},
open: false
};
}

8
src/modal_dialog/view/ModalView.js

@ -70,9 +70,9 @@ export default class ModalView extends View {
* @return {HTMLElement}
* @private
*/
getTitle() {
getTitle(opts = {}) {
if (!this.$title) this.$title = this.$el.find('.' + this.pfx + 'title');
return this.$title.get(0);
return opts.$ ? this.$title : this.$title.get(0);
}
/**
@ -93,8 +93,8 @@ export default class ModalView extends View {
* @private
* */
updateTitle() {
var title = this.getTitle();
if (title) title.innerHTML = this.model.get('title');
const title = this.getTitle({ $: true });
title && title.empty().append(this.model.get('title'));
}
/**

3
src/style_manager/index.js

@ -422,8 +422,9 @@ export default () => {
* @private
* */
render() {
SectView && SectView.remove();
const el = SectView && SectView.el;
SectView = new SectorsView({
el,
collection: sectors,
target: c.em,
config: c

22
src/style_manager/view/PropertyFileView.js

@ -121,21 +121,19 @@ export default PropertyView.extend({
* @return void
* */
openAssetManager() {
const { em, modal } = this;
const editor = em ? em.get('Editor') : '';
const { em } = this;
const am = em && em.get('AssetManager');
const handleAsset = a => this.spreadUrl(isString(a) ? a : a.get('src'));
if (editor) {
editor.runCommand('open-assets', {
types: ['image'],
accept: 'image/*',
target: this.getTargetModel(),
onClick: handleAsset,
onDblClick(asset) {
modal.close();
am &&
am.open({
select(asset, complete) {
handleAsset(asset);
}
complete && am.close();
},
target: this.getTargetModel(),
types: ['image'],
accept: 'image/*'
});
}
}
});

6
src/styles/scss/_gjs_canvas.scss

@ -203,6 +203,12 @@ $guide_pad: 5px !default;
left: 0;
top: $canvasTop;
&.#{$cui-cls} {
width: 100%;
height: 100%;
top: 0;
}
&#{gjs-is(grab)},
&#{gjs-is(grabbing)} {
.#{$cv-prefix}canvas__frames {

1
src/styles/scss/_gjs_variables.scss

@ -13,6 +13,7 @@ $sm-prefix: $app-prefix + 'sm-' !default;
$cv-prefix: $app-prefix + 'cv-' !default;
$clm-prefix: $app-prefix + 'clm-' !default;
$trt-prefix: $app-prefix + 'trt-' !default;
$cui-cls: $app-prefix + 'cui' !default;
/*
New Pattern Color System

29
src/trait_manager/index.js

@ -10,7 +10,6 @@ import TraitButtonView from './view/TraitButtonView';
export default () => {
let c = {};
let types = {};
let TraitsViewer;
const typesDef = {
text: TraitView,
@ -24,8 +23,6 @@ export default () => {
return {
TraitsView,
types,
/**
* Name of the module
* @type {String}
@ -50,7 +47,7 @@ export default () => {
c = config;
defaults(c, defaultOpts);
const ppfx = c.pStylePrefix;
types = { ...typesDef };
this.types = { ...typesDef };
ppfx && (c.stylePrefix = `${ppfx}${c.stylePrefix}`);
return this;
},
@ -79,8 +76,8 @@ export default () => {
* @param {Object} methods Object representing the trait
*/
addType(name, trait) {
const baseView = types.text;
types[name] = baseView.extend(trait);
const baseView = this.getType('text');
this.types[name] = baseView.extend(trait);
},
/**
@ -89,23 +86,33 @@ export default () => {
* @return {Object}
*/
getType(name) {
return types[name];
return this.getTypes()[name];
},
/**
* Get all trait types
* @returns {Object}
*/
getTypes() {
return this.types;
},
render() {
TraitsViewer && TraitsViewer.remove();
const el = TraitsViewer && TraitsViewer.el;
TraitsViewer = new TraitsView({
el,
collection: [],
editor: c.em,
config: c
});
TraitsViewer.itemsView = types;
return TraitsViewer.render().el;
TraitsViewer.itemsView = this.getTypes();
TraitsViewer.updatedCollection();
return TraitsViewer.el;
},
destroy() {
TraitsViewer && TraitsViewer.remove();
[c, types, TraitsViewer].forEach(i => (i = {}));
[c, TraitsViewer].forEach(i => (i = {}));
}
};
};

18
src/trait_manager/view/TraitsView.js

@ -8,14 +8,14 @@ export default DomainViews.extend({
initialize(o = {}) {
const config = o.config || {};
const pfx = config.stylePrefix || '';
const em = o.editor;
this.config = config;
this.em = o.editor;
this.pfx = config.stylePrefix || '';
this.em = em;
this.pfx = pfx;
this.ppfx = config.pStylePrefix || '';
this.className = this.pfx + 'traits';
const toListen = 'component:toggled';
this.listenTo(this.em, toListen, this.updatedCollection);
this.updatedCollection();
this.className = `${pfx}traits`;
this.listenTo(em, 'component:toggled', this.updatedCollection);
},
/**
@ -23,9 +23,9 @@ export default DomainViews.extend({
* @private
*/
updatedCollection() {
const ppfx = this.ppfx;
const comp = this.em.getSelected();
this.el.className = `${this.className} ${ppfx}one-bg ${ppfx}two-color`;
const { ppfx, className, em } = this;
const comp = em.getSelected();
this.el.className = `${className} ${ppfx}one-bg ${ppfx}two-color`;
this.collection = comp ? comp.get('traits') : [];
this.render();
}

2
src/utils/dom.js

@ -68,6 +68,8 @@ export const createEl = (tag, attrs = '', child) => {
return el;
};
export const createText = str => document.createTextNode(str);
// Unfortunately just creating `KeyboardEvent(e.type, e)` is not enough,
// the keyCode/which will be always `0`. Even if it's an old/deprecated
// property keymaster (and many others) still use it... using `defineProperty`

5
src/utils/mixins.js

@ -11,6 +11,11 @@ const matches =
elProt.mozMatchesSelector ||
elProt.msMatchesSelector;
export const getUiClass = (em, defCls) => {
const { stylePrefix, customUI } = em.getConfig();
return [customUI && `${stylePrefix}cui`, defCls].filter(i => i).join(' ');
};
/**
* Import styles asynchronously
* @param {String|Array<String>} styles

29
test/specs/grapesjs/headless.js

@ -41,6 +41,35 @@ describe('GrapesJS Headless', () => {
expect(editor.getCss()).toBe(''); // same as default
});
test('Add components with children', () => {
const res = editor.addComponents([
{
tagName: 'h1',
type: 'text',
components: [
{
type: 'textnode',
removable: false,
draggable: false,
highlightable: 0,
copyable: false,
selectable: true,
content: 'Hello!',
_innertext: false
}
]
}
]);
expect(res.length).toBe(1);
const resHtml = '<h1>Hello!</h1>';
const comp = res[0];
expect(comp.toHTML()).toBe(resHtml);
expect(editor.Selectors.getAll().length).toBe(0);
expect(editor.Css.getAll().length).toBe(0);
expect(editor.getHtml()).toBe(resHtml);
expect(editor.getCss()).toBe('');
});
test('Add styles', () => {
const res = editor.addStyle(styleObj);
expect(res.length).toBe(1);

708
yarn.lock

File diff suppressed because it is too large
Loading…
Cancel
Save