Browse Source

development environment upgrades (#6059)

* init work to upgrade node and linting

* feat: add jest-environment-jsdom

* test: refactor remove unused before

* fix: add option chain for frame

* test: use correct jest spy

* skip jsdom broken tests

* config: *

* config: ignore dist in linting

* format: apply transformations

* remove repeated config
pull/6068/head
Daniel Starns 1 year ago
committed by GitHub
parent
commit
1b8ce38f10
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 18
      .editorconfig
  2. 26
      .eslintrc
  3. 56
      .eslintrc.js
  4. 4
      .github/ISSUE_TEMPLATE/1.bug_report.yml
  5. 2
      .github/ISSUE_TEMPLATE/config.yml
  6. 3
      .github/lock.yml
  7. 9
      .github/workflows/publish.yml
  8. 18
      .github/workflows/quailty.yml
  9. 2
      .prettierignore
  10. 10
      .prettierrc
  11. 20
      CODE_OF_CONDUCT.md
  12. 11
      CONTRIBUTING.md
  13. 179
      README.md
  14. 8
      babel.config.js
  15. 14
      docs/.vuepress/components/Demo.vue
  16. 22
      docs/.vuepress/components/DemoBasicBlocks.vue
  17. 10
      docs/.vuepress/components/DemoCanvasOnly.vue
  18. 56
      docs/.vuepress/components/DemoCustomPanels.vue
  19. 50
      docs/.vuepress/components/DemoDevices.vue
  20. 25
      docs/.vuepress/components/DemoLayers.vue
  21. 40
      docs/.vuepress/components/DemoStyle.vue
  22. 50
      docs/.vuepress/components/DemoTheme.vue
  23. 40
      docs/.vuepress/components/DemoTraits.vue
  24. 74
      docs/.vuepress/components/DemoViewer.vue
  25. 174
      docs/.vuepress/components/demos/utils.js
  26. 47
      docs/.vuepress/config.js
  27. 4
      docs/.vuepress/enhanceApp.js
  28. 32437
      docs/.vuepress/public/grapes.min.js
  29. 4
      docs/.vuepress/theme/index.js
  30. 25
      docs/.vuepress/theme/layouts/CarbonAds.vue
  31. 8
      docs/.vuepress/theme/layouts/Layout.vue
  32. 35
      docs/Home.md
  33. 49
      docs/README.md
  34. 159
      docs/api.js
  35. 246
      docs/getting-started.md
  36. 43
      docs/guides/Custom-CSS-parser.md
  37. 19
      docs/guides/Replace-Rich-Text-Editor.md
  38. 51
      docs/guides/Symbols.md
  39. 150
      docs/modules/Assets.md
  40. 97
      docs/modules/Blocks.md
  41. 65
      docs/modules/Canvas.md
  42. 83
      docs/modules/Commands.md
  43. 53
      docs/modules/Components-js.md
  44. 287
      docs/modules/Components.md
  45. 76
      docs/modules/I18n.md
  46. 29
      docs/modules/Layers.md
  47. 44
      docs/modules/Modal.md
  48. 64
      docs/modules/Pages.md
  49. 39
      docs/modules/Plugins.md
  50. 44
      docs/modules/Selectors.md
  51. 81
      docs/modules/Storage.md
  52. 135
      docs/modules/Style-manager.md
  53. 197
      docs/modules/Traits.md
  54. 82
      index.html
  55. 12
      jest.config.js
  56. 65
      package.json
  57. 14
      src/abstract/Module.ts
  58. 4
      src/abstract/ModuleCategories.ts
  59. 4
      src/abstract/ModuleCategory.ts
  60. 4
      src/abstract/ModuleCollection.ts
  61. 4
      src/abstract/ModuleDomainViews.ts
  62. 2
      src/abstract/ModuleModel.ts
  63. 9
      src/abstract/ModuleView.ts
  64. 2
      src/asset_manager/view/AssetsView.ts
  65. 20
      src/asset_manager/view/FileUploader.ts
  66. 22
      src/block_manager/index.ts
  67. 2
      src/block_manager/view/BlocksView.ts
  68. 6
      src/canvas/index.ts
  69. 2
      src/canvas/model/Canvas.ts
  70. 6
      src/canvas/model/Frame.ts
  71. 8
      src/canvas/model/Frames.ts
  72. 6
      src/canvas/view/CanvasView.ts
  73. 45
      src/canvas/view/FrameView.ts
  74. 8
      src/canvas/view/FrameWrapView.ts
  75. 4
      src/canvas/view/FramesView.ts
  76. 20
      src/code_manager/model/CssGenerator.ts
  77. 20
      src/commands/index.ts
  78. 2
      src/commands/view/CanvasMove.ts
  79. 2
      src/commands/view/ComponentDelete.ts
  80. 30
      src/commands/view/ComponentDrag.ts
  81. 2
      src/commands/view/ComponentEnter.ts
  82. 2
      src/commands/view/ComponentExit.ts
  83. 2
      src/commands/view/ComponentNext.ts
  84. 2
      src/commands/view/ComponentPrev.ts
  85. 4
      src/commands/view/ComponentStyleClear.ts
  86. 2
      src/commands/view/CopyComponent.ts
  87. 2
      src/commands/view/OpenAssets.ts
  88. 12
      src/commands/view/PasteComponent.ts
  89. 2
      src/commands/view/Preview.ts
  90. 8
      src/commands/view/SelectComponent.ts
  91. 4
      src/commands/view/SelectPosition.ts
  92. 2
      src/common/index.ts
  93. 12
      src/css_composer/index.ts
  94. 12
      src/css_composer/model/CssRule.ts
  95. 6
      src/css_composer/view/CssRulesView.ts
  96. 4
      src/device_manager/index.ts
  97. 2
      src/device_manager/model/Device.ts
  98. 2
      src/device_manager/view/DevicesView.ts
  99. 14
      src/dom_components/index.ts
  100. 82
      src/dom_components/model/Component.ts

18
.editorconfig

@ -1,18 +0,0 @@
root = true
[*]
end_of_line = lf
insert_final_newline = true
[*.js]
charset = utf-8
indent_style = space
indent_size = 2
quote_type = single
[*.ts]
charset = utf-8
indent_style = space
indent_size = 2
quote_type = single
max_line_length = 120

26
.eslintrc

@ -1,26 +0,0 @@
{
"env": {
"browser": true,
"node": true
},
"parserOptions": {
"ecmaVersion": "latest",
"sourceType": "module"
},
"rules": {
"strict": 0,
"quotes": ["warn", "single", { "avoidEscape": true }],
"eol-last": ["warn", "always"],
"no-mixed-requires": [0],
"no-underscore-dangle": [0]
},
"overrides": [{
"files": ["*.ts"],
"parser": "@typescript-eslint/parser"
}],
"ignorePatterns": [
"dist/*",
"locale/*",
"private/*"
]
}

56
.eslintrc.js

@ -0,0 +1,56 @@
module.exports = {
root: true, // Add this to indicate this is the root ESLint configuration
env: {
browser: true,
node: true,
'jest/globals': true,
},
globals: {
$: true,
grapesjs: true,
},
extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
plugins: ['@typescript-eslint', 'jest'],
parserOptions: {
ecmaVersion: 2021,
sourceType: 'module',
},
rules: {
'no-var': 'off',
'prefer-const': 'off',
'no-prototype-builtins': 'off',
'no-useless-escape': 'off',
'prefer-rest-params': 'off',
'no-empty': 'off',
'prefer-spread': 'off',
'no-extra-boolean-cast': 'off',
'no-unsafe-optional-chaining': 'off',
'no-shadow-restricted-names': 'off',
'no-cond-assign': 'off',
'no-fallthrough': 'off',
'no-sparse-arrays': 'off',
'no-redeclare': 'off',
'no-control-regex': 'off',
'no-constant-condition': 'off',
'no-misleading-character-class': 'off',
'no-undef': 'off',
'no-func-assign': 'off',
'no-regex-spaces': 'off',
'@typescript-eslint/ban-ts-comment': 'off',
'@typescript-eslint/no-require-imports': 'off',
'@typescript-eslint/no-explicit-any': 'off',
'@typescript-eslint/no-unused-vars': 'off',
'@typescript-eslint/no-unused-expressions': 'off',
'@typescript-eslint/no-unnecessary-type-const': 'off',
'@typescript-eslint/no-empty-object-type': 'off',
'@typescript-eslint/no-unnecessary-type-constraint': 'off',
'@typescript-eslint/no-this-alias': 'off',
'@typescript-eslint/no-unsafe-function-type': 'off',
'@typescript-eslint/no-non-null-asserted-optional-chain': 'off',
'@typescript-eslint/no-wrapper-object-types': 'off',
'linebreak-style': ['error', 'unix'],
'max-len': ['error', { code: 300 }],
'no-multiple-empty-lines': ['error', { max: 1, maxEOF: 1 }],
},
ignorePatterns: ['docs/api/*', 'dist/*'],
};

4
.github/ISSUE_TEMPLATE/1.bug_report.yml

@ -1,6 +1,6 @@
name: 🐞 Bug report
description: Create a bug report for GrapesJS.
title: "BUG: "
title: 'BUG: '
labels: []
body:
- type: markdown
@ -61,4 +61,4 @@ body:
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
required: true

2
.github/ISSUE_TEMPLATE/config.yml

@ -8,4 +8,4 @@ contact_links:
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!"
about: "Have something nice to say or share about GrapesJS? We'd love to hear it!"

3
.github/lock.yml

@ -23,7 +23,6 @@ lockComment: >
# Assign `resolved` as the reason for locking. Set to `false` to disable
setLockReason: true
# Limit to only `issues` or `pulls`
# only: issues
@ -37,4 +36,4 @@ setLockReason: true
# daysUntilLock: 30
# Repository to extend settings from
# _extends: repo
# _extends: repo

9
.github/workflows/publish.yml

@ -9,15 +9,14 @@ jobs:
if: ${{ false }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/setup-node@v3
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '12.x'
node-version: '20.x'
registry-url: 'https://registry.npmjs.org'
cache: 'yarn'
- run: yarn --frozen-lockfile
- run: yarn build
- run: npm run lint
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}

18
.github/workflows/build.yml → .github/workflows/quailty.yml

@ -2,27 +2,29 @@ name: Tests
on:
push:
branches: [ dev ]
branches: [dev]
pull_request:
branches: [ dev ]
branches: [dev]
jobs:
build:
quailty:
runs-on: ubuntu-latest
strategy:
matrix:
node-version: [14.17, 16.x]
node-version: [18.x, 20.x]
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Use Node.js ${{ matrix.node-version }}
uses: actions/setup-node@v3
uses: actions/setup-node@v4
with:
node-version: ${{ matrix.node-version }}
cache: 'yarn'
- run: yarn --frozen-lockfile
- run: yarn build
- run: npm run lint
- run: npm run lint:ts
- run: npm run format:check
- run: yarn build
- run: npm run ts:build:check
- run: npm test
- run: npm test

2
.prettierignore

@ -0,0 +1,2 @@
docs/api/*.md
dist/

10
.prettierrc

@ -0,0 +1,10 @@
{
"endOfLine": "lf",
"insertPragma": false,
"requirePragma": false,
"trailingComma": "all",
"tabWidth": 2,
"useTabs": false,
"singleQuote": true,
"printWidth": 120
}

20
CODE_OF_CONDUCT.md

@ -8,19 +8,19 @@ In the interest of fostering an open and welcoming environment, we as contributo
Examples of behavior that contributes to creating a positive environment include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a professional setting
- The use of sexualized language or imagery and unwelcome sexual attention or advances
- Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic address, without explicit permission
- Other conduct which could reasonably be considered inappropriate in a professional setting
## Our Responsibilities

11
CONTRIBUTING.md

@ -5,12 +5,12 @@
First of all, thank you for considering contributing to GrapesJS!
We welcome any type of contribution, not only code. Like for example:
- **QA**: file bug reports, the more details you can give the better (e.g. screenshots with the console open)
- **Marketing**: writing blog posts, howto's, tutorials, etc.
- **Community**: presenting the project at meetups, organizing a dedicated meetup for the local community, etc.
- **Money**: We welcome financial contributions in full transparency on our [Open Collective].
## Setting up the repository
This is a Node.js project and you need to have Node.js installed on your machine. You can download it from [here](https://nodejs.org/). We test versions 14 and 16 of Node in the CI, so it's recommended to use one of these versions, or the latest of: 16.20.2
@ -45,7 +45,6 @@ Navigate to `http://localhost:8080/` to see the editor in action. The developmen
Working on your first Pull Request? You can learn how from this **free** series, [How to Contribute to an Open Source Project on GitHub](https://app.egghead.io/playlists/how-to-contribute-to-an-open-source-project-on-github).
## Submitting code
Any code change should be submitted as a pull request. Before start working on something make always a search in opened issues and pull requests, this might help you to avoid wasting time.
@ -54,29 +53,23 @@ A pull request could be a bug fix, new feature and much more, but in all cases,
The title should be brief but comprehensive, the description contains a link to the opened issue and the proposed solution. The pull request should contain tests whenever possible. Keep in mind that the bigger is the pull request, the longer it will take to review and merge. Try to break down large pull requests in smaller chunks that are easier to review and merge.
## Styleguide
The code is auto formatted with [prettier](https://github.com/prettier/prettier) on any commit, therefore you can write in any style you prefer
## Expenses
Anyone can file an expense (code, marketing, etc.) via our [Open Collective]. If the expense makes sense for the development of the community, it will be "merged" in the ledger of our open collective by the core contributors and the person who filed the expense will be reimbursed.
Before submitting an expense contact core contributors via the current active chat room ([Discord](https://discord.gg/QAbgGXq)) and explain your intents
## Questions
If you have any questions, create an [issue](https://github.com/GrapesJS/grapesjs/issues) (protip: do a quick search first to see if someone else didn't ask the same question before!).
## Credits
Thank you to all the people who have already contributed to GrapesJS!
<a href="/GrapesJS/grapesjs/graphs/contributors"><img src="https://opencollective.com/grapesjs/contributors.svg?width=890" /></a>
[Open Collective]: <https://opencollective.com/grapesjs>
[Open Collective]: https://opencollective.com/grapesjs

179
README.md

@ -5,10 +5,8 @@
[![CDNJS](https://img.shields.io/cdnjs/v/grapesjs.svg)](https://cdnjs.com/libraries/grapesjs)
[![npm](https://img.shields.io/npm/v/grapesjs.svg)](https://www.npmjs.com/package/grapesjs)
<p align="center"><img src="http://grapesjs.com/assets/images/grapesjs-front-page-m.jpg" alt="GrapesJS" width="500" align="center"/></p>
GrapesJS is a free and open source Web Builder Framework which helps building HTML templates, faster and easily, to be delivered in sites, newsletters or mobile apps. Mainly, GrapesJS was designed to be used inside a [CMS] to speed up the creation of dynamic templates. To better understand this concept check the image below
<br/>
@ -21,86 +19,70 @@ This demos show examples of what is possible to achieve:<br/>
Webpage Demo - http://grapesjs.com/demo.html<br/>
Newsletter Demo - http://grapesjs.com/demo-newsletter-editor.html<br/>
## Table of contents
* [Features](#features)
* [Download](#download)
* [Usage](#usage)
* [Development](#development)
* [Documentation](#documentation)
* [API](#api)
* [Testing](#testing)
* [Plugins](#plugins)
* [Support](#support)
* [Changelog](https://github.com/GrapesJS/grapesjs/releases)
* [Contributing](https://github.com/GrapesJS/grapesjs/blob/master/CONTRIBUTING.md)
* [License](#license)
- [Features](#features)
- [Download](#download)
- [Usage](#usage)
- [Development](#development)
- [Documentation](#documentation)
- [API](#api)
- [Testing](#testing)
- [Plugins](#plugins)
- [Support](#support)
- [Changelog](https://github.com/GrapesJS/grapesjs/releases)
- [Contributing](https://github.com/GrapesJS/grapesjs/blob/master/CONTRIBUTING.md)
- [License](#license)
## Features
| Blocks | Style Manager | Layer Manager |
|--|--|--|
|<img src="http://grapesjs.com/assets/images/sc-grapesjs-blocks-prp.jpg" alt="GrapesJS - Block Manager" height="400" align="center"/>|<img src="http://grapesjs.com/assets/images/sc-grapesjs-style-2.jpg" alt="GrapesJS - Style Manager" height="400" align="center"/>|<img src="http://grapesjs.com/assets/images/sc-grapesjs-layers-2.jpg" alt="GrapesJS - Layer Manager" height="400" align="center"/>|
| Code Viewer | Asset Manager |
|--|--|
|<img src="http://grapesjs.com/assets/images/sc-grapesjs-code.jpg" alt="GrapesJS - Code Viewer" height="300" align="center"/>|<img src="http://grapesjs.com/assets/images/sc-grapesjs-assets-1.jpg" alt="GrapesJS - Asset Manager" height="250" align="center"/>|
* Local and remote storage
* Default built-in commands (basically for creating and managing different components)
| Blocks | Style Manager | Layer Manager |
| ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| <img src="http://grapesjs.com/assets/images/sc-grapesjs-blocks-prp.jpg" alt="GrapesJS - Block Manager" height="400" align="center"/> | <img src="http://grapesjs.com/assets/images/sc-grapesjs-style-2.jpg" alt="GrapesJS - Style Manager" height="400" align="center"/> | <img src="http://grapesjs.com/assets/images/sc-grapesjs-layers-2.jpg" alt="GrapesJS - Layer Manager" height="400" align="center"/> |
| Code Viewer | Asset Manager |
| -------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| <img src="http://grapesjs.com/assets/images/sc-grapesjs-code.jpg" alt="GrapesJS - Code Viewer" height="300" align="center"/> | <img src="http://grapesjs.com/assets/images/sc-grapesjs-assets-1.jpg" alt="GrapesJS - Asset Manager" height="250" align="center"/> |
- Local and remote storage
- Default built-in commands (basically for creating and managing different components)
## Download
* CDNs
* UNPKG (resolves to the latest version)
* `https://unpkg.com/grapesjs`
* `https://unpkg.com/grapesjs/dist/css/grapes.min.css`
* CDNJS (replace `X.X.X` with the current version)
* `https://cdnjs.cloudflare.com/ajax/libs/grapesjs/X.X.X/grapes.min.js`
* `https://cdnjs.cloudflare.com/ajax/libs/grapesjs/X.X.X/css/grapes.min.css`
* NPM
* `npm i grapesjs`
* GIT
* `git clone https://github.com/GrapesJS/grapesjs.git`
- CDNs
- UNPKG (resolves to the latest version)
- `https://unpkg.com/grapesjs`
- `https://unpkg.com/grapesjs/dist/css/grapes.min.css`
- CDNJS (replace `X.X.X` with the current version)
- `https://cdnjs.cloudflare.com/ajax/libs/grapesjs/X.X.X/grapes.min.js`
- `https://cdnjs.cloudflare.com/ajax/libs/grapesjs/X.X.X/css/grapes.min.css`
- NPM
- `npm i grapesjs`
- GIT
- `git clone https://github.com/GrapesJS/grapesjs.git`
For the development purpose you should follow instructions below.
## Usage
```html
<link rel="stylesheet" href="path/to/grapes.min.css">
<link rel="stylesheet" href="path/to/grapes.min.css" />
<script src="path/to/grapes.min.js"></script>
<div id="gjs"></div>
<script type="text/javascript">
var editor = grapesjs.init({
container : '#gjs',
components: '<div class="txt-red">Hello world!</div>',
style: '.txt-red{color: red}',
container: '#gjs',
components: '<div class="txt-red">Hello world!</div>',
style: '.txt-red{color: red}',
});
</script>
```
For a more practical example I'd suggest looking up the code inside this demo: http://grapesjs.com/demo.html
## Development
Clone the repository and install all the necessary dependencies (`yarn` is highly recommended)
@ -119,86 +101,65 @@ $ yarn start
Once the development server is started you should be able to reach the demo page (eg. `http://localhost:8080`)
## Documentation
Check the getting started guide here: [Documentation]
## API
API References could be found here: [API-Reference]
## Testing
```sh
$ yarn test
```
## Plugins
[Official Plugins](https://github.com/orgs/GrapesJS/repositories?q=-repo%3Agrapesjs%2Fgrapesjs&type=source) | [Community Plugins](https://github.com/topics/grapesjs-plugin)
### Wrappers
* [@grapesjs/react](https://github.com/GrapesJS/react) - GrapesJS wrapper for React that allows you to build custom and declarative UI for your editor.
- [@grapesjs/react](https://github.com/GrapesJS/react) - GrapesJS wrapper for React that allows you to build custom and declarative UI for your editor.
### Extensions
* [grapesjs-plugin-export](https://github.com/GrapesJS/export) - Export GrapesJS templates in a zip archive
* [grapesjs-plugin-filestack](https://github.com/GrapesJS/filestack) - Add Filestack uploader in Asset Manager
* [grapesjs-plugin-ckeditor](https://github.com/GrapesJS/ckeditor) - Replaces the built-in RTE with CKEditor
* [grapesjs-tui-image-editor](https://github.com/GrapesJS/tui-image-editor) - GrapesJS TOAST UI Image Editor
* [grapesjs-blocks-basic](https://github.com/GrapesJS/blocks-basic) - Basic set of blocks
* [grapesjs-plugin-forms](https://github.com/GrapesJS/components-forms) - Set of form components and blocks
* [grapesjs-navbar](https://github.com/GrapesJS/components-navbar) - Simple navbar component
* [grapesjs-component-countdown](https://github.com/GrapesJS/components-countdown) - Simple countdown component
* [grapesjs-style-gradient](https://github.com/GrapesJS/style-gradient) - Add `gradient` type input to the Style Manager
* [grapesjs-style-filter](https://github.com/GrapesJS/style-filter) - Add `filter` type input to the Style Manager
* [grapesjs-style-bg](https://github.com/GrapesJS/style-bg) - Full-stack background style property type, with the possibility to add images, colors, and gradients
* [grapesjs-blocks-flexbox](https://github.com/GrapesJS/blocks-flexbox) - Add the flexbox block
* [grapesjs-lory-slider](https://github.com/GrapesJS/components-lory) - Slider component by using [lory](https://github.com/meandmax/lory)
* [grapesjs-tabs](https://github.com/GrapesJS/components-tabs) - Simple tabs component
* [grapesjs-tooltip](https://github.com/GrapesJS/components-tooltip) - Simple, CSS only, tooltip component for GrapesJS
* [grapesjs-custom-code](https://github.com/GrapesJS/components-custom-code) - Embed custom code
* [grapesjs-touch](https://github.com/GrapesJS/touch) - Enable touch support
* [grapesjs-indexeddb](https://github.com/GrapesJS/storage-indexeddb) - Storage wrapper for IndexedDB
* [grapesjs-firestore](https://github.com/GrapesJS/storage-firestore) - Storage wrapper for [Cloud Firestore](https://firebase.google.com/docs/firestore)
* [grapesjs-parser-postcss](https://github.com/GrapesJS/parser-postcss) - Custom CSS parser for GrapesJS by using [PostCSS](https://github.com/postcss/postcss)
* [grapesjs-typed](https://github.com/GrapesJS/components-typed) - Typed component made by wrapping Typed.js library
* [grapesjs-ui-suggest-classes](https://github.com/silexlabs/grapesjs-ui-suggest-classes) - Enable auto-complete of classes in the SelectorManager UI
* [grapesjs-fonts](https://github.com/silexlabs/grapesjs-fonts) - Custom Fonts plugin, adds a UI to manage fonts in websites
* [grapesjs-symbols](https://github.com/silexlabs/grapesjs-symbols) - Symbols plugin to reuse elements in a website and accross pages
* [grapesjs-click](https://github.com/bgrand-ch/grapesjs-click) - Grab and drop blocks and components with click (no more drag-and-drop)
* [grapesjs-float](https://github.com/bgrand-ch/grapesjs-float) - Anchor a floating element next to another element (selected component, ...)
- [grapesjs-plugin-export](https://github.com/GrapesJS/export) - Export GrapesJS templates in a zip archive
- [grapesjs-plugin-filestack](https://github.com/GrapesJS/filestack) - Add Filestack uploader in Asset Manager
- [grapesjs-plugin-ckeditor](https://github.com/GrapesJS/ckeditor) - Replaces the built-in RTE with CKEditor
- [grapesjs-tui-image-editor](https://github.com/GrapesJS/tui-image-editor) - GrapesJS TOAST UI Image Editor
- [grapesjs-blocks-basic](https://github.com/GrapesJS/blocks-basic) - Basic set of blocks
- [grapesjs-plugin-forms](https://github.com/GrapesJS/components-forms) - Set of form components and blocks
- [grapesjs-navbar](https://github.com/GrapesJS/components-navbar) - Simple navbar component
- [grapesjs-component-countdown](https://github.com/GrapesJS/components-countdown) - Simple countdown component
- [grapesjs-style-gradient](https://github.com/GrapesJS/style-gradient) - Add `gradient` type input to the Style Manager
- [grapesjs-style-filter](https://github.com/GrapesJS/style-filter) - Add `filter` type input to the Style Manager
- [grapesjs-style-bg](https://github.com/GrapesJS/style-bg) - Full-stack background style property type, with the possibility to add images, colors, and gradients
- [grapesjs-blocks-flexbox](https://github.com/GrapesJS/blocks-flexbox) - Add the flexbox block
- [grapesjs-lory-slider](https://github.com/GrapesJS/components-lory) - Slider component by using [lory](https://github.com/meandmax/lory)
- [grapesjs-tabs](https://github.com/GrapesJS/components-tabs) - Simple tabs component
- [grapesjs-tooltip](https://github.com/GrapesJS/components-tooltip) - Simple, CSS only, tooltip component for GrapesJS
- [grapesjs-custom-code](https://github.com/GrapesJS/components-custom-code) - Embed custom code
- [grapesjs-touch](https://github.com/GrapesJS/touch) - Enable touch support
- [grapesjs-indexeddb](https://github.com/GrapesJS/storage-indexeddb) - Storage wrapper for IndexedDB
- [grapesjs-firestore](https://github.com/GrapesJS/storage-firestore) - Storage wrapper for [Cloud Firestore](https://firebase.google.com/docs/firestore)
- [grapesjs-parser-postcss](https://github.com/GrapesJS/parser-postcss) - Custom CSS parser for GrapesJS by using [PostCSS](https://github.com/postcss/postcss)
- [grapesjs-typed](https://github.com/GrapesJS/components-typed) - Typed component made by wrapping Typed.js library
- [grapesjs-ui-suggest-classes](https://github.com/silexlabs/grapesjs-ui-suggest-classes) - Enable auto-complete of classes in the SelectorManager UI
- [grapesjs-fonts](https://github.com/silexlabs/grapesjs-fonts) - Custom Fonts plugin, adds a UI to manage fonts in websites
- [grapesjs-symbols](https://github.com/silexlabs/grapesjs-symbols) - Symbols plugin to reuse elements in a website and accross pages
- [grapesjs-click](https://github.com/bgrand-ch/grapesjs-click) - Grab and drop blocks and components with click (no more drag-and-drop)
- [grapesjs-float](https://github.com/bgrand-ch/grapesjs-float) - Anchor a floating element next to another element (selected component, ...)
### Presets
* [grapesjs-preset-webpage](https://github.com/GrapesJS/preset-webpage) - Webpage Builder
* [grapesjs-preset-newsletter](https://github.com/GrapesJS/preset-newsletter) - Newsletter Builder
* [grapesjs-mjml](https://github.com/GrapesJS/mjml) - Newsletter Builder with MJML components
- [grapesjs-preset-webpage](https://github.com/GrapesJS/preset-webpage) - Webpage Builder
- [grapesjs-preset-newsletter](https://github.com/GrapesJS/preset-newsletter) - Newsletter Builder
- [grapesjs-mjml](https://github.com/GrapesJS/mjml) - Newsletter Builder with MJML components
Find out more about plugins here: [Creating plugins](https://grapesjs.com/docs/modules/Plugins.html)
## Support
If you like the project and you wish to see it grow, please consider supporting us with a donation of your choice or become a backer/sponsor via [Open Collective](https://opencollective.com/grapesjs)
@ -214,12 +175,10 @@ If you like the project and you wish to see it grow, please consider supporting
[![BrowserStack](https://user-images.githubusercontent.com/11614725/39406324-4ef89c40-4bb5-11e8-809a-113d9432e5a5.png)](https://www.browserstack.com)<br/>
Thanks to [BrowserStack](https://www.browserstack.com) for providing us browser testing services
## License
BSD 3-clause
[Documentation]: <https://grapesjs.com/docs/>
[API-Reference]: <https://grapesjs.com/docs/api/>
[CMS]: <https://en.wikipedia.org/wiki/Content_management_system>
[Documentation]: https://grapesjs.com/docs/
[API-Reference]: https://grapesjs.com/docs/api/
[CMS]: https://en.wikipedia.org/wiki/Content_management_system

8
babel.config.js

@ -0,0 +1,8 @@
/** @type {import('@babel/core')} */
module.exports = {
env: {
test: {
presets: ['@babel/preset-env', '@babel/preset-typescript'],
},
},
};

14
docs/.vuepress/components/Demo.vue

@ -1,14 +1,14 @@
<template>
<div class="demo-container">
<slot/>
<slot />
</div>
</template>
<style scoped>
.demo-container {
min-height: 20px;
border: 1px solid #eee;
border-radius: 2px;
padding: 25px 35px;
}
.demo-container {
min-height: 20px;
border: 1px solid #eee;
border-radius: 2px;
padding: 25px 35px;
}
</style>

22
docs/.vuepress/components/DemoBasicBlocks.vue

@ -13,18 +13,18 @@ import utils from './demos/utils.js';
export default {
mounted() {
window.editor2 = grapesjs.init(utils.gjsConfigBlocks);
}
}
},
};
</script>
<style>
.gjs {
border: 3px solid #444;
box-sizing: border-box;
}
.gjs-block {
width: auto;
height: auto;
min-height: auto;
}
.gjs {
border: 3px solid #444;
box-sizing: border-box;
}
.gjs-block {
width: auto;
height: auto;
min-height: auto;
}
</style>

10
docs/.vuepress/components/DemoCanvasOnly.vue

@ -1,5 +1,4 @@
<template src="./demos/DemoCanvasOnly.html">
</template>
<template src="./demos/DemoCanvasOnly.html"></template>
<script>
import utils from './demos/utils.js';
@ -7,9 +6,8 @@ import utils from './demos/utils.js';
export default {
mounted() {
const editor = grapesjs.init(utils.gjsConfigStart);
}
}
},
};
</script>
<style src="./demos/DemoCanvasOnly.css">
</style>
<style src="./demos/DemoCanvasOnly.css"></style>

56
docs/.vuepress/components/DemoCustomPanels.vue

@ -1,7 +1,7 @@
<template>
<div>
<div class="panel__top" id="panel__top3">
<div class="panel__basic-actions" id="panel__basic-actions3"></div>
<div class="panel__basic-actions" id="panel__basic-actions3"></div>
</div>
<div class="gjs" id="gjs3">
<h1>Hello World Component!</h1>
@ -16,33 +16,37 @@ import utils from './demos/utils.js';
export default {
mounted() {
const editor3 = grapesjs.init(utils.gjsConfigPanels);
editor3.Panels.addPanel(Object.assign({}, utils.panelTop, {
el: '#panel__top3'
}));
editor3.Panels.addPanel(Object.assign({}, utils.panelBasicActions, {
el: '#panel__basic-actions3'
}));
editor3.Panels.addPanel(
Object.assign({}, utils.panelTop, {
el: '#panel__top3',
}),
);
editor3.Panels.addPanel(
Object.assign({}, utils.panelBasicActions, {
el: '#panel__basic-actions3',
}),
);
window.editor3 = editor3;
}
}
},
};
</script>
<style>
.panel__top {
padding: 0;
width: 100%;
display: flex;
position: initial;
justify-content: center;
justify-content: space-between;
}
.panel__basic-actions {
position: initial;
}
.content pre {
padding-top: 0;
padding-bottom: 0;
margin-top: 0;
margin-bottom: 0;
}
.panel__top {
padding: 0;
width: 100%;
display: flex;
position: initial;
justify-content: center;
justify-content: space-between;
}
.panel__basic-actions {
position: initial;
}
.content pre {
padding-top: 0;
padding-bottom: 0;
margin-top: 0;
margin-bottom: 0;
}
</style>

50
docs/.vuepress/components/DemoDevices.vue

@ -1,9 +1,9 @@
<template>
<div>
<div class="panel__top" id="panel__top7">
<div class="panel__basic-actions" id="panel__basic-actions7"></div>
<div class="panel__devices" id="panel__devices7"></div>
<div class="panel__switcher" id="panel__switcher7"></div>
<div class="panel__basic-actions" id="panel__basic-actions7"></div>
<div class="panel__devices" id="panel__devices7"></div>
<div class="panel__switcher" id="panel__switcher7"></div>
</div>
<div class="editor-row">
@ -29,24 +29,34 @@ import utils from './demos/utils.js';
export default {
mounted() {
const editor7 = grapesjs.init(utils.gjsConfigDevices);
editor7.Panels.addPanel(Object.assign({}, utils.panelTop, {
el: '#panel__top7'
}));
editor7.Panels.addPanel(Object.assign({}, utils.panelBasicActions, {
el: '#panel__basic-actions7'
}));
editor7.Panels.addPanel(Object.assign({}, utils.panelSidebar, {
el: '#panel__right7'
}));
editor7.Panels.addPanel(Object.assign({}, utils.panelSwitcherTraits, {
el: '#panel__switcher7'
}));
editor7.Panels.addPanel(Object.assign({}, utils.panelDevices, {
el: '#panel__devices7'
}));
editor7.Panels.addPanel(
Object.assign({}, utils.panelTop, {
el: '#panel__top7',
}),
);
editor7.Panels.addPanel(
Object.assign({}, utils.panelBasicActions, {
el: '#panel__basic-actions7',
}),
);
editor7.Panels.addPanel(
Object.assign({}, utils.panelSidebar, {
el: '#panel__right7',
}),
);
editor7.Panels.addPanel(
Object.assign({}, utils.panelSwitcherTraits, {
el: '#panel__switcher7',
}),
);
editor7.Panels.addPanel(
Object.assign({}, utils.panelDevices, {
el: '#panel__devices7',
}),
);
window.editor7 = editor7;
}
}
},
};
</script>
<style>

25
docs/.vuepress/components/DemoLayers.vue

@ -1,7 +1,7 @@
<template>
<div>
<div class="panel__top" id="panel__top4">
<div class="panel__basic-actions" id="panel__basic-actions4"></div>
<div class="panel__basic-actions" id="panel__basic-actions4"></div>
</div>
<div class="editor-row">
@ -25,16 +25,19 @@ import utils from './demos/utils.js';
export default {
mounted() {
const editor4 = grapesjs.init(utils.gjsConfigLayers);
editor4.Panels.addPanel(Object.assign({}, utils.panelTop, {
el: '#panel__top4'
}));
editor4.Panels.addPanel(Object.assign({}, utils.panelBasicActions, {
el: '#panel__basic-actions4'
}));
editor4.Panels.addPanel(
Object.assign({}, utils.panelTop, {
el: '#panel__top4',
}),
);
editor4.Panels.addPanel(
Object.assign({}, utils.panelBasicActions, {
el: '#panel__basic-actions4',
}),
);
window.editor4 = editor4;
}
}
},
};
</script>
<style src="./demos/DemoLayers.css">
</style>
<style src="./demos/DemoLayers.css"></style>

40
docs/.vuepress/components/DemoStyle.vue

@ -1,8 +1,8 @@
<template>
<div>
<div class="panel__top" id="panel__top5">
<div class="panel__basic-actions" id="panel__basic-actions5"></div>
<div class="panel__switcher" id="panel__switcher5"></div>
<div class="panel__basic-actions" id="panel__basic-actions5"></div>
<div class="panel__switcher" id="panel__switcher5"></div>
</div>
<div class="editor-row">
@ -27,21 +27,29 @@ import utils from './demos/utils.js';
export default {
mounted() {
const editor5 = grapesjs.init(utils.gjsConfigStyle);
editor5.Panels.addPanel(Object.assign({}, utils.panelTop, {
el: '#panel__top5'
}));
editor5.Panels.addPanel(Object.assign({}, utils.panelBasicActions, {
el: '#panel__basic-actions5'
}));
editor5.Panels.addPanel(Object.assign({}, utils.panelSidebar, {
el: '#panel__right5'
}));
editor5.Panels.addPanel(Object.assign({}, utils.panelSwitcher, {
el: '#panel__switcher5'
}));
editor5.Panels.addPanel(
Object.assign({}, utils.panelTop, {
el: '#panel__top5',
}),
);
editor5.Panels.addPanel(
Object.assign({}, utils.panelBasicActions, {
el: '#panel__basic-actions5',
}),
);
editor5.Panels.addPanel(
Object.assign({}, utils.panelSidebar, {
el: '#panel__right5',
}),
);
editor5.Panels.addPanel(
Object.assign({}, utils.panelSwitcher, {
el: '#panel__switcher5',
}),
);
window.editor5 = editor5;
}
}
},
};
</script>
<style>

50
docs/.vuepress/components/DemoTheme.vue

@ -1,9 +1,9 @@
<template>
<div class="gjs__themed">
<div class="panel__top" id="panel__top8">
<div class="panel__basic-actions" id="panel__basic-actions8"></div>
<div class="panel__devices" id="panel__devices8"></div>
<div class="panel__switcher" id="panel__switcher8"></div>
<div class="panel__basic-actions" id="panel__basic-actions8"></div>
<div class="panel__devices" id="panel__devices8"></div>
<div class="panel__switcher" id="panel__switcher8"></div>
</div>
<div class="editor-row">
@ -29,24 +29,34 @@ import utils from './demos/utils.js';
export default {
mounted() {
const editor8 = grapesjs.init(utils.gjsConfigTheme);
editor8.Panels.addPanel(Object.assign({}, utils.panelTop, {
el: '#panel__top8'
}));
editor8.Panels.addPanel(Object.assign({}, utils.panelBasicActionsIcons, {
el: '#panel__basic-actions8'
}));
editor8.Panels.addPanel(Object.assign({}, utils.panelSidebar, {
el: '#panel__right8'
}));
editor8.Panels.addPanel(Object.assign({}, utils.panelSwitcherTraitsIcons, {
el: '#panel__switcher8'
}));
editor8.Panels.addPanel(Object.assign({}, utils.panelDevicesIcons, {
el: '#panel__devices8'
}));
editor8.Panels.addPanel(
Object.assign({}, utils.panelTop, {
el: '#panel__top8',
}),
);
editor8.Panels.addPanel(
Object.assign({}, utils.panelBasicActionsIcons, {
el: '#panel__basic-actions8',
}),
);
editor8.Panels.addPanel(
Object.assign({}, utils.panelSidebar, {
el: '#panel__right8',
}),
);
editor8.Panels.addPanel(
Object.assign({}, utils.panelSwitcherTraitsIcons, {
el: '#panel__switcher8',
}),
);
editor8.Panels.addPanel(
Object.assign({}, utils.panelDevicesIcons, {
el: '#panel__devices8',
}),
);
window.editor8 = editor8;
}
}
},
};
</script>
<style lang="stylus">

40
docs/.vuepress/components/DemoTraits.vue

@ -1,8 +1,8 @@
<template>
<div>
<div class="panel__top" id="panel__top6">
<div class="panel__basic-actions" id="panel__basic-actions6"></div>
<div class="panel__switcher" id="panel__switcher6"></div>
<div class="panel__basic-actions" id="panel__basic-actions6"></div>
<div class="panel__switcher" id="panel__switcher6"></div>
</div>
<div class="editor-row">
@ -28,21 +28,29 @@ import utils from './demos/utils.js';
export default {
mounted() {
const editor6 = grapesjs.init(utils.gjsConfigTraits);
editor6.Panels.addPanel(Object.assign({}, utils.panelTop, {
el: '#panel__top6'
}));
editor6.Panels.addPanel(Object.assign({}, utils.panelBasicActions, {
el: '#panel__basic-actions6'
}));
editor6.Panels.addPanel(Object.assign({}, utils.panelSidebar, {
el: '#panel__right6'
}));
editor6.Panels.addPanel(Object.assign({}, utils.panelSwitcherTraits, {
el: '#panel__switcher6'
}));
editor6.Panels.addPanel(
Object.assign({}, utils.panelTop, {
el: '#panel__top6',
}),
);
editor6.Panels.addPanel(
Object.assign({}, utils.panelBasicActions, {
el: '#panel__basic-actions6',
}),
);
editor6.Panels.addPanel(
Object.assign({}, utils.panelSidebar, {
el: '#panel__right6',
}),
);
editor6.Panels.addPanel(
Object.assign({}, utils.panelSwitcherTraits, {
el: '#panel__switcher6',
}),
);
window.editor6 = editor6;
}
}
},
};
</script>
<style>

74
docs/.vuepress/components/DemoViewer.vue

@ -1,43 +1,43 @@
<template>
<iframe :width="width" :height="height" :src="src" allowfullscreen="allowfullscreen" frameborder="0"/>
<iframe :width="width" :height="height" :src="src" allowfullscreen="allowfullscreen" frameborder="0" />
</template>
<script>
export default {
name: 'DemoViewer',
props: {
value: {
type: String,
default: '',
},
user: {
type: String,
default: 'artur_arseniev',
},
width: {
type: String,
default: '100%',
},
height: {
type: String,
default: '300',
},
darkcode: {
type: Boolean,
default: false,
},
show: {
type: Boolean,
default: false,
},
name: 'DemoViewer',
props: {
value: {
type: String,
default: '',
},
computed: {
src() {
const { value, user, darkcode, show } = this;
const tabs = show ? 'result,js,html,css' : 'js,html,css,result';
const dcStr = darkcode ? '/dark/?menuColor=fff&fontColor=333&accentColor=e67891' : '';
return `//jsfiddle.net/${user}/${value}/embedded/${tabs}${dcStr}`;
}
}
}
</script>
user: {
type: String,
default: 'artur_arseniev',
},
width: {
type: String,
default: '100%',
},
height: {
type: String,
default: '300',
},
darkcode: {
type: Boolean,
default: false,
},
show: {
type: Boolean,
default: false,
},
},
computed: {
src() {
const { value, user, darkcode, show } = this;
const tabs = show ? 'result,js,html,css' : 'js,html,css,result';
const dcStr = darkcode ? '/dark/?menuColor=fff&fontColor=333&accentColor=e67891' : '';
return `//jsfiddle.net/${user}/${value}/embedded/${tabs}${dcStr}`;
},
},
};
</script>

174
docs/.vuepress/components/demos/utils.js

@ -1,20 +1,21 @@
export const loadScript = url => new Promise((resolve, reject) => {
const script = document.createElement('script');
script.onload = resolve;
script.onerror = reject;
script.src = url;
document.head.appendChild(script);
});
export const loadStyle = url => new Promise((resolve) => {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = url;
document.head.appendChild(link);
resolve();
});
export const loadScript = (url) =>
new Promise((resolve, reject) => {
const script = document.createElement('script');
script.onload = resolve;
script.onerror = reject;
script.src = url;
document.head.appendChild(script);
});
export const loadStyle = (url) =>
new Promise((resolve) => {
const link = document.createElement('link');
link.rel = 'stylesheet';
link.type = 'text/css';
link.href = url;
document.head.appendChild(link);
resolve();
});
// Don't know yet why but can't use ES6
@ -24,16 +25,18 @@ var blockManager = {
{
id: 'section', // id is mandatory
label: '<b>Section</b>',
attributes: { class:'gjs-block-section' },
attributes: { class: 'gjs-block-section' },
content: `<section>
<h1>This is a simple title</h1>
<div>This is just a Lorem text: Lorem ipsum dolor sit amet, consectetur adipiscing elit</div>
</section>`,
}, {
},
{
id: 'text',
label: 'Text',
content: '<div data-gjs-type="text">Insert your text here</div>',
}, {
},
{
id: 'image',
label: 'Image',
// Select the component once dropped in canavas
@ -43,26 +46,30 @@ var blockManager = {
content: { type: 'image' },
// This triggers `active` on dropped components
activate: true,
}
]
},
],
};
var blockManagerIcons = Object.assign({}, blockManager, {
blocks: [
Object.assign({}, blockManager.blocks[0], {
label: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 3H5c-1.11 0-2 .89-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5a2 2 0 0 0-2-2m0 2v14H5V5h14z"></path></svg>',
label:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 3H5c-1.11 0-2 .89-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2V5a2 2 0 0 0-2-2m0 2v14H5V5h14z"></path></svg>',
}),
Object.assign({}, blockManager.blocks[1], {
label: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18.5 4l1.16 4.35-.96.26c-.45-.87-.91-1.74-1.44-2.18C16.73 6 16.11 6 15.5 6H13v10.5c0 .5 0 1 .33 1.25.34.25 1 .25 1.67.25v1H9v-1c.67 0 1.33 0 1.67-.25.33-.25.33-.75.33-1.25V6H8.5c-.61 0-1.23 0-1.76.43-.53.44-.99 1.31-1.44 2.18l-.96-.26L5.5 4h13z"></path></svg>',
label:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M18.5 4l1.16 4.35-.96.26c-.45-.87-.91-1.74-1.44-2.18C16.73 6 16.11 6 15.5 6H13v10.5c0 .5 0 1 .33 1.25.34.25 1 .25 1.67.25v1H9v-1c.67 0 1.33 0 1.67-.25.33-.25.33-.75.33-1.25V6H8.5c-.61 0-1.23 0-1.76.43-.53.44-.99 1.31-1.44 2.18l-.96-.26L5.5 4h13z"></path></svg>',
}),
Object.assign({}, blockManager.blocks[2], {
label: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 5c1.1 0 2 .9 2 2v10c0 1.1-.9 2-2 2H4a2 2 0 0 1-2-2V7c0-1.11.89-2 2-2h16M5 16h14l-4.5-6-3.5 4.5-2.5-3L5 16z"></path></svg>',
label:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 5c1.1 0 2 .9 2 2v10c0 1.1-.9 2-2 2H4a2 2 0 0 1-2-2V7c0-1.11.89-2 2-2h16M5 16h14l-4.5-6-3.5 4.5-2.5-3L5 16z"></path></svg>',
}),
]
],
});
var styleManager = {
sectors: [{
sectors: [
{
name: 'Dimension',
open: false,
// Use built-in properties
@ -78,9 +85,10 @@ var styleManager = {
units: ['px', '%'], // Units, available only for 'integer' types
defaults: 'auto', // Default value
min: 0, // Min value, available only for 'integer' types
}
]
},{
},
],
},
{
name: 'Extra',
open: false,
buildProps: ['background-color', 'box-shadow', 'custom-prop'],
@ -97,23 +105,27 @@ var styleManager = {
{ value: '18px', name: 'Medium' },
{ value: '32px', name: 'Big' },
],
}
]
}]
},
],
},
],
};
var layerManager = { scrollLayers: 0 };
var selectorManager = {};
var traitManager = {};
var deviceManager = {
devices: [{
devices: [
{
name: 'Desktop',
width: '', // default size
}, {
},
{
name: 'Mobile',
width: '320px', // this value will be used on canvas width
widthMedia: '480px', // this value will be used in CSS @media
}]
},
],
};
var panelTop = { id: 'panel-top' };
@ -128,40 +140,47 @@ var panelBasicActions = {
label: '<u>B</u>',
// Built-in command
command: 'sw-visibility',
}, {
},
{
id: 'export',
className: 'btn-open-export',
label: 'Exp',
command: 'export-template',
// For grouping context of buttons in the same panel
context: 'export-template',
}, {
},
{
id: 'show-json',
className: 'btn-show-json',
label: 'JSON',
context: 'show-json',
command(editor) {
editor.Modal.setTitle('Components JSON')
.setContent(`<textarea style="width:100%; height: 250px;">
.setContent(
`<textarea style="width:100%; height: 250px;">
${JSON.stringify(editor.getComponents())}
</textarea>`)
</textarea>`,
)
.open();
},
}
},
],
};
var panelBasicActionsIcons = Object.assign({}, panelBasicActions, {
buttons: [
Object.assign({}, panelBasicActions.buttons[0], {
label: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M15 5h2V3h-2m0 18h2v-2h-2M11 5h2V3h-2m8 2h2V3h-2m0 6h2V7h-2m0 14h2v-2h-2m0-6h2v-2h-2m0 6h2v-2h-2M3 5h2V3H3m0 6h2V7H3m0 6h2v-2H3m0 6h2v-2H3m0 6h2v-2H3m8 2h2v-2h-2m-4 2h2v-2H7M7 5h2V3H7v2z"></path></svg>',
label:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M15 5h2V3h-2m0 18h2v-2h-2M11 5h2V3h-2m8 2h2V3h-2m0 6h2V7h-2m0 14h2v-2h-2m0-6h2v-2h-2m0 6h2v-2h-2M3 5h2V3H3m0 6h2V7H3m0 6h2v-2H3m0 6h2v-2H3m0 6h2v-2H3m8 2h2v-2h-2m-4 2h2v-2H7M7 5h2V3H7v2z"></path></svg>',
}),
Object.assign({}, panelBasicActions.buttons[1], {
label: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5 20h14v-2H5m14-9h-4V3H9v6H5l7 7 7-7z"></path></svg>',
label:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M5 20h14v-2H5m14-9h-4V3H9v6H5l7 7 7-7z"></path></svg>',
}),
Object.assign({}, panelBasicActions.buttons[2], {
label: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8 3c-1.1 0-2 .9-2 2v4c0 1.1-.9 2-2 2H3v2h1c1.1 0 2 .9 2 2v4c0 1.1.9 2 2 2h2v-2H8v-5c0-1.1-.9-2-2-2 1.1 0 2-.9 2-2V5h2V3m6 0c1.1 0 2 .9 2 2v4c0 1.1.9 2 2 2h1v2h-1c-1.1 0-2 .9-2 2v4c0 1.1-.9 2-2 2h-2v-2h2v-5c0-1.1.9-2 2-2-1.1 0-2-.9-2-2V5h-2V3h2z"></path></svg>',
label:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M8 3c-1.1 0-2 .9-2 2v4c0 1.1-.9 2-2 2H3v2h1c1.1 0 2 .9 2 2v4c0 1.1.9 2 2 2h2v-2H8v-5c0-1.1-.9-2-2-2 1.1 0 2-.9 2-2V5h2V3m6 0c1.1 0 2 .9 2 2v4c0 1.1.9 2 2 2h1v2h-1c-1.1 0-2 .9-2 2v4c0 1.1-.9 2-2 2h-2v-2h2v-5c0-1.1.9-2 2-2-1.1 0-2-.9-2-2V5h-2V3h2z"></path></svg>',
}),
]
],
});
var panelSidebar = {
el: '#panel__right4',
@ -186,9 +205,15 @@ var buttonShowLayers = {
togglable: false,
label: 'Layers',
command: {
getRowEl(editor) { return editor.getContainer().parentNode.parentNode; },
getLayersEl(row) { return row.querySelector('.layers-container') },
getStyleEl(row) { return row.querySelector('.styles-container') },
getRowEl(editor) {
return editor.getContainer().parentNode.parentNode;
},
getLayersEl(row) {
return row.querySelector('.layers-container');
},
getStyleEl(row) {
return row.querySelector('.styles-container');
},
run(editor, sender) {
const row = this.getRowEl(editor);
@ -208,9 +233,15 @@ var buttonShowStyle = {
togglable: false,
active: true,
command: {
getRowEl(editor) { return editor.getContainer().parentNode.parentNode; },
getLayersEl(row) { return row.querySelector('.layers-container') },
getStyleEl(row) { return row.querySelector('.styles-container') },
getRowEl(editor) {
return editor.getContainer().parentNode.parentNode;
},
getLayersEl(row) {
return row.querySelector('.layers-container');
},
getStyleEl(row) {
return row.querySelector('.styles-container');
},
run(editor, sender) {
const row = this.getRowEl(editor);
@ -245,61 +276,62 @@ var buttonShowTraits = {
var panelSwitcher = {
id: 'panel-switcher',
buttons: [
buttonShowLayers,
buttonShowStyle,
],
buttons: [buttonShowLayers, buttonShowStyle],
};
var panelSwitcherTraits = {
id: 'panel-switcher',
buttons: [
buttonShowLayers,
buttonShowStyle,
buttonShowTraits,
],
buttons: [buttonShowLayers, buttonShowStyle, buttonShowTraits],
};
var panelSwitcherTraitsIcons = {
id: 'panel-switcher',
buttons: [
Object.assign({}, buttonShowLayers, {
label: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 16l7.36-5.73L21 9l-9-7-9 7 1.63 1.27M12 18.54l-7.38-5.73L3 14.07l9 7 9-7-1.63-1.27L12 18.54z"></path></svg>',
label:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 16l7.36-5.73L21 9l-9-7-9 7 1.63 1.27M12 18.54l-7.38-5.73L3 14.07l9 7 9-7-1.63-1.27L12 18.54z"></path></svg>',
}),
Object.assign({}, buttonShowStyle, {
label: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17.5 12c-.83 0-1.5-.67-1.5-1.5S16.67 9 17.5 9s1.5.67 1.5 1.5-.67 1.5-1.5 1.5m-3-4c-.83 0-1.5-.67-1.5-1.5S13.67 5 14.5 5s1.5.67 1.5 1.5S15.33 8 14.5 8m-5 0C8.67 8 8 7.33 8 6.5S8.67 5 9.5 5s1.5.67 1.5 1.5S10.33 8 9.5 8m-3 4c-.83 0-1.5-.67-1.5-1.5S5.67 9 6.5 9 8 9.67 8 10.5 7.33 12 6.5 12M12 3a9 9 0 0 0 0 18c.83 0 1.5-.67 1.5-1.5 0-.39-.15-.74-.39-1-.23-.27-.38-.62-.38-1 0-.83.67-1.5 1.5-1.5H16c2.76 0 5-2.24 5-5 0-4.42-4.03-8-9-8z"></path></svg>',
label:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M17.5 12c-.83 0-1.5-.67-1.5-1.5S16.67 9 17.5 9s1.5.67 1.5 1.5-.67 1.5-1.5 1.5m-3-4c-.83 0-1.5-.67-1.5-1.5S13.67 5 14.5 5s1.5.67 1.5 1.5S15.33 8 14.5 8m-5 0C8.67 8 8 7.33 8 6.5S8.67 5 9.5 5s1.5.67 1.5 1.5S10.33 8 9.5 8m-3 4c-.83 0-1.5-.67-1.5-1.5S5.67 9 6.5 9 8 9.67 8 10.5 7.33 12 6.5 12M12 3a9 9 0 0 0 0 18c.83 0 1.5-.67 1.5-1.5 0-.39-.15-.74-.39-1-.23-.27-.38-.62-.38-1 0-.83.67-1.5 1.5-1.5H16c2.76 0 5-2.24 5-5 0-4.42-4.03-8-9-8z"></path></svg>',
}),
Object.assign({}, buttonShowTraits, {
label: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5m7.43-2.53c.04-.32.07-.64.07-.97 0-.33-.03-.66-.07-1l2.11-1.63c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.31-.61-.22l-2.49 1c-.52-.39-1.06-.73-1.69-.98l-.37-2.65A.506.506 0 0 0 14 2h-4c-.25 0-.46.18-.5.42l-.37 2.65c-.63.25-1.17.59-1.69.98l-2.49-1c-.22-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64L4.57 11c-.04.34-.07.67-.07 1 0 .33.03.65.07.97l-2.11 1.66c-.19.15-.25.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1.01c.52.4 1.06.74 1.69.99l.37 2.65c.04.24.25.42.5.42h4c.25 0 .46-.18.5-.42l.37-2.65c.63-.26 1.17-.59 1.69-.99l2.49 1.01c.22.08.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.66z"></path></svg>',
label:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 15.5c-1.93 0-3.5-1.57-3.5-3.5s1.57-3.5 3.5-3.5 3.5 1.57 3.5 3.5-1.57 3.5-3.5 3.5m7.43-2.53c.04-.32.07-.64.07-.97 0-.33-.03-.66-.07-1l2.11-1.63c.19-.15.24-.42.12-.64l-2-3.46c-.12-.22-.39-.31-.61-.22l-2.49 1c-.52-.39-1.06-.73-1.69-.98l-.37-2.65A.506.506 0 0 0 14 2h-4c-.25 0-.46.18-.5.42l-.37 2.65c-.63.25-1.17.59-1.69.98l-2.49-1c-.22-.09-.49 0-.61.22l-2 3.46c-.13.22-.07.49.12.64L4.57 11c-.04.34-.07.67-.07 1 0 .33.03.65.07.97l-2.11 1.66c-.19.15-.25.42-.12.64l2 3.46c.12.22.39.3.61.22l2.49-1.01c.52.4 1.06.74 1.69.99l.37 2.65c.04.24.25.42.5.42h4c.25 0 .46-.18.5-.42l.37-2.65c.63-.26 1.17-.59 1.69-.99l2.49 1.01c.22.08.49 0 .61-.22l2-3.46c.12-.22.07-.49-.12-.64l-2.11-1.66z"></path></svg>',
}),
],
};
var panelDevices = {
id: 'panel-devices',
buttons: [{
buttons: [
{
id: 'device-desktop',
label: 'D',
command: { run: editor => editor.setDevice('Desktop') },
command: { run: (editor) => editor.setDevice('Desktop') },
active: true,
togglable: false,
}, {
},
{
id: 'device-mobile',
label: 'M',
command: { run: editor => editor.setDevice('Mobile') },
command: { run: (editor) => editor.setDevice('Mobile') },
togglable: false,
}],
},
],
};
var panelDevicesIcons = Object.assign({}, panelDevices, {
buttons: [
Object.assign({}, panelDevices.buttons[0], {
label: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M21 14H3V4h18m0-2H3c-1.11 0-2 .89-2 2v12c0 1.1.9 2 2 2h7l-2 3v1h8v-1l-2-3h7c1.1 0 2-.9 2-2V4a2 2 0 0 0-2-2z"></path></svg>',
label:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M21 14H3V4h18m0-2H3c-1.11 0-2 .89-2 2v12c0 1.1.9 2 2 2h7l-2 3v1h8v-1l-2-3h7c1.1 0 2-.9 2-2V4a2 2 0 0 0-2-2z"></path></svg>',
}),
Object.assign({}, panelDevices.buttons[1], {
label: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M16 18H7V4h9m-4.5 18c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5m4-21h-8A2.5 2.5 0 0 0 5 3.5v17A2.5 2.5 0 0 0 7.5 23h8a2.5 2.5 0 0 0 2.5-2.5v-17A2.5 2.5 0 0 0 15.5 1z"></path></svg>',
label:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M16 18H7V4h9m-4.5 18c-.83 0-1.5-.67-1.5-1.5s.67-1.5 1.5-1.5 1.5.67 1.5 1.5-.67 1.5-1.5 1.5m4-21h-8A2.5 2.5 0 0 0 5 3.5v17A2.5 2.5 0 0 0 7.5 23h8a2.5 2.5 0 0 0 2.5-2.5v-17A2.5 2.5 0 0 0 15.5 1z"></path></svg>',
}),
]
],
});
var gjsConfigStart = {
@ -331,7 +363,7 @@ var gjsConfigLayers = Object.assign({}, gjsConfigBlocks, {
container: '#gjs4',
blockManager: Object.assign({}, blockManager, { appendTo: '#blocks4' }),
layerManager: { appendTo: '#layers-container', scrollLayers: 0 },
panels: { defaults: [panelSidebar] }
panels: { defaults: [panelSidebar] },
});
var gjsConfigStyle = Object.assign({}, gjsConfigBlocks, {

47
docs/.vuepress/config.js

@ -2,7 +2,7 @@ const version = require('./../../package.json').version;
const isDev = process.argv[2] === 'dev';
const devPath = 'http://localhost:8080';
const baseUrl = 'https://grapesjs.com';
const subDivider = " ‍ ‍ ‍ ";
const subDivider = ' ‍ ‍ ‍ ';
module.exports = {
title: 'GrapesJS',
@ -11,8 +11,19 @@ module.exports = {
serviceWorker: false, // Enable Service Worker for offline usage
head: [
['link', { rel: 'icon', href: '/logo-icon.png' }],
['link', { rel: 'stylesheet', href: isDev ? `${devPath}/dist/css/grapes.min.css` : `${baseUrl}/assets/styles/grapesjs/grapes.min.css?${version}` }],
['script', { src: isDev ? `${devPath}/grapes.min.js` : `${baseUrl}/assets/scripts/grapesjs/grapes.min.js?${version}` }],
[
'link',
{
rel: 'stylesheet',
href: isDev
? `${devPath}/dist/css/grapes.min.css`
: `${baseUrl}/assets/styles/grapesjs/grapes.min.css?${version}`,
},
],
[
'script',
{ src: isDev ? `${devPath}/grapes.min.js` : `${baseUrl}/assets/scripts/grapesjs/grapes.min.js?${version}` },
],
],
localesSKIP: {
'/': {
@ -21,7 +32,7 @@ module.exports = {
'/it/': {
lang: 'it-IT',
description: 'GrapesJS documentazione',
}
},
},
themeConfig: {
editLinks: true,
@ -40,14 +51,9 @@ module.exports = {
'/it/': {
selectText: 'IT',
label: 'Italiano',
nav: [
{ text: 'Supportaci', link: 'https://opencollective.com/grapesjs' },
],
sidebar: [
'/',
['/getting-started', 'Getting Started'],
]
}
nav: [{ text: 'Supportaci', link: 'https://opencollective.com/grapesjs' }],
sidebar: ['/', ['/getting-started', 'Getting Started']],
},
},
nav: [
{ text: 'Docs', link: '/' },
@ -120,20 +126,19 @@ module.exports = {
['/modules/Storage', 'Storage Manager'],
['/modules/Modal', 'Modal'],
['/modules/Plugins', 'Plugins'],
]
}, {
],
},
{
title: 'Guides',
collapsable: false,
children: [
['/guides/Symbols', 'Symbols'],
['/guides/Replace-Rich-Text-Editor', 'Replace Rich Text Editor'],
['/guides/Custom-CSS-parser', 'Use Custom CSS Parser'],
]
}
],
},
],
}
},
},
plugins: [
[ '@vuepress/google-analytics', { ga: 'UA-74284223-1' } ],
],
}
plugins: [['@vuepress/google-analytics', { ga: 'UA-74284223-1' }]],
};

4
docs/.vuepress/enhanceApp.js

@ -3,7 +3,7 @@ module.exports = ({
Vue, // the version of Vue being used in the VuePress app
options, // the options for the root Vue instance
router, // the router instance for the app
siteData // site metadata
siteData, // site metadata
}) => {
// ...apply enhancements to the app
}
};

32437
docs/.vuepress/public/grapes.min.js

File diff suppressed because one or more lines are too long

4
docs/.vuepress/theme/index.js

@ -1,3 +1,3 @@
module.exports = {
extend: '@vuepress/theme-default'
}
extend: '@vuepress/theme-default',
};

25
docs/.vuepress/theme/layouts/CarbonAds.vue

@ -1,9 +1,9 @@
<script>
export default {
render (h) {
return h('div', { class: 'carbon-ads', attrs: { id: 'native-carbon' }})
render(h) {
return h('div', { class: 'carbon-ads', attrs: { id: 'native-carbon' } });
},
mounted () {
mounted() {
// window.BSANativeCallback = (a) => {
// const total = a.ads.length;
//
@ -18,26 +18,23 @@ export default {
this.load();
},
watch: {
'$route' (to, from) {
if (
to.path !== from.path &&
this.$el.querySelector('#carbonads')
) {
$route(to, from) {
if (to.path !== from.path && this.$el.querySelector('#carbonads')) {
this.$el.innerHTML = '';
this.load();
}
}
},
},
methods: {
initCarbon() {
const { _bsa } = window;
if(typeof _bsa !== 'undefined' && _bsa) {
if (typeof _bsa !== 'undefined' && _bsa) {
_bsa.init('default', 'CK7I62QJ', 'placement:grapesjscomdocs', {
target: '#native-carbon',
});
}
},
load () {
load() {
// const s = document.createElement('script');
// s.src = `//m.servedby-buysellads.com/monetization.js`;
// s.onload = () => this.initCarbon();
@ -48,9 +45,9 @@ export default {
src.setAttribute('id', '_carbonads_js');
const adCont = document.getElementById('native-carbon');
adCont && adCont.appendChild(src);
}
}
}
},
},
};
// @import "~@default-theme/styles/config.styl"
</script>

8
docs/.vuepress/theme/layouts/Layout.vue

@ -1,6 +1,6 @@
<template>
<Layout>
<CarbonAds slot="sidebar-top"/>
<CarbonAds slot="sidebar-top" />
</Layout>
</template>
@ -9,13 +9,13 @@
// var CarbonAds = require('./CarbonAds.vue').default;
// import Layout from '@vuepress/theme-default/layouts/Layout.vue';
// import CarbonAds from './CarbonAds.vue';
import Layout from '@parent-theme/layouts/Layout.vue'
import Layout from '@parent-theme/layouts/Layout.vue';
import CarbonAds from './CarbonAds.vue';
export default {
components: {
Layout,
CarbonAds,
}
}
},
};
</script>

35
docs/Home.md

@ -5,19 +5,20 @@ This page will introduce you to the main options of GrapesJS and how it works, i
The pretty minimalistic way to instantiate the editor could be like this:
```html
<link rel="stylesheet" href="path/to/grapes.min.css">
<link rel="stylesheet" href="path/to/grapes.min.css" />
<script src="path/to/grapes.min.js"></script>
<div id="gjs"></div>
<script type="text/javascript">
var editor = grapesjs.init({
container : '#gjs',
components: '<div class="txt-red">Hello world!</div>',
style: '.txt-red{color: red}',
container: '#gjs',
components: '<div class="txt-red">Hello world!</div>',
style: '.txt-red{color: red}',
});
</script>
```
In just few lines, with the default configurations, you're already able to see something with which play around.
[[img/default-gjs.jpg]]
@ -28,7 +29,6 @@ Of course all those stuff (panels, buttons, commands, etc.) are set just as defa
[[img/canvas-panels.jpg]]
If you'd like to extend the already instantiated editor you have to check [API Reference]. Check also [how to create plugins](./Creating-plugins) using the same API.
In this guide we'll focus on how to initialize the editor with all custom UI from scratch.
@ -48,6 +48,7 @@ var editor = grapesjs.init({
});
...
```
In this example we set a panel with 'commands' as an id and after the render we'll see nothing more than an empty div added to our panels. The new panel is already styled as the id 'commands' is one of the default but you can use whatever you like and place it wherever you want with CSS. With refresh we might see something like shown in the image below, with the new panel on the left:
[[img/new-panel.png]]
@ -79,7 +80,6 @@ Yeah, the button is pretty nice and happy, but useless without any command assig
> Check [Panels API Reference] for more details about Panels and Buttons
Assigning commands is pretty easy, but before you should define one or use one of defaults ([Built-in commands](./Built-in-commands)). So in this case we gonna create a new one.
```js
@ -111,17 +111,15 @@ Assigning commands is pretty easy, but before you should define one or use one o
}
...
```
As you see we added a new command `helloWorld` and used its `id` as an identifier inside `button.command`. In addition to this we've also implemented two required methods, `run` and `stop`, to make button execute commands.
[[img/btn-clicked.png]]
> Check [Commands API Reference]
Check the [demo](http://grapesjs.com/demo.html) for more complete usage of panels, buttons and built-in commands.
## Components
Components are elements inside the canvas, which can be drawn by commands or injected directly via configurations. In simple terms components represent the structure of our HTML document. You can init the editor with passing components as an HTML string
@ -136,6 +134,7 @@ Components are elements inside the canvas, which can be drawn by commands or inj
'<div style="width:500px; min-height:100px; margin: 0 auto"></div>',
...
```
We added 3 simple components with some basic style. If you refresh probably you'll see the same empty page but are actually there, you only need to highlight them.
For this purpose already exists a command, so add it to your panel in this way
@ -161,6 +160,7 @@ For this purpose already exists a command, so add it to your panel in this way
},
...
```
Worth noting the use of `context` option (try to click 'smile' command without it) and `active` to enable it after the render.
Now you should be able to see blocks inside canvas.
@ -170,7 +170,6 @@ You could add other commands to enable interactions with blocks. Check [Built-in
> Check [Components API Reference]
## Style Manager
Any HTML structure requires, at some point, a proper style, so to meet this need the Style Manager was added as a built-in feature in GrapesJS. Style manager is composed by sectors, which group inside different types of CSS properties. So you can add, for instance, a `Dimension` sector for `width` and `height`, and another one as `Typography` for `font-size` and `color`. So it's up to you decide how organize sectors.
@ -377,7 +376,6 @@ As you can see using `buildProps` actually will save you a lot of work. You coul
> Check [Style Manager API Reference]
## Store/load data
In this last part we're gonna see how to store and load template data inside GrapesJS. You may already noticed that even if you refresh the page after changes on canvas your data are not lost and this because GrapesJS comes with some built-in storage implementation.
@ -448,11 +446,10 @@ If you prefer you could also disable autosaving and do it by yourself using some
> Check [Storage Manager API Reference]
[API Reference]: <API-Reference>
[Panels API Reference]: <API-Panels>
[Commands API Reference]: <API-Commands>
[Components API Reference]: <API-Components>
[Style Manager API Reference]: <API-Style-Manager>
[Editor API Reference]: <API-Editor>
[Storage Manager API Reference]: <API-Storage-Manager>
[API Reference]: API-Reference
[Panels API Reference]: API-Panels
[Commands API Reference]: API-Commands
[Components API Reference]: API-Components
[Style Manager API Reference]: API-Style-Manager
[Editor API Reference]: API-Editor
[Storage Manager API Reference]: API-Storage-Manager

49
docs/README.md

@ -3,55 +3,42 @@
[[toc]]
## What is GrapesJS?
At first glance one might think this is just another page/HTML builder, but it's something more. GrapesJS is a multi-purpose, Web Builder Framework, which means it allows you to easily create a drag & drop enabled builder of "things". By "things" we mean anything with HTML-like structure, which entails much more than web pages. We use HTML-like structure basically everywhere: Newsletters (eg. [MJML](https://mjml.io/)), Native Mobile Applications (eg. [React Native](https://github.com/facebook/react-native)), Native Desktop Applications (eg. [Vuido](https://vuido.mimec.org)), PDFs (eg. [React PDF](https://github.com/diegomura/react-pdf)), etc. So, for everything you can imagine as a set of elements like `<tag some="attribute">... other nested elements ...</tag>` you can create easily a GrapesJS builder around it and then use it independently in your applications.
GrapesJS ships with features and tools that enable you to craft easy to use builders. Which allows your users to create complex HTML-like templates without any knowledge of coding.
At first glance one might think this is just another page/HTML builder, but it's something more. GrapesJS is a multi-purpose, Web Builder Framework, which means it allows you to easily create a drag & drop enabled builder of "things". By "things" we mean anything with HTML-like structure, which entails much more than web pages. We use HTML-like structure basically everywhere: Newsletters (eg. [MJML](https://mjml.io/)), Native Mobile Applications (eg. [React Native](https://github.com/facebook/react-native)), Native Desktop Applications (eg. [Vuido](https://vuido.mimec.org)), PDFs (eg. [React PDF](https://github.com/diegomura/react-pdf)), etc. So, for everything you can imagine as a set of elements like `<tag some="attribute">... other nested elements ...</tag>` you can create easily a GrapesJS builder around it and then use it independently in your applications.
GrapesJS ships with features and tools that enable you to craft easy to use builders. Which allows your users to create complex HTML-like templates without any knowledge of coding.
## Why GrapesJS?
GrapesJS was designed primarily for use inside Content Management Systems to speed up the creation of dynamic templates and replace common WYSIWYG editors, which are good for content editing, but inappropriate for creating HTML structures. Instead of creating an application we decided to create an extensible framework that could be used by anyone for any purpose.
GrapesJS was designed primarily for use inside Content Management Systems to speed up the creation of dynamic templates and replace common WYSIWYG editors, which are good for content editing, but inappropriate for creating HTML structures. Instead of creating an application we decided to create an extensible framework that could be used by anyone for any purpose.
## Quick Start
To showcase the power of GrapesJS we have created some presets.
* [grapesjs-preset-webpage](https://github.com/GrapesJS/preset-webpage) - [Webpage Builder](https://grapesjs.com/demo.html)
* [grapesjs-preset-newsletter](https://github.com/GrapesJS/preset-newsletter) - [Newsletter Builder](https://grapesjs.com/demo-newsletter-editor.html)
* [grapesjs-mjml](https://github.com/GrapesJS/mjml) - [Newsletter Builder with MJML](https://grapesjs.com/demo-mjml.html)
- [grapesjs-preset-webpage](https://github.com/GrapesJS/preset-webpage) - [Webpage Builder](https://grapesjs.com/demo.html)
- [grapesjs-preset-newsletter](https://github.com/GrapesJS/preset-newsletter) - [Newsletter Builder](https://grapesjs.com/demo-newsletter-editor.html)
- [grapesjs-mjml](https://github.com/GrapesJS/mjml) - [Newsletter Builder with MJML](https://grapesjs.com/demo-mjml.html)
You can actually use them as a starting point for your editors, so, just follow the instructions on their repositories to get a quick start for your builder.
## Download
Latest version: [![npm](https://img.shields.io/npm/v/grapesjs.svg?colorB=e67891)](https://www.npmjs.com/package/grapesjs)
You can download GrapesJS from one of these sources
* CDNs
* unpkg
* `https://unpkg.com/grapesjs`
* `https://unpkg.com/grapesjs/dist/css/grapes.min.css`
* cdnjs
* `https://cdnjs.cloudflare.com/ajax/libs/grapesjs/0.12.17/grapes.min.js`
* `https://cdnjs.cloudflare.com/ajax/libs/grapesjs/0.12.17/css/grapes.min.css`
* npm
* `npm i grapesjs`
* git
* `git clone https://github.com/GrapesJS/grapesjs.git`
- CDNs
- unpkg
- `https://unpkg.com/grapesjs`
- `https://unpkg.com/grapesjs/dist/css/grapes.min.css`
- cdnjs
- `https://cdnjs.cloudflare.com/ajax/libs/grapesjs/0.12.17/grapes.min.js`
- `https://cdnjs.cloudflare.com/ajax/libs/grapesjs/0.12.17/css/grapes.min.css`
- npm
- `npm i grapesjs`
- git
- `git clone https://github.com/GrapesJS/grapesjs.git`
## Changelog
To track changes made in the library we rely on [Github Releases](https://github.com/GrapesJS/grapesjs/releases)

159
docs/api.js

@ -15,12 +15,14 @@ const getEventsMdFromTypes = async (filePath) => {
const typesFilePath = `${dirname}types.ts`;
if (fs.existsSync(typesFilePath)) {
const resTypes = await documentation.build([typesFilePath], { shallow: true })
.then(cm => documentation.formats.md(cm, /*{ markdownToc: true }*/));
const resTypes = await documentation
.build([typesFilePath], { shallow: true })
.then((cm) => documentation.formats.md(cm /*{ markdownToc: true }*/));
const indexFrom = resTypes.indexOf(START_EVENTS) + START_EVENTS.length;
const indexTo = resTypes.indexOf(END_EVENTS);
// console.log(`${resTypes}`)
const result = resTypes.substring(indexFrom, indexTo)
const result = resTypes
.substring(indexFrom, indexTo)
.replace(/\n### Examples\n/gi, '')
.replace(/\n## types\n/gi, '')
.replace(/## /gi, '* ')
@ -30,92 +32,95 @@ const getEventsMdFromTypes = async (filePath) => {
.replace(/\]\\\(/gi, '](')
.trim();
return result
return result;
}
return '';
}
};
async function generateDocs () {
async function generateDocs() {
log('Start API Reference generation...');
await Promise.all([
['editor/index.ts', 'editor.md'],
['asset_manager/index.ts', 'assets.md'],
['asset_manager/model/Asset.ts', 'asset.md'],
['block_manager/index.ts', 'block_manager.md'],
['block_manager/model/Block.ts', 'block.md'],
['commands/index.ts', 'commands.md'],
['dom_components/index.ts', 'components.md'],
['dom_components/model/Component.ts', 'component.md'],
['panels/index.ts', 'panels.md'],
['style_manager/index.ts', 'style_manager.md'],
['style_manager/model/Sector.ts', 'sector.md'],
['style_manager/model/Property.ts', 'property.md'],
['style_manager/model/PropertyNumber.ts', 'property_number.md'],
['style_manager/model/PropertySelect.ts', 'property_select.md'],
['style_manager/model/PropertyComposite.ts', 'property_composite.md'],
['style_manager/model/PropertyStack.ts', 'property_stack.md'],
['style_manager/model/Layer.ts', 'layer.md'],
['storage_manager/index.ts', 'storage_manager.md'],
['trait_manager/index.ts', 'trait_manager.md'],
['trait_manager/model/Trait.ts', 'trait.md'],
['device_manager/index.ts', 'device_manager.md'],
['device_manager/model/Device.ts', 'device.md'],
['selector_manager/index.ts', 'selector_manager.md'],
['selector_manager/model/Selector.ts', 'selector.md'],
['selector_manager/model/State.ts', 'state.md'],
['css_composer/index.ts', 'css_composer.md'],
['css_composer/model/CssRule.ts', 'css_rule.md'],
['modal_dialog/index.ts', 'modal_dialog.md'],
['rich_text_editor/index.ts', 'rich_text_editor.md'],
['keymaps/index.ts', 'keymaps.md'],
['undo_manager/index.ts', 'undo_manager.md'],
['canvas/index.ts', 'canvas.md'],
['canvas/model/Frame.ts', 'frame.md'],
['canvas/model/CanvasSpot.ts', 'canvas_spot.md'],
['i18n/index.ts', 'i18n.md'],
['navigator/index.ts', 'layer_manager.md'],
['pages/index.ts', 'pages.md'],
['pages/model/Page.ts', 'page.md'],
['parser/index.ts', 'parser.md'],
].map(async (file) => {
const filePath = `${srcRoot}/${file[0]}`;
await Promise.all(
[
['editor/index.ts', 'editor.md'],
['asset_manager/index.ts', 'assets.md'],
['asset_manager/model/Asset.ts', 'asset.md'],
['block_manager/index.ts', 'block_manager.md'],
['block_manager/model/Block.ts', 'block.md'],
['commands/index.ts', 'commands.md'],
['dom_components/index.ts', 'components.md'],
['dom_components/model/Component.ts', 'component.md'],
['panels/index.ts', 'panels.md'],
['style_manager/index.ts', 'style_manager.md'],
['style_manager/model/Sector.ts', 'sector.md'],
['style_manager/model/Property.ts', 'property.md'],
['style_manager/model/PropertyNumber.ts', 'property_number.md'],
['style_manager/model/PropertySelect.ts', 'property_select.md'],
['style_manager/model/PropertyComposite.ts', 'property_composite.md'],
['style_manager/model/PropertyStack.ts', 'property_stack.md'],
['style_manager/model/Layer.ts', 'layer.md'],
['storage_manager/index.ts', 'storage_manager.md'],
['trait_manager/index.ts', 'trait_manager.md'],
['trait_manager/model/Trait.ts', 'trait.md'],
['device_manager/index.ts', 'device_manager.md'],
['device_manager/model/Device.ts', 'device.md'],
['selector_manager/index.ts', 'selector_manager.md'],
['selector_manager/model/Selector.ts', 'selector.md'],
['selector_manager/model/State.ts', 'state.md'],
['css_composer/index.ts', 'css_composer.md'],
['css_composer/model/CssRule.ts', 'css_rule.md'],
['modal_dialog/index.ts', 'modal_dialog.md'],
['rich_text_editor/index.ts', 'rich_text_editor.md'],
['keymaps/index.ts', 'keymaps.md'],
['undo_manager/index.ts', 'undo_manager.md'],
['canvas/index.ts', 'canvas.md'],
['canvas/model/Frame.ts', 'frame.md'],
['canvas/model/CanvasSpot.ts', 'canvas_spot.md'],
['i18n/index.ts', 'i18n.md'],
['navigator/index.ts', 'layer_manager.md'],
['pages/index.ts', 'pages.md'],
['pages/model/Page.ts', 'page.md'],
['parser/index.ts', 'parser.md'],
].map(async (file) => {
const filePath = `${srcRoot}/${file[0]}`;
if (!fs.existsSync(filePath)) {
throw `File not found '${filePath}'`;
}
if (!fs.existsSync(filePath)) {
throw `File not found '${filePath}'`;
}
return documentation.build([filePath], { shallow: true })
.then(cm => documentation.formats.md(cm, /*{ markdownToc: true }*/))
.then(async (output) => {
let addLogs = [];
let result = output
.replace(/\*\*\\\[/g, '**[')
.replace(/\*\*\(\\\[/g, '**([')
.replace(/<\\\[/g, '<[')
.replace(/<\(\\\[/g, '<([')
.replace(/\| \\\[/g, '| [')
.replace(/\\n```js/g, '```js')
.replace(/docsjs\./g, '')
.replace('**Extends ModuleModel**', '')
.replace('**Extends Model**', '');
return documentation
.build([filePath], { shallow: true })
.then((cm) => documentation.formats.md(cm /*{ markdownToc: true }*/))
.then(async (output) => {
let addLogs = [];
let result = output
.replace(/\*\*\\\[/g, '**[')
.replace(/\*\*\(\\\[/g, '**([')
.replace(/<\\\[/g, '<[')
.replace(/<\(\\\[/g, '<([')
.replace(/\| \\\[/g, '| [')
.replace(/\\n```js/g, '```js')
.replace(/docsjs\./g, '')
.replace('**Extends ModuleModel**', '')
.replace('**Extends Model**', '');
// Search for module event documentation
if (result.indexOf(REPLACE_EVENTS) >= 0) {
const eventsMd = await getEventsMdFromTypes(filePath);
if (eventsMd && result.indexOf(REPLACE_EVENTS) >= 0) {
addLogs.push('replaced events');
// Search for module event documentation
if (result.indexOf(REPLACE_EVENTS) >= 0) {
const eventsMd = await getEventsMdFromTypes(filePath);
if (eventsMd && result.indexOf(REPLACE_EVENTS) >= 0) {
addLogs.push('replaced events');
}
result = eventsMd ? result.replace(REPLACE_EVENTS, `## Available Events\n${eventsMd}`) : result;
}
result = eventsMd ? result.replace(REPLACE_EVENTS, `## Available Events\n${eventsMd}`) : result;
}
fs.writeFileSync(`${docRoot}/api/${file[1]}`, result);
log('Created', file[1], addLogs.length ? `(${addLogs.join(', ')})` : '');
});
}));
fs.writeFileSync(`${docRoot}/api/${file[1]}`, result);
log('Created', file[1], addLogs.length ? `(${addLogs.join(', ')})` : '');
});
}),
);
log('API Reference generation done!');
};
}
generateDocs();

246
docs/getting-started.md

@ -15,7 +15,7 @@ This is a step-by-step guide for anyone who wants to create their own builder wi
Before you start using GrapesJS, you'll have to import it. Let's import the latest version:
```html
<link rel="stylesheet" href="//unpkg.com/grapesjs/dist/css/grapes.min.css">
<link rel="stylesheet" href="//unpkg.com/grapesjs/dist/css/grapes.min.css" />
<script src="//unpkg.com/grapesjs"></script>
<!--
If you need plugins, put them below the main grapesjs script
@ -41,12 +41,13 @@ The main part of the GrapesJS editor is the canvas, this is where you create the
<<< @/docs/.vuepress/components/demos/DemoCanvasOnly.js
<<< @/docs/.vuepress/components/demos/DemoCanvasOnly.css
<Demo>
<DemoCanvasOnly/>
<DemoCanvasOnly/>
</Demo>
With just the canvas you're already able to move, copy and delete components from the structure. For now, we see the example template taken from the container. Next let's look at how to create and drag custom blocks into our canvas.
## Add Blocks
The block in GrapesJS is just a reusable piece of HTML that you can drop in the canvas. A block can be an image, a button, or an entire section with videos, forms and iframes. Let's start by creating another container and append a few basic blocks inside of it. Later we can use this technique to build more complex structures.
```html{4}
@ -55,6 +56,7 @@ The block in GrapesJS is just a reusable piece of HTML that you can drop in the
</div>
<div id="blocks"></div>
```
```js
const editor = grapesjs.init({
// ...
@ -64,16 +66,18 @@ const editor = grapesjs.init({
{
id: 'section', // id is mandatory
label: '<b>Section</b>', // You can use HTML/SVG inside labels
attributes: { class:'gjs-block-section' },
attributes: { class: 'gjs-block-section' },
content: `<section>
<h1>This is a simple title</h1>
<div>This is just a Lorem text: Lorem ipsum dolor sit amet</div>
</section>`,
}, {
},
{
id: 'text',
label: 'Text',
content: '<div data-gjs-type="text">Insert your text here</div>',
}, {
},
{
id: 'image',
label: 'Image',
// Select the component once it's dropped
@ -84,11 +88,12 @@ const editor = grapesjs.init({
// This triggers `active` event on dropped components and the `image`
// reacts by opening the AssetManager
activate: true,
}
]
},
],
},
});
```
```css
.gjs-block {
width: auto;
@ -96,6 +101,7 @@ const editor = grapesjs.init({
min-height: auto;
}
```
<Demo>
<DemoBasicBlocks/>
</Demo>
@ -107,19 +113,22 @@ editor.BlockManager.add('my-block-id', {
label: '...',
category: '...',
// ...
})
});
```
::: tip
If you want to learn more about blocks we suggest reading its dedicated article: [Block Manager Module](modules/Blocks.html).
:::
## Define Components
Technically, once you drop your HTML block inside the canvas each element of the content is transformed into a GrapesJS Component. A GrapesJS Component is an object containing information about how the element is rendered in the canvas (managed in the View) and how it might look in its final code (created by the properties in the Model). Generally, all Model properties are reflected in the View. Therefore, if you add a new attribute to the model, it will be available in the export code (which we will learn more about later), and the element you see in the canvas will be updated with new attributes.
This isn't totally out of the ordinary, but the unique thing about Components is that you can create a totally decoupled View. This means you can show the user whatever you desire regardless of what is in the Model. For example, by dragging a placeholder text you can fetch and show instead a dynamic content. If you want to learn more about Custom Components, you should check out [Component Manager Module](modules/Components.html).
GrapesJS comes with a few [built-in Components](modules/Components.html#built-in-component-types) that enable different features once rendered in the canvas. For example, by double clicking on an image component you will see the default [Asset Manager](modules/Assets.html), which you can customize or integrate your own. By double clicking on the text component you're able to edit it via the built-in Rich Text Editor, which is also customizable and [replaceable](guides/Replace-Rich-Text-Editor.html).
As we have seen before you can create Blocks directly as Components:
```js
editor.BlockManager.add('my-block-id', {
// ...
@ -131,16 +140,18 @@ editor.BlockManager.add('my-block-id', {
{
tagName: 'span',
content: '<b>Some static content</b>',
}, {
},
{
tagName: 'div',
// use `content` for static strings, `components` string will be parsed
// and transformed in Components
components: '<span>HTML at some point</span>',
}
]
}
})
},
],
},
});
```
::: tip
Check out the [Components API](api/components.html) to learn how to interact with components dynamically.
:::
@ -156,6 +167,7 @@ myComponent.components('<div>New content</div>');
```
## Panels & Buttons
Now that we have a canvas and custom blocks let's see how to create a new custom panel with some buttons inside (using [Panels API](api/panels.html)) which trigger commands (the core one or custom).
```html{1,2,3}
@ -197,25 +209,29 @@ editor.Panels.addPanel({
className: 'btn-toggle-borders',
label: '<u>B</u>',
command: 'sw-visibility', // Built-in command
}, {
},
{
id: 'export',
className: 'btn-open-export',
label: 'Exp',
command: 'export-template',
context: 'export-template', // For grouping context of buttons from the same panel
}, {
},
{
id: 'show-json',
className: 'btn-show-json',
label: 'JSON',
context: 'show-json',
command(editor) {
editor.Modal.setTitle('Components JSON')
.setContent(`<textarea style="width:100%; height: 250px;">
.setContent(
`<textarea style="width:100%; height: 250px;">
${JSON.stringify(editor.getComponents())}
</textarea>`)
</textarea>`,
)
.open();
},
}
},
],
});
```
@ -228,7 +244,7 @@ We have defined where to render the panel with `el: '#basic-panel'` and then for
Try to use [Commands](api/commands.html) when possible, they allow you to track actions globally. Commands also execute callbacks before and after their execution (you can even interrupt them).
```js
editor.on('run:export-template:before', opts => {
editor.on('run:export-template:before', (opts) => {
console.log('Before the command run');
if (0 /* some condition */) {
opts.abort = 1;
@ -242,8 +258,8 @@ editor.on('abort:export-template', () => console.log('Command aborted'));
Check out the [Panels API](api/panels.html) to see all the available methods.
:::
## Layers
Another utility tool you might find useful when working with web elements is the layer manager. It's a tree overview of the structure nodes and enables you to manage it easier. To enable it you just have to specify where you want to render it.
```html{4,5,6,7,8,9,10,11}
@ -260,40 +276,45 @@ Another utility tool you might find useful when working with web elements is the
</div>
<div id="blocks"></div>
```
<<< @/docs/.vuepress/components/demos/DemoLayers.css
```js
const editor = grapesjs.init({
// ...
layerManager: {
appendTo: '.layers-container'
appendTo: '.layers-container',
},
// We define a default panel as a sidebar to contain layers
panels: {
defaults: [{
id: 'layers',
el: '.panel__right',
// Make the panel resizable
resizable: {
maxDim: 350,
minDim: 200,
tc: 0, // Top handler
cl: 1, // Left handler
cr: 0, // Right handler
bc: 0, // Bottom handler
// Being a flex child we need to change `flex-basis` property
// instead of the `width` (default)
keyWidth: 'flex-basis',
defaults: [
{
id: 'layers',
el: '.panel__right',
// Make the panel resizable
resizable: {
maxDim: 350,
minDim: 200,
tc: 0, // Top handler
cl: 1, // Left handler
cr: 0, // Right handler
bc: 0, // Bottom handler
// Being a flex child we need to change `flex-basis` property
// instead of the `width` (default)
keyWidth: 'flex-basis',
},
},
}]
}
],
},
});
```
<Demo>
<DemoLayers/>
</Demo>
## Style Manager
Once you have defined the structure of the template the next step is the ability to style it. To meet this need GrapesJS includes the Style Manager module which is composed by CSS style properties and sectors. To make it more clear, let's see how to define a basic set.
Let's start by adding one more panel inside the `panel__right` and another one in `panel__top` which will contain a Layer/Style Manager switcher:
@ -310,11 +331,13 @@ Let's start by adding one more panel inside the `panel__right` and another one i
</div>
...
```
```css
.panel__switcher {
position: initial;
}
```
```js
const editor = grapesjs.init({
// ...
@ -324,33 +347,37 @@ const editor = grapesjs.init({
{
id: 'panel-switcher',
el: '.panel__switcher',
buttons: [{
buttons: [
{
id: 'show-layers',
active: true,
label: 'Layers',
command: 'show-layers',
// Once activated disable the possibility to turn it off
togglable: false,
}, {
},
{
id: 'show-style',
active: true,
label: 'Styles',
command: 'show-styles',
togglable: false,
}],
}
]
},
],
},
],
},
// The Selector Manager allows to assign classes and
// different states (eg. :hover) on components.
// Generally, it's used in conjunction with Style Manager
// but it's not mandatory
selectorManager: {
appendTo: '.styles-container'
appendTo: '.styles-container',
},
styleManager: {
appendTo: '.styles-container',
sectors: [{
sectors: [
{
name: 'Dimension',
open: false,
// Use built-in properties
@ -366,9 +393,10 @@ const editor = grapesjs.init({
units: ['px', '%'], // Units, available only for 'integer' types
defaults: 'auto', // Default value
min: 0, // Min value, available only for 'integer' types
}
]
},{
},
],
},
{
name: 'Extra',
open: false,
buildProps: ['background-color', 'box-shadow', 'custom-prop'],
@ -385,16 +413,21 @@ const editor = grapesjs.init({
{ value: '18px', name: 'Medium' },
{ value: '32px', name: 'Big' },
],
}
]
}]
},
],
},
],
},
});
// Define commands
editor.Commands.add('show-layers', {
getRowEl(editor) { return editor.getContainer().closest('.editor-row'); },
getLayersEl(row) { return row.querySelector('.layers-container') },
getRowEl(editor) {
return editor.getContainer().closest('.editor-row');
},
getLayersEl(row) {
return row.querySelector('.layers-container');
},
run(editor, sender) {
const lmEl = this.getLayersEl(this.getRowEl(editor));
@ -406,8 +439,12 @@ editor.Commands.add('show-layers', {
},
});
editor.Commands.add('show-styles', {
getRowEl(editor) { return editor.getContainer().closest('.editor-row'); },
getStyleEl(row) { return row.querySelector('.styles-container') },
getRowEl(editor) {
return editor.getContainer().closest('.editor-row');
},
getStyleEl(row) {
return row.querySelector('.styles-container');
},
run(editor, sender) {
const smEl = this.getStyleEl(this.getRowEl(editor));
@ -438,6 +475,7 @@ Each component can also indicate what to style and what not.
-->
## Traits
Most of the time you would style your components and place them somewhere in the structure, but sometimes your components might need custom attributes or even custom behaviors and for this need you can make use of traits. Traits are commonly used to update HTML element attributes (eg. `placeholder` for inputs or `alt` for images), but you can also define your own custom traits. Access the selected Component model and do whatever you want. For this guide, we are going to show you how to render available traits, for more details on how to extend them we suggest you read the [Trait Manager Module page](modules/Traits.html).
Let's create a new container for traits. Tell the editor where to render it and update the sidebar switcher:
@ -469,9 +507,10 @@ const editor = grapesjs.init({
label: 'Traits',
command: 'show-traits',
togglable: false,
}],
}
]
},
],
},
],
},
traitManager: {
appendTo: '.traits-container',
@ -501,6 +540,7 @@ editor.Commands.add('show-traits', {
Now if you switch to the Trait panel and select one of the inner components you should see its default traits.
## Responsive templates
GrapesJS implements a module which allows you to work with responsive templates easily. Let's see how to define different devices and a button for device switching:
```html{3}
@ -511,23 +551,28 @@ GrapesJS implements a module which allows you to work with responsive templates
</div>
...
```
```css
.panel__devices {
position: initial;
}
```
```js
const editor = grapesjs.init({
// ...
deviceManager: {
devices: [{
devices: [
{
name: 'Desktop',
width: '', // default size
}, {
},
{
name: 'Mobile',
width: '320px', // this value will be used on canvas width
widthMedia: '480px', // this value will be used in CSS @media
}]
},
],
},
// ...
panels: {
@ -536,29 +581,32 @@ const editor = grapesjs.init({
{
id: 'panel-devices',
el: '.panel__devices',
buttons: [{
buttons: [
{
id: 'device-desktop',
label: 'D',
command: 'set-device-desktop',
active: true,
togglable: false,
}, {
},
{
id: 'device-mobile',
label: 'M',
command: 'set-device-mobile',
togglable: false,
}],
}
]
},
],
},
],
},
});
// Commands
editor.Commands.add('set-device-desktop', {
run: editor => editor.setDevice('Desktop')
run: (editor) => editor.setDevice('Desktop'),
});
editor.Commands.add('set-device-mobile', {
run: editor => editor.setDevice('Mobile')
run: (editor) => editor.setDevice('Mobile'),
});
```
@ -579,15 +627,18 @@ const editor = grapesjs.init({
// ...
mediaCondition: 'min-width', // default is `max-width`
deviceManager: {
devices: [{
devices: [
{
name: 'Mobile',
width: '320',
widthMedia: '',
}, {
},
{
name: 'Desktop',
width: '',
widthMedia:'1024',
}]
widthMedia: '1024',
},
],
},
// ...
});
@ -601,24 +652,26 @@ Check out the [Device Manager API](api/device_manager.html) to see all the avail
:::
## Store & load data
Once you have finished with defining your builder interface the next step would be to setup the storing and loading process.
GrapesJS implements 2 simple type of storages inside its Storage Manager: The local (by using `localStorage`, active by default) and the remote one. Those are enough to cover most of the cases, but it's also possible to add new implementations ([grapesjs-indexeddb](https://github.com/GrapesJS/storage-indexeddb) is a good example).
Let's see how the default options work:
```js
grapesjs.init({
// ...
storageManager: {
type: 'local', // Type of the storage, available: 'local' | 'remote'
autosave: true, // Store data automatically
autoload: true, // Autoload stored data on init
stepsBeforeSave: 1, // If autosave enabled, indicates how many changes are necessary before store method is triggered
options: {
local: { // Options for the `local` type
key: 'gjsProject', // The key for the local storage
},
}
}
// ...
storageManager: {
type: 'local', // Type of the storage, available: 'local' | 'remote'
autosave: true, // Store data automatically
autoload: true, // Autoload stored data on init
stepsBeforeSave: 1, // If autosave enabled, indicates how many changes are necessary before store method is triggered
options: {
local: {
// Options for the `local` type
key: 'gjsProject', // The key for the local storage
},
},
},
});
```
@ -626,21 +679,22 @@ Let's take a look at the configuration required to setup the remote storage:
```js
grapesjs.init({
// ...
storageManager: {
type: 'remote',
// ...
storageManager: {
type: 'remote',
// ...
stepsBeforeSave: 10,
options: {
remote: {
headers: {}, // Custom headers for the remote storage request
urlStore: 'https://your-server/endpoint/store', // Endpoint URL where to store data project
urlLoad: 'https://your-server/endpoint/load', // Endpoint URL where to load data project
}
stepsBeforeSave: 10,
options: {
remote: {
headers: {}, // Custom headers for the remote storage request
urlStore: 'https://your-server/endpoint/store', // Endpoint URL where to store data project
urlLoad: 'https://your-server/endpoint/load', // Endpoint URL where to load data project
},
}
},
},
});
```
As you might noticed, we've left some default options unchanged, increased changes necessary for autosave triggering and passed remote endpoints.
If you prefer you could also disable the autosaving and use a custom command to trigger the store:
@ -669,6 +723,7 @@ If you prefer you could also disable the autosaving and use a custom command to
To get a better overview of the Storage Manager and how you should store/load the template, or how to define new storages you should read the [Storage Manager Module](modules/Storage.html) page.
## Theming
One last step that might actually improve a lot your editor personality is how it looks visually. To achieve an easy theming we have adapted an atomic design for this purpose. So for example to customize the main palette of colors all you have to do is to place your custom CSS rules after the GrapesJS styles.
To complete our builder let's customize its color palette and to make it more visually "readable" we can replace all button labels with SVG icons:
@ -706,6 +761,7 @@ To complete our builder let's customize its color palette and to make it more vi
There is also a bunch of [CSS custom properties (variables)](https://developer.mozilla.org/en-US/docs/Web/CSS/Using_CSS_custom_properties) that you can use to customize the styles of the editor.
For example, you could achieve the same result as above by doing this:
```css
:root {
--gjs-primary-color: #78366a;
@ -715,10 +771,8 @@ For example, you could achieve the same result as above by doing this:
}
```
And here is our final result:
<Demo id="final-result">
<DemoTheme/>
</Demo>

43
docs/guides/Custom-CSS-parser.md

@ -1,9 +1,10 @@
---
title: Use Custom CSS Parser
---
# Use Custom CSS Parser
If you just use GrapesJS for building templates from scratch, so you start from an empty canvas and for editing you strictly rely on the generated JSON (final HTML/CSS only for end-users) then, probably, you might skip this guide. On the other hand, if you import templates from already defined HTML/CSS or let the user embed custom codes (eg. using the [grapesjs-custom-code](https://github.com/GrapesJS/components-custom-code) plugin), then you have to know that you might face strange behaviors.
If you just use GrapesJS for building templates from scratch, so you start from an empty canvas and for editing you strictly rely on the generated JSON (final HTML/CSS only for end-users) then, probably, you might skip this guide. On the other hand, if you import templates from already defined HTML/CSS or let the user embed custom codes (eg. using the [grapesjs-custom-code](https://github.com/GrapesJS/components-custom-code) plugin), then you have to know that you might face strange behaviors.
::: warning
This guide requires GrapesJS v0.14.33 or higher
@ -11,7 +12,6 @@ This guide requires GrapesJS v0.14.33 or higher
[[toc]]
## Import HTML/CSS
Importing already defined HTML/CSS is a really good feature as it lets you start editing immediately any kind of template and obviously, GrapesJS itself promotes this kind of approach
@ -19,13 +19,17 @@ Importing already defined HTML/CSS is a really good feature as it lets you start
```html
<div id="gjs">
<div class="txt-red">Hello world!</div>
<style>.txt-red{color: red}</style>
<style>
.txt-red {
color: red;
}
</style>
</div>
<script type="text/javascript">
const editor = grapesjs.init({
container : '#gjs',
fromElement: true,
container: '#gjs',
fromElement: true,
});
</script>
```
@ -80,10 +84,10 @@ So, for our case we just take in account a simple rule, we'll parse it and print
for (var i = 0, len = style.length; i < len; i++) {
var property = style[i];
var value = style.getPropertyValue(property);
styleStr += "\t" + property + ': ' + value + ";\n";
styleStr += '\t' + property + ': ' + value + ';\n';
}
var result = document.getElementById('result');
result.innerHTML = rule.selectorText + " {\n" + styleStr + "}";
result.innerHTML = rule.selectorText + ' {\n' + styleStr + '}';
}
var css = document.getElementById('css-to-parse').innerText;
@ -92,6 +96,7 @@ So, for our case we just take in account a simple rule, we'll parse it and print
```
### Results
Here some results (using latest versions + IE11)
<img :src="$withBase('/cssom-result.jpg')">
@ -115,10 +120,10 @@ The custom parser you have to use it's just a function receiving 2 arguments: `c
const parserCss = (css, editor) => {
const result = [];
// ... parse the CSS string
result.push({
selectors: '.someclass, div .otherclass',
style: { color: 'red' }
})
result.push({
selectors: '.someclass, div .otherclass',
style: { color: 'red' },
});
// ...
return result; // Result should be ALWAYS an array
};
@ -129,7 +134,7 @@ const editor = grapesjs.init({
//...
parser: {
parserCss,
}
},
});
// Or later, via editor API
@ -140,12 +145,12 @@ editor.setCustomParserCss(parserCss);
The syntax of rule objects is pretty straightforward, each object might contain following keys
| Key | Description | Example |
|-|-|-
| `selectors` | Selectors of the rule. <br> **REQUIRED** return an empty string in case the rule has no selectors | `.class1, div > #someid` |
| `style` | Style declarations as an object | `{ color: 'red' }` |
| `atRule` | At-rule name | `media` |
| `params` | Parameters of the at-rule | `screen and (min-width: 480px)` |
| Key | Description | Example |
| ----------- | ------------------------------------------------------------------------------------------------- | ------------------------------- |
| `selectors` | Selectors of the rule. <br> **REQUIRED** return an empty string in case the rule has no selectors | `.class1, div > #someid` |
| `style` | Style declarations as an object | `{ color: 'red' }` |
| `atRule` | At-rule name | `media` |
| `params` | Parameters of the at-rule | `screen and (min-width: 480px)` |
To make it more clear let's see a few examples
@ -249,4 +254,4 @@ To make it more clear let's see a few examples
Below the list of current available CSS parsers as plugins, if you need to create your own we highly suggest to explore their sources
* [grapesjs-parser-postcss](https://github.com/GrapesJS/parser-postcss) - Using [PostCSS](https://github.com/postcss/postcss) parser
- [grapesjs-parser-postcss](https://github.com/GrapesJS/parser-postcss) - Using [PostCSS](https://github.com/postcss/postcss) parser

19
docs/guides/Replace-Rich-Text-Editor.md

@ -1,6 +1,7 @@
---
title: Replace the built-in Rich Text Editor
---
# Replace the built-in Rich Text Editor
As you might have noticed the default Rich Text Editor (RTE) is really tiny and so doesn't seem like a complete solution as a text editor. Instead of showing how to add new commands inside the default one we'll show how to completely replace it with another one.
@ -13,7 +14,6 @@ This guide is referring to GrapesJS v0.21.2 or higher
[[toc]]
## Interface
### Enable
@ -71,8 +71,6 @@ const editor = grapesjs.init({
});
```
### Disable
Once we know how to enable the RTE let's implement the method which disable it, so let's create the `disable()` function.
@ -90,8 +88,6 @@ editor.setCustomRte({
});
```
### Content
Each third-party library could handle the state of the content differently and what is actually rendered as a DOM in the preview might not rapresent the final HTML output. So, by default, GrapesJS takes the `innerHTML` as the final output directly from the DOM element but is highly recommended to specify the method responsable to return the final state as HTML string (each third-party library might handle it differently).
@ -106,8 +102,6 @@ editor.setCustomRte({
});
```
### Focus
The `focus()` method is just a helper used inside `enable()` and not required by the interface
@ -120,7 +114,7 @@ const focus = (el, rte) => {
}
el.contentEditable = true;
rte?.focus();
}
};
editor.setCustomRte({
// ...
@ -132,11 +126,9 @@ editor.setCustomRte({
});
```
## Toolbar position
Sometimes the default top-left position of the toolbar is not always what you need. For example, when you scroll the canvas and the toolbar reaches the top, you'd like to move it down. For this purpose, you can add a listener which applies your logic in this way:
Sometimes the default top-left position of the toolbar is not always what you need. For example, when you scroll the canvas and the toolbar reaches the top, you'd like to move it down. For this purpose, you can add a listener which applies your logic in this way:
```js
editor.on('rteToolbarPosUpdate', (pos) => {
@ -144,15 +136,12 @@ editor.on('rteToolbarPosUpdate', (pos) => {
});
```
## The built-in vs third-party
One thing you have to keep in mind when using a custom RTE is that all the content and its behavior are handled by the library itself, the GrapesJS's component will just store the content as it is.
For example, when you create a link using the built-in RTE then you'll be able to select it and edit its `href` via Component Settings. With a custom RTE, it will be its own task to show the proper modal for the link editing.
Obviously, each third-party library has its own APIs and can present some limitations and drawbacks, so, a minimal knowledge of the library is a plus.
### Enable content parser
As an experimental feature, now it's possible to tell the editor to parse the returned HTML content from the custom RTE and store the result as components and not as a simple HTML string. This allows GrapesJS to handle the custom RTE more closely to the native implementation and enable features like [textable components](https://github.com/GrapesJS/grapesjs/issues/2771#issuecomment-1040486056).
@ -165,8 +154,6 @@ editor.setCustomRte({
});
```
## Plugins
For the CKEditor, you can find a complete plugin here [grapesjs-plugin-ckeditor](https://github.com/GrapesJS/ckeditor).

51
docs/guides/Symbols.md

@ -1,12 +1,12 @@
---
title: Symbols
---
# Symbols
::: warning
This feature is released as a beta from GrapesJS v0.21.11
To get a better understanding of the content in this guide we recommend reading [Components] first
:::
@ -14,7 +14,6 @@ Symbols are a special type of [Component] that allows you to easily reuse common
[[toc]]
## Concept
A Symbol created from a component retains the same shape and uses the same [Components API], but it includes a reference to other related Symbols. When you create a new Symbol from a Component, it creates a Main Symbol, and the original component becomes an Instance Symbol.
@ -29,13 +28,10 @@ Below is a simple representation of the connection between Main and Instance Sym
This feature operates at a low level, meaning there is no built-in UI for creating and managing symbols. Developers need to implement their own UI to interact with this feature. Below you'll find an example of implementation.
:::
## Programmatic usage
Let's see how to work with and manage Symbols in your project.
### Create symbol
Create a new Symbol from any component in your project:
@ -62,8 +58,6 @@ const symbols = editor.Components.getSymbols();
const symbolMain = symbols[0];
```
### Symbol details
Once you have Symbols in your project, you might need to know when a Component is a Symbol and get details about it. Use the `getSymbolInfo` method for this:
@ -92,8 +86,6 @@ symbolMainInfo.instances; // [anyComponent, secondInstance]; Reference to Instan
symbolMainInfo.relatives; // [anyComponent, symbolMain]; Relative Symbols
```
### Overrides
When you update a Symbol's properties, changes are propagated to all related Symbols. To avoid propagating specific properties, you can specify at the component level which properties to skip:
@ -109,8 +101,6 @@ anyComponent.set('my-property', false);
secondInstance.get('my-property'); // true; change didn't propagate
```
### Detach symbol
Once you have Symbol instances you might need to disconnect one to create a new custom shape with other components inside, in that case you can use `detachSymbol`.
@ -125,8 +115,6 @@ const infoMain = editor.Components.getSymbolInfo(symbolMain);
infoMain.instances; // [secondInstance]; Removed the reference
```
### Remove symbol
To remove a Main Symbol and detach all related instances:
@ -136,62 +124,62 @@ const symbolMain = editor.Components.getSymbols()[0];
symbolMain.remove();
```
## Events
The editor triggers several symbol-related events that you can leverage for your integration:
- `symbol:main:add` Added new root main symbol.
* `symbol:main:add` Added new root main symbol.
```js
editor.on('symbol:main:add', ({ component }) => { ... });
```
* `symbol:main:update` Root main symbol updated.
- `symbol:main:update` Root main symbol updated.
```js
editor.on('symbol:main:update', ({ component }) => { ... });
```
* `symbol:main:remove` Root main symbol removed.
- `symbol:main:remove` Root main symbol removed.
```js
editor.on('symbol:main:remove', ({ component }) => { ... });
```
* `symbol:main` Catch-all event related to root main symbol updates.
- `symbol:main` Catch-all event related to root main symbol updates.
```js
editor.on('symbol:main', ({ event, component }) => { ... });
```
* `symbol:instance:add` Added new root instance symbol.
- `symbol:instance:add` Added new root instance symbol.
```js
editor.on('symbol:instance:add', ({ component }) => { ... });
```
* `symbol:instance:remove` Root instance symbol removed.
- `symbol:instance:remove` Root instance symbol removed.
```js
editor.on('symbol:instance:remove', ({ component }) => { ... });
```
* `symbol:instance` Catch-all event related to root instance symbol updates.
- `symbol:instance` Catch-all event related to root instance symbol updates.
```js
editor.on('symbol:instance', ({ event, component }) => { ... });
```
* `symbol` Catch-all event for any symbol update (main or instance).
- `symbol` Catch-all event for any symbol update (main or instance).
```js
editor.on('symbol', () => { ... });
```
## Example
Below is a basic UI implementation leveraging the Symbols API:
<demo-viewer value="ta19s6go" height="500" darkcode show/>
<!-- Demo template, here for reference
@ -300,7 +288,6 @@ const app = new Vue({
</script>
-->
[Component]: </modules/Components.html>
[Components]: </modules/Components.html>
[Components API]: </api/component.html>
[Component]: /modules/Components.html
[Components]: /modules/Components.html
[Components API]: /api/component.html

150
docs/modules/Assets.md

@ -10,7 +10,6 @@ In this section, you will see how to setup and take the full advantage of built-
[[toc]]
## Configuration
To change default configurations you'd need to pass the `assetManager` property with the main configuration object
@ -33,10 +32,6 @@ const amConfig = editor.AssetManager.getConfig();
Check the full list of available options here: [Asset Manager Config](https://github.com/GrapesJS/grapesjs/blob/master/src/asset_manager/config/config.ts)
## Initialization
The Asset Manager is ready to work by default, so pass few URLs to see them loaded
@ -68,15 +63,12 @@ const editor = grapesjs.init({
});
```
If you want a complete list of available properties check out the source [AssetImage Model](https://github.com/GrapesJS/grapesjs/blob/dev/src/asset_manager/model/AssetImage.ts)
The built-in Asset Manager modal is implemented and is showing up when requested. By default, you can make it appear by dragging Image Components in canvas, double clicking on images and all other stuff related to images (eg. CSS styling)
<img :src="$withBase('/assets-builtin-modal.png')">
<!--
Making the modal appear is registered with a command, so you can make it appear with this
@ -97,21 +89,14 @@ editor.runCommand('open-assets', {
Now you should be able to change the image of the component.
-->
## 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
```js
const editor = grapesjs.init({
...
@ -128,7 +113,6 @@ const editor = grapesjs.init({
});
```
### 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
@ -155,7 +139,6 @@ 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:
@ -172,11 +155,10 @@ When the uploading is over, by default (via config parameter `autoAdd: 1`), the
width: 200,
},
// ...
]
];
}
```
<!-- Deprecated
### Setup Dropzone
@ -198,10 +180,6 @@ const editor = grapesjs.init({
});
``` -->
## Programmatic usage
If you need to manage your assets programmatically you have to use its [APIs][API-Asset-Manager]
@ -212,8 +190,9 @@ const am = editor.AssetManager;
```
First of all, it's worth noting that Asset Manager keeps 2 collections of assets:
* **global** - which is just the one with all available assets, you can get it with `am.getAll()`
* **visible** - this is the collection which is currently rendered by the Asset Manager, you get it with `am.getAllVisible()`
- **global** - which is just the one with all available assets, you can get it with `am.getAll()`
- **visible** - this is the collection which is currently rendered by the Asset Manager, you get it with `am.getAllVisible()`
This allows you to decide which assets to show and when. Let's say we'd like to have a category switcher, first of all you gonna add to the **global** collection all your assets (which you may already defined at init by `config.assetManager.assets = [...]`)
@ -223,13 +202,15 @@ am.add([
// You can pass any custom property you want
category: 'c1',
src: 'http://placehold.it/350x250/78c5d6/fff/image1.jpg',
}, {
},
{
category: 'c1',
src: 'http://placehold.it/350x250/459ba8/fff/image2.jpg',
}, {
},
{
category: 'c2',
src: 'http://placehold.it/350x250/79c267/fff/image3.jpg',
}
},
// ...
]);
```
@ -240,8 +221,8 @@ Now if you call the `render()`, without an argument, you will see all the assets
// without any argument
am.render();
am.getAll().length // <- 3
am.getAllVisible().length // <- 3
am.getAll().length; // <- 3
am.getAllVisible().length; // <- 3
```
Ok, now let's show only assets form the first category
@ -249,12 +230,10 @@ Ok, now let's show only assets form the first category
```js
const assets = am.getAll();
am.render(assets.filter(
asset => asset.get('category') == 'c1'
));
am.render(assets.filter((asset) => asset.get('category') == 'c1'));
am.getAll().length // Still have 3 assets
am.getAllVisible().length // but only 2 are shown
am.getAll().length; // Still have 3 assets
am.getAllVisible().length; // but only 2 are shown
```
You can also mix arrays of assets
@ -262,6 +241,7 @@ 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`
@ -295,25 +275,21 @@ 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();
}
}
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 default UI.
@ -322,24 +298,23 @@ All you have to do is to indicate the editor your intent to use a custom UI and
```js
const editor = grapesjs.init({
// ...
assetManager: {
// ...
assetManager: {
// ...
custom: true,
},
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.
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.
});
```
@ -354,30 +329,30 @@ How to approach the case when your Asset Manager is a completely independent/ext
```js
const editor = grapesjs.init({
// ...
assetManager: {
// ...
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
},
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()`.
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
@ -581,13 +556,8 @@ am.addType('image', {
})
``` -->
## Events
For a complete list of available events, you can check it [here](/api/assets.html#available-events).
[API-Asset-Manager]: </api/assets.html>
[API-Asset-Manager]: /api/assets.html

97
docs/modules/Blocks.md

@ -18,7 +18,6 @@ This guide is referring to GrapesJS v0.17.27 or higher
[[toc]]
## Configuration
To change the default configurations you have to pass the `blockManager` property with the main configuration object.
@ -35,7 +34,6 @@ const editor = grapesjs.init({
Check the full list of available options here: [Block Manager Config](https://github.com/GrapesJS/grapesjs/blob/master/src/block_manager/config/config.ts)
## Initialization
By default, Block Manager UI is considered a hidden component. Currently, the GrapesJS core, renders default panels and buttons that allow you to show them, but in long term, this is something that might will change. Here below you can see how to init the editor without default panels and immediately rendered Block Manager UI.
@ -65,20 +63,18 @@ const editor = grapesjs.init({
// We want to activate it once dropped in the canvas.
activate: true,
// select: true, // Default with `activate: true`
}
},
],
}
},
});
```
## Block content types
The key of connecting blocks to components is the `block.content` property and what we passed in the example above is the [Component Definition]. This is the component-oriented way to create blocks and this is how we highly recommend the creation of your blocks.
### Component-oriented
The `content` can accept different formats, like an HTML string (which will be parsed and converted to components), but the component-oriented approach is the most precise as you can keep the control of your each dropped block in the canvas. Another advice is to keep your blocks' [Component Definition] as light as possible, if you're defining a lot of redundant properties, probably it makes sense to create another dedicated component, this might reduce the size of your project JSON file. Here an example:
```js
@ -98,6 +94,7 @@ editor.Components.addType('my-cmp', {
{ ..., content: { type: 'my-cmp', prop1: 'value1-EXT', prop2: 'value2-EXT' } }
]
```
Here we're reusing the same component multiple times with the same set of properties (just an example, makes more sense with composed content of components), this can be reduced to something like this.
```js
@ -121,7 +118,9 @@ editor.Components.addType('my-cmp-alt', {
```
### HTML strings
Using HTML strings as `content` is not wrong, in some cases you don't need the finest control over components and want to leave the user full freedom on template composition (eg. static site builder editor with HTML copy-pasted from a framework like [Tailwind Components](https://tailwindcomponents.com/))
```js
// Your block
{
@ -130,9 +129,10 @@ Using HTML strings as `content` is not wrong, in some cases you don't need the f
<div class="el-Y el-A">Element A</div>
<div class="el-Y el-B">Element B</div>
<div class="el-Y el-C">Element C</div>
</div>`
</div>`;
}
```
In such a case, all rendered elements will be converted to the best suited default component (eg. `.el-Y` elements will be treated like `text` components). The user will be able to style and drag them with no particular restrictions.
Thanks to Components' [isComponet](Components.html#iscomponent) feature (executed post parsing), you're still able to bind your rendered elements to components and enforce an extra logic. Here an example how you would enforce all `.el-Y` elements to be placed only inside `.el-X` one, without touching any part of the original HTML used in the `content`.
@ -141,15 +141,16 @@ Thanks to Components' [isComponet](Components.html#iscomponent) feature (execute
// Your component
editor.Components.addType('cmp-Y', {
// Detect '.el-Y' elements
isComponent: el => el.classList?.contains('el-Y'),
isComponent: (el) => el.classList?.contains('el-Y'),
model: {
defaults: {
name: 'Component Y', // Simple custom name
draggable: '.el-X', // Add `draggable` logic
}
}
},
},
});
```
Another alternative is to leverage `data-gjs-*` attributes to attach properties to components.
::: tip
@ -164,7 +165,7 @@ You can use most of the available [Component properties](/api/component.html#pro
<div data-gjs-type="cmp-Y" class="el-Y el-A">Element A</div>
<div data-gjs-type="cmp-Y" class="el-Y el-B">Element B</div>
<div data-gjs-type="cmp-Y" class="el-Y el-C">Element C</div>
</div>`
</div>`;
}
// Component
editor.Components.addType('cmp-Y', {
@ -173,8 +174,8 @@ editor.Components.addType('cmp-Y', {
defaults: {
name: 'Component Y', // Simple custom name
draggable: '.el-X', // Add `draggable` logic
}
}
},
},
});
// -- [Option 2]: Declare properties in HTML strings (less recommended option) --
@ -184,7 +185,7 @@ editor.Components.addType('cmp-Y', {
<div data-gjs-name="Component Y" data-gjs-draggable=".el-X" class="el-Y el-A">Element A</div>
<div data-gjs-name="Component Y" data-gjs-draggable=".el-X" class="el-Y el-B">Element B</div>
<div data-gjs-name="Component Y" data-gjs-draggable=".el-X" class="el-Y el-C">Element C</div>
</div>`
</div>`;
}
// No need for a custom component.
// You're already defining properties of each element.
@ -193,9 +194,10 @@ editor.Components.addType('cmp-Y', {
Here we showed all the possibilities you have with HTML strings, but we strongly advise against the abuse of the `Option 2` and to stick to a more component-oriented approach.
Without a proper component type, not only your HTML will be harder to read, but all those defined properties will be "hard-coded" to a generic component of those elements. So, if one day you decide to "upgrade" the logic of the component (eg. `draggable: '.el-X'` -> `draggable: '.el-X, .el-Z'`), you won't be able.
### Mixed
It's also possible to mix components with HTML strings by passing an array.
```js
{
// ...
@ -208,7 +210,6 @@ It's also possible to mix components with HTML strings by passing an array.
}
```
## Important caveats
::: danger Read carefully
@ -218,6 +219,7 @@ It's also possible to mix components with HTML strings by passing an array.
### Avoid non serializable properties
Don't put non serializable properties, like functions, in your blocks, keep them only in your components.
```js
// Your block
{
@ -227,10 +229,13 @@ Don't put non serializable properties, like functions, in your blocks, keep them
},
}
```
This will work, but if you try to save and reload a stored project, those will disappear.
### Avoid styles
Don't put styles in your blocks, keep them always in your components.
```js
// Your block
{
@ -251,17 +256,15 @@ Don't put styles in your blocks, keep them always in your components.
],
}
```
<!-- Styles imported via component definition, by using the `styles` property, are connected to that specific component type. This allows the editor to remove automatically those styles if all related components are deleted. -->
With the component-oriented approach, you put yourself in a risk of conflicting styles and having a lot of useless redundant styles definitions in your project JSON.
With the HTML string, if you remove all related elements, the editor is not able to clean those styles from the project JSON, as there is no safe way to connect them.
## Programmatic usage
If you need to manage your blocks programmatically you can use its [APIs][Blocks API].
::: warning
@ -269,6 +272,7 @@ All Blocks API methods update mainly your Block Manager UI, it has nothing to do
:::
Below an example of commonly used methods.
```js
// Get the BlockManager module first
const bm = editor.Blocks; // `Blocks` is an alias of `BlockManager`
@ -294,32 +298,28 @@ const removedBlock = bm.remove('BLOCK-ID-2');
To know more about the available block properties, check the [Block API Reference][Block].
## Customization
The default Block Manager UI is great for simple things, but except the possibility to tweak some CSS style, adding more complex elements requires a replace of the default UI.
All you have to do is to indicate the editor your intent to use a custom UI and then subscribe to the `block:custom` event that will give you all the information on any requested change.
```js
const editor = grapesjs.init({
// ...
blockManager: {
// ...
blockManager: {
// ...
custom: true,
},
custom: true,
},
});
editor.on('block:custom', props => {
// The `props` will contain all the information you need in order to update your UI.
// props.blocks (Array<Block>) - Array of all blocks
// props.dragStart (Function<Block>) - A callback to trigger the start of block dragging.
// props.dragStop (Function<Block>) - A callback to trigger the stop of block dragging.
// props.container (HTMLElement) - The default element where you can append your UI
// Here you would put the logic to render/update your UI.
editor.on('block:custom', (props) => {
// The `props` will contain all the information you need in order to update your UI.
// props.blocks (Array<Block>) - Array of all blocks
// props.dragStart (Function<Block>) - A callback to trigger the start of block dragging.
// props.dragStop (Function<Block>) - A callback to trigger the stop of block dragging.
// props.container (HTMLElement) - The default element where you can append your UI
// Here you would put the logic to render/update your UI.
});
```
@ -329,15 +329,10 @@ Here an example of using custom Block Manager with a Vue component.
From the demo above you can also see how we decided to hide our custom Block Manager and append it to the default container, but that is up to your preferences.
## Events
For a complete list of available events, you can check it [here](/api/block_manager.html#available-events).
<!--
## Custom render <Badge text="0.14.55+"/>
@ -383,13 +378,9 @@ blockManager.add('some-block-id', {
<img :src="$withBase('/block-custom-render2.jpg')">
-->
[Block]: </api/block.html>
[Component]: </api/component.html>
[Components]: <Components.html>
[Getting Started]: </getting-started.html>
[Blocks API]: </api/block_manager.html>
[Component Definition]: <Components.html#component-definition>
[Block]: /api/block.html
[Component]: /api/component.html
[Components]: Components.html
[Getting Started]: /getting-started.html
[Blocks API]: /api/block_manager.html
[Component Definition]: Components.html#component-definition

65
docs/modules/Canvas.md

@ -14,7 +14,6 @@ This guide is referring to GrapesJS v0.21.5 or higher
[[toc]]
## Configuration
To change the default configurations you have to pass the `canvas` property with the main configuration object.
@ -30,7 +29,6 @@ const editor = grapesjs.init({
Check the full list of available options here: [Canvas Config](https://github.com/GrapesJS/grapesjs/blob/master/src/canvas/config/config.ts)
## Canvas Spots
Canvas spots are elements drawn on top of the canvas. They can be used to represent anything you might need but the most common use case of canvas spots is rendering information and managing components rendered in the canvas.
@ -47,9 +45,11 @@ The `select` type is responsable for showing selected components and rendering t
::: tip
Get the toolbar items from the component.
```js
const toolbarItems = editor.getSelected().toolbar;
```
:::
#### Resize type
@ -60,9 +60,11 @@ The `resize` type allows resizing of a component, based on the component's resiz
::: tip
Get the component resizable options.
```js
const resizable = editor.getSelected().resizable;
```
:::
#### Target type
@ -73,17 +75,19 @@ The `target` type is used to highlight the component, like during the drag & dro
::: warning
The default green position indicator is not part of the spot but you can easily customize it via CSS.
```css
.gjs-placeholder.horizontal {
border-color: transparent red;
border-color: transparent red;
}
.gjs-placeholder.vertical {
border-color: red transparent;
border-color: red transparent;
}
.gjs-placeholder-int {
background-color: red;
background-color: red;
}
```
:::
#### Hover type
@ -94,40 +98,36 @@ The `hover` is used to highlight the hovered component and show the component na
::: tip
Get the component name.
```js
const name = editor.getSelected().getName();
```
:::
#### Spacing type
The `spacing` type is used to show component offsets like paddings and margins (visible on the `hover` type image above).
### Disable built-in types
You can disable the rendering of built-in canvas spots (some or all of them) during the editor initialization.
```js
grapesjs.init({
// ...
canvas: {
// Disable only the hover type spot
customSpots: {
hover: true,
},
// Disable all built-in spots
customSpots: true,
// ...
canvas: {
// Disable only the hover type spot
customSpots: {
hover: true,
},
})
// Disable all built-in spots
customSpots: true,
},
});
```
In the next section, we'll see how it's possible to reuse the built-in spots and create your own.
In the next section, we'll see how it's possible to reuse the built-in spots and create your own.
### Spots customization
@ -139,21 +139,28 @@ In the example below we'll see how to reuse the built-in `hover` canvas spot to
Worth noting a few important points:
* Our custom container has to be moved inside the GrapesJS spots container.
- Our custom container has to be moved inside the GrapesJS spots container.
```js
editor.onReady(() => {
Canvas.getSpotsEl().appendChild(this.$el);
Canvas.getSpotsEl().appendChild(this.$el);
});
```
* We pass the `component` to our custom spot, in order to have the style coordinates properly updated when we scroll the page or update the component.
- We pass the `component` to our custom spot, in order to have the style coordinates properly updated when we scroll the page or update the component.
```js
Canvas.addSpot({ type: customSpotType, component });
```
* The single spot is placed properly with `spot.getStyle()`
- The single spot is placed properly with `spot.getStyle()`
```html
<div ... class="spot" :style="spot.getStyle()">...</div>
```
* The spots container, by default, relies on `pointer-events: none`, in order to prevent the spot from blocking the interaction with the components. This is why we have to re-enable the pointer event on the button in order to make it interactable.
- The spots container, by default, relies on `pointer-events: none`, in order to prevent the spot from blocking the interaction with the components. This is why we have to re-enable the pointer event on the button in order to make it interactable.
```css
.spot-text-btn {
/*...*/
@ -266,10 +273,6 @@ Canvas.addSpot({ type: customSpotType, component });
</script>
-->
## Events
For a complete list of available events, you can check it [here](/api/canvas.html#available-events).
For a complete list of available events, you can check it [here](/api/canvas.html#available-events).

83
docs/modules/Commands.md

@ -12,7 +12,6 @@ This guide is referring to GrapesJS v0.14.61 or higher
[[toc]]
## Basic configuration
You can create your commands already from the initialization step by passing them in the `commands.defaults` options:
@ -43,7 +42,7 @@ Most commonly commands are created dynamically post-initialization, in that case
```js
const commands = editor.Commands;
commands.add('my-command-id', editor => {
commands.add('my-command-id', (editor) => {
alert('This is my command');
});
@ -83,45 +82,37 @@ commands.add('my-command-id', (editor, sender, options = {}) => {
The second argument, `sender`, just indicates who requested the command, in our case will be always the `editor`
Until now there is nothing exciting except a common entry point for functions, but we'll see later its real advantages.
## Default commands
GrapesJS comes along with some default set of commands and you can get a list of all currently available commands via `editor.Commands.getAll()`. This will give you an object of all available commands, so, also those added later, like via plugins. You can recognize default commands by their namespace `core:*`, we also recommend to use namespaces in your own custom commands, but let's get a look more in detail here:
* [`core:canvas-clear`](https://github.com/GrapesJS/grapesjs/blob/dev/src/commands/view/CanvasClear.ts) - Clear all the content from the canvas (HTML and CSS)
* [`core:component-delete`](https://github.com/GrapesJS/grapesjs/blob/dev/src/commands/view/ComponentDelete.ts) - Delete a component
* [`core:component-enter`](https://github.com/GrapesJS/grapesjs/blob/dev/src/commands/view/ComponentEnter.ts) - Select the first children component of the selected one
* [`core:component-exit`](https://github.com/GrapesJS/grapesjs/blob/dev/src/commands/view/ComponentExit.ts) - Select the parent component of the current selected one
* [`core:component-next`](https://github.com/GrapesJS/grapesjs/blob/dev/src/commands/view/ComponentNext.ts) - Select the next sibling component
* [`core:component-prev`](https://github.com/GrapesJS/grapesjs/blob/dev/src/commands/view/ComponentPrev.ts) - Select the previous sibling component
* [`core:component-outline`](https://github.com/GrapesJS/grapesjs/blob/dev/src/commands/view/SwitchVisibility.ts) - Enable outline border on components
* [`core:component-offset`](https://github.com/GrapesJS/grapesjs/blob/dev/src/commands/view/ShowOffset.ts) - Enable components offset (margins, paddings)
* [`core:component-select`](https://github.com/GrapesJS/grapesjs/blob/dev/src/commands/view/SelectComponent.ts) - Enable the process of selecting components in the canvas
* [`core:copy`](https://github.com/GrapesJS/grapesjs/blob/dev/src/commands/view/CopyComponent.ts) - Copy the current selected component
* [`core:paste`](https://github.com/GrapesJS/grapesjs/blob/dev/src/commands/view/PasteComponent.ts) - Paste copied component
* [`core:preview`](https://github.com/GrapesJS/grapesjs/blob/dev/src/commands/view/Preview.ts) - Show the preview of the template in canvas
* [`core:fullscreen`](https://github.com/GrapesJS/grapesjs/blob/dev/src/commands/view/Fullscreen.ts) - Set the editor fullscreen
* [`core:open-code`](https://github.com/GrapesJS/grapesjs/blob/dev/src/commands/view/ExportTemplate.ts) - Open a default panel with the template code
* [`core:open-layers`](https://github.com/GrapesJS/grapesjs/blob/dev/src/commands/view/OpenLayers.ts) - Open a default panel with layers
* [`core:open-styles`](https://github.com/GrapesJS/grapesjs/blob/dev/src/commands/view/OpenStyleManager.ts) - Open a default panel with the style manager
* [`core:open-traits`](https://github.com/GrapesJS/grapesjs/blob/dev/src/commands/view/OpenTraitManager.ts) - Open a default panel with the trait manager
* [`core:open-blocks`](https://github.com/GrapesJS/grapesjs/blob/dev/src/commands/view/OpenBlocks.ts) - Open a default panel with the blocks
* [`core:open-assets`](https://github.com/GrapesJS/grapesjs/blob/dev/src/commands/view/OpenAssets.ts) - Open a default panel with the assets
* `core:undo` - Call undo operation
* `core:redo` - Call redo operation
<!-- * `core:canvas-move` -->
<!-- * `core:component-drag` -->
<!-- * `core:component-style-clear` -->
<!-- tlb-clone tlb-delete tlb-move -->
- [`core:canvas-clear`](https://github.com/GrapesJS/grapesjs/blob/dev/src/commands/view/CanvasClear.ts) - Clear all the content from the canvas (HTML and CSS)
- [`core:component-delete`](https://github.com/GrapesJS/grapesjs/blob/dev/src/commands/view/ComponentDelete.ts) - Delete a component
- [`core:component-enter`](https://github.com/GrapesJS/grapesjs/blob/dev/src/commands/view/ComponentEnter.ts) - Select the first children component of the selected one
- [`core:component-exit`](https://github.com/GrapesJS/grapesjs/blob/dev/src/commands/view/ComponentExit.ts) - Select the parent component of the current selected one
- [`core:component-next`](https://github.com/GrapesJS/grapesjs/blob/dev/src/commands/view/ComponentNext.ts) - Select the next sibling component
- [`core:component-prev`](https://github.com/GrapesJS/grapesjs/blob/dev/src/commands/view/ComponentPrev.ts) - Select the previous sibling component
- [`core:component-outline`](https://github.com/GrapesJS/grapesjs/blob/dev/src/commands/view/SwitchVisibility.ts) - Enable outline border on components
- [`core:component-offset`](https://github.com/GrapesJS/grapesjs/blob/dev/src/commands/view/ShowOffset.ts) - Enable components offset (margins, paddings)
- [`core:component-select`](https://github.com/GrapesJS/grapesjs/blob/dev/src/commands/view/SelectComponent.ts) - Enable the process of selecting components in the canvas
- [`core:copy`](https://github.com/GrapesJS/grapesjs/blob/dev/src/commands/view/CopyComponent.ts) - Copy the current selected component
- [`core:paste`](https://github.com/GrapesJS/grapesjs/blob/dev/src/commands/view/PasteComponent.ts) - Paste copied component
- [`core:preview`](https://github.com/GrapesJS/grapesjs/blob/dev/src/commands/view/Preview.ts) - Show the preview of the template in canvas
- [`core:fullscreen`](https://github.com/GrapesJS/grapesjs/blob/dev/src/commands/view/Fullscreen.ts) - Set the editor fullscreen
- [`core:open-code`](https://github.com/GrapesJS/grapesjs/blob/dev/src/commands/view/ExportTemplate.ts) - Open a default panel with the template code
- [`core:open-layers`](https://github.com/GrapesJS/grapesjs/blob/dev/src/commands/view/OpenLayers.ts) - Open a default panel with layers
- [`core:open-styles`](https://github.com/GrapesJS/grapesjs/blob/dev/src/commands/view/OpenStyleManager.ts) - Open a default panel with the style manager
- [`core:open-traits`](https://github.com/GrapesJS/grapesjs/blob/dev/src/commands/view/OpenTraitManager.ts) - Open a default panel with the trait manager
- [`core:open-blocks`](https://github.com/GrapesJS/grapesjs/blob/dev/src/commands/view/OpenBlocks.ts) - Open a default panel with the blocks
- [`core:open-assets`](https://github.com/GrapesJS/grapesjs/blob/dev/src/commands/view/OpenAssets.ts) - Open a default panel with the assets
- `core:undo` - Call undo operation
- `core:redo` - Call redo operation
<!-- * `core:canvas-move` -->
<!-- * `core:component-drag` -->
<!-- * `core:component-style-clear` -->
<!-- tlb-clone tlb-delete tlb-move -->
## Stateful commands
@ -203,17 +194,13 @@ If you run it, close the modal (eg. by clicking the 'x' on top) and then try to
In the example above, we make use of few helper methods from the Modal module (`onceClose`) and the command itself (`stopCommand`) but obviously, the logic might be different due to your requirements and specific UI.
## Extending
Another big advantage of commands is the possibility to easily extend or override them with another command.
Let's take a simple example
```js
commands.add('my-command-1', editor => {
commands.add('my-command-1', (editor) => {
alert('This is command 1');
});
```
@ -221,7 +208,7 @@ commands.add('my-command-1', editor => {
If you need to overwrite this command with another one, just add it and keep the same id.
```js
commands.add('my-command-1', editor => {
commands.add('my-command-1', (editor) => {
alert('This is command 1 overwritten');
});
```
@ -253,15 +240,10 @@ commands.extend('my-command-2', {
});
```
## Events
The Commands module offers also a set of events that you can use to intercept the command flow for adding more functionality or even interrupting it.
### Intercept run and stop
By using our previously created `my-command-modal` command let's see which events we can listen to
@ -288,16 +270,15 @@ editor.on('command:stop:before:my-command-modal:before', () => {
If you need, you can also listen to all commands
```js
editor.on('command:run', commandId => {
editor.on('command:run', (commandId) => {
console.log('Run', commandId);
});
editor.on('command:stop', commandId => {
editor.on('command:stop', (commandId) => {
console.log('Stop', commandId);
});
```
### Interrupt command flow
Sometimes you might need to interrupt the execution of an existant command due to some condition. In that case, you have to use `command:run:before:{COMMAND-ID}` event and set to `true` the abort option
@ -305,7 +286,7 @@ Sometimes you might need to interrupt the execution of an existant command due t
```js
const condition = 1;
editor.on('command:run:before:my-command-modal', options => {
editor.on('command:run:before:my-command-modal', (options) => {
if (condition) {
options.abort = true;
console.log('Prevent `my-command-modal` from execution');
@ -313,8 +294,6 @@ editor.on('command:run:before:my-command-modal', options => {
});
```
## Conclusion
The Commands module is quite simple but, at the same time, really powerful if used correctly. So, if you're creating a plugin for GrapesJS, use commands as much as possible, this will allow higher reusability and control over your logic.

53
docs/modules/Components-js.md

@ -13,14 +13,13 @@ To get a better understanding of the content in this guide, we recommend reading
[[toc]]
## Basic scripts
Let's see how to create a component with scripts.
```js
// This is our custom script (avoid using arrow functions)
const script = function() {
const script = function () {
alert('Hi');
// `this` is bound to the component element
console.log('the element', this);
@ -36,9 +35,9 @@ editor.Components.addType('comp-with-js', {
width: '100px',
height: '100px',
background: 'red',
}
}
}
},
},
},
});
// Create a block for the component, so we can drop it easily
@ -48,6 +47,7 @@ editor.Blocks.add('test-block', {
content: { type: 'comp-with-js' },
});
```
Now if you drag the new block inside the canvas you'll see an alert and the message in console, as you might expect.
One thing worth noting is that `this` context is bound to the component's element, so, for example, if you need to change its property, you'd do `this.innerHTML = 'inner content'`.
@ -58,12 +58,12 @@ One thing you should take into account is how the script is bound to component o
<script>
var items = document.querySelectorAll('#c764');
for (var i = 0, len = items.length; i < len; i++) {
(function(){
(function () {
// START component code
alert('Hi');
console.log('the element', this)
console.log('the element', this);
// END component code
}.bind(items[i]))();
}).bind(items[i])();
}
</script>
```
@ -76,12 +76,12 @@ As you see the editor attaches a unique ID to all components with scripts and re
<script>
var items = document.querySelectorAll('#c764, #c765');
for (var i = 0, len = items.length; i < len; i++) {
(function(){
(function () {
// START component code
alert('Hi');
console.log('the element', this)
console.log('the element', this);
// END component code
}.bind(items[i]))();
}).bind(items[i])();
}
</script>
```
@ -99,7 +99,7 @@ That means **you can't use stuff outside of the function scope**. Take a look at
```js
const myVar = 'John';
const script = function() {
const script = function () {
alert('Hi ' + myVar);
console.log('the element', this);
};
@ -112,10 +112,10 @@ This won't work. You'll get an error of the undefined `myVar`. The final HTML sh
<script>
var items = document.querySelectorAll('#c764');
for (var i = 0, len = items.length; i < len; i++) {
(function(){
(function () {
alert('Hi ' + myVar); // <- ERROR: undefined myVar
console.log('the element', this);
}.bind(items[i]))();
}).bind(items[i])();
}
</script>
```
@ -127,7 +127,7 @@ You can do it by using the `script-props` property on your component.
```js
// The `props` argument will contain only the properties you have declared in `script-props`
const script = function(props) {
const script = function (props) {
const myLibOpts = {
prop1: props.myprop1,
prop2: props.myprop2,
@ -152,24 +152,22 @@ editor.Components.addType('comp-with-js', {
{ value: 'value1', name: 'Value 1' },
{ value: 'value2', name: 'Value 2' },
],
}, {
},
{
type: 'number',
name: 'myprop2',
changeProp: true,
}
},
],
// Define which properties to pass (this will also reset your script on their changes)
'script-props': ['myprop1', 'myprop2'],
// ...
}
}
},
},
});
```
Now, if you try to change traits, you'll also see how the script will be triggered with the new updated properties.
Now, if you try to change traits, you'll also see how the script will be triggered with the new updated properties.
## Dependencies
@ -182,8 +180,8 @@ The component related approach is definitely the best one, as the dependencies w
All you have to do is to load your dependencies before executing your initialization script.
```js
const script = function(props) {
const initLib = function() {
const script = function (props) {
const initLib = function () {
const el = this;
const myLibOpts = {
prop1: props.myprop1,
@ -217,8 +215,7 @@ const editor = grapesjs.init({
}
});
```
Keep in mind that the editor won't render those dependencies in the exported HTML (eg. via `editor.getHtml()`), so it's up to you how to include them in the final page where the final HTML is rendered.
Keep in mind that the editor won't render those dependencies in the exported HTML (eg. via `editor.getHtml()`), so it's up to you how to include them in the final page where the final HTML is rendered.
[Traits]: <Traits.html>
[Traits]: Traits.html

287
docs/modules/Components.md

@ -12,16 +12,12 @@ This guide is referring to GrapesJS v0.15.8 or higher
[[toc]]
## How Components work?
Let's see in detail how components work by looking at all the steps from adding an HTML string to the editor.
::: tip
All the following snippets can be run directly in console from the [main demo](https://grapesjs.com/demo.html)
All the following snippets can be run directly in console from the [main demo](https://grapesjs.com/demo.html)
:::
This is how we can add new components to the canvas:
@ -43,17 +39,19 @@ editor.getWrapper().append(`<div>...`);
::: tip
If you need to append a component at a specific position, you can use `at` option. So, to add a component on top of all others (in the same collection) you would use
```js
component.append('<div>...', { at: 0 })
component.append('<div>...', { at: 0 });
```
or in the middle
```js
const { length } = component.components();
component.append('<div>...', { at: parseInt(length / 2, 10) })
component.append('<div>...', { at: parseInt(length / 2, 10) });
```
:::
:::
### Component Definition
@ -85,8 +83,6 @@ You might notice the result is similar to what is generally called a **Virtual D
The meaning of properties like `tagName`, `attributes` and `components` are quite obvious, but what about `type`?! This particular property specifies the **Component Type** of our **Component Definition** (you check the list of default components [below](#built-in-component-types)) and if it's omitted, the default one will be used `type: 'default'`.
At this point, a good question would be, how the editor assigns those types by starting from a simple HTML string? This step is identified as **Component Recognition** and it's explained in detail in the next paragraph.
### Component Recognition and Component Type Stack
As we mentioned before, when you pass an HTML string as a component to the editor, that string is parsed and compiled to the [Component Definition] with a new `type` property. To understand what `type` should be assigned, for each parsed HTML Element, the editor iterates over all the defined components, called **Component Type Stack**, and checks via `isComponent` method (we will see it later) if that component type is appropriate for that element. The Component Type Stack is just a simple array of component types but what matters is the order of those types. Any new added custom **Component Type** (we'll see later how to create them) goes on top of the Component Type Stack and each element returned from the parser iterates the stack from top to bottom (the last element of the stack is the `default` one), the iteration stops once one of the component returns a truthy value from the `isComponent` method.
@ -98,8 +94,6 @@ If you're importing big string chunks of HTML code you might want to improve the
Read [here](#setup-jsx-syntax) about how to setup JSX syntax parser
:::
### Component instance
Once the **Component Definition** is ready and the type is assigned, the [Component] instance can be created (known also as the **Model**). Let's step back to our previous example with the HTML string, the result of the `append` method is an array of added components.
@ -113,19 +107,23 @@ const component = editor.addComponents(`<div>
The Component instance contains properties and methods which allows you to obtain its data and change them.
You can read properties with the `get` method, like, for example, the `type`
```js
const componentType = component.get('type'); // eg. 'image'
```
and to update properties you'd use `set`, which might change the way a component behaves in the canvas.
```js
// Make the component not draggable
component.set('draggable', false);
```
You can also use methods like `getAttributes`, `setAttributes`, `components`, etc.
```js
const innerComponents = component.components();
innerComponents.forEach(comp => console.log(comp.toHTML()));
innerComponents.forEach((comp) => console.log(comp.toHTML()));
// Update component content
component.components(`<div>Component 1</div><div>Component 2</div>`);
```
@ -142,7 +140,7 @@ This will return a string containing the HTML of the component and all of its ch
The component implements also `toJSON` methods so you can get its JSON structure in this way
```js
JSON.stringify(component)
JSON.stringify(component);
```
::: tip
@ -151,8 +149,6 @@ For storing/loading all the components you should rely on the [Storage Manager](
So, the **Component instance** is responsible for the **final data** (eg. HTML, JSON) of your templates. If you need, for example, to update/add some attribute in the HTML you need to update its component (eg. `component.addAttributes({ title: 'Title added' })`), so the Component/Model is your **Source of Truth**.
### Component rendering
Another important part of components is how they are rendered in the **canvas**, this aspect is handled by its **View**. It has nothing to do with the **final HTML data**, you can return a big `<div>...</div>` string as HTML of your component but render it as a simple image in the canvas (think about placeholders for complex/dynamic data).
@ -167,50 +163,40 @@ const component = editor.getSelected();
// Get the View
const view = component.getView();
// Get the DOM element
const el = component.getEl();
const el = component.getEl();
```
Generally, the View is something you wouldn't need to change as the default one handles already the sync with the Model but in case you'd need more control over elements (eg. custom UI in canvas) you'll probably need to create a custom component type and extend the default View with your logic. We'll see later how to create custom Component Types.
So far we have seen the core concept behind Components and how they work. The **Model/Component** is the **source of truth** for the final code of templates (eg. the HTML export relies on it) and the **View/ComponentView** is what is used by the editor to **preview our components** to users in the canvas.
<!--
TODO
A more advanced use case of custom components is an implementation of a custom renderer inside of them
-->
## Built-in Component Types
Here below you can see the list of built-in component types, ordered by their position in the **Component Type Stack**
* [`cell`](https://github.com/GrapesJS/grapesjs/blob/dev/src/dom_components/model/ComponentTableCell.ts) - Component for handle `<td>` and `<th>` elements
* [`row`](https://github.com/GrapesJS/grapesjs/blob/dev/src/dom_components/model/ComponentTableRow.ts) - Component for handle `<tr>` elements
* [`table`](https://github.com/GrapesJS/grapesjs/blob/dev/src/dom_components/model/ComponentTable.ts) - Component for handle `<table>` elements
* [`thead`](https://github.com/GrapesJS/grapesjs/blob/dev/src/dom_components/model/ComponentTableHead.ts) - Component for handle `<thead>` elements
* [`tbody`](https://github.com/GrapesJS/grapesjs/blob/dev/src/dom_components/model/ComponentTableBody.ts) - Component for handle `<tbody>` elements
* [`tfoot`](https://github.com/GrapesJS/grapesjs/blob/dev/src/dom_components/model/ComponentTableFoot.ts) - Component for handle `<tfoot>` elements
* [`map`](https://github.com/GrapesJS/grapesjs/blob/dev/src/dom_components/model/ComponentMap.ts) - Component for handle `<a>` elements
* [`link`](https://github.com/GrapesJS/grapesjs/blob/dev/src/dom_components/model/ComponentLink.ts) - Component for handle `<a>` elements
* [`label`](https://github.com/GrapesJS/grapesjs/blob/dev/src/dom_components/model/ComponentLabel.ts) - Component for handle properly `<label>` elements
* [`video`](https://github.com/GrapesJS/grapesjs/blob/dev/src/dom_components/model/ComponentVideo.ts) - Component for videos
* [`image`](https://github.com/GrapesJS/grapesjs/blob/dev/src/dom_components/model/ComponentImage.ts) - Component for images
* [`script`](https://github.com/GrapesJS/grapesjs/blob/dev/src/dom_components/model/ComponentScript.ts) - Component for handle `<script>` elements
* [`svg`](https://github.com/GrapesJS/grapesjs/blob/dev/src/dom_components/model/ComponentSvg.ts) - Component for handle SVG elements
* [`comment`](https://github.com/GrapesJS/grapesjs/blob/dev/src/dom_components/model/ComponentComment.ts) - Component for comments (might be useful for email editors)
* [`textnode`](https://github.com/GrapesJS/grapesjs/blob/dev/src/dom_components/model/ComponentTextNode.ts) - Similar to the textnode in DOM definition, so a text element without a tag element.
* [`text`](https://github.com/GrapesJS/grapesjs/blob/dev/src/dom_components/model/ComponentText.ts) - A simple text component that can be edited inline
* [`wrapper`](https://github.com/GrapesJS/grapesjs/blob/dev/src/dom_components/model/ComponentWrapper.ts) - The canvas need to contain a root component, a wrapper, this component was made to identify it
* [`default`](https://github.com/GrapesJS/grapesjs/blob/dev/src/dom_components/model/Component.ts) Default base component
- [`cell`](https://github.com/GrapesJS/grapesjs/blob/dev/src/dom_components/model/ComponentTableCell.ts) - Component for handle `<td>` and `<th>` elements
- [`row`](https://github.com/GrapesJS/grapesjs/blob/dev/src/dom_components/model/ComponentTableRow.ts) - Component for handle `<tr>` elements
- [`table`](https://github.com/GrapesJS/grapesjs/blob/dev/src/dom_components/model/ComponentTable.ts) - Component for handle `<table>` elements
- [`thead`](https://github.com/GrapesJS/grapesjs/blob/dev/src/dom_components/model/ComponentTableHead.ts) - Component for handle `<thead>` elements
- [`tbody`](https://github.com/GrapesJS/grapesjs/blob/dev/src/dom_components/model/ComponentTableBody.ts) - Component for handle `<tbody>` elements
- [`tfoot`](https://github.com/GrapesJS/grapesjs/blob/dev/src/dom_components/model/ComponentTableFoot.ts) - Component for handle `<tfoot>` elements
- [`map`](https://github.com/GrapesJS/grapesjs/blob/dev/src/dom_components/model/ComponentMap.ts) - Component for handle `<a>` elements
- [`link`](https://github.com/GrapesJS/grapesjs/blob/dev/src/dom_components/model/ComponentLink.ts) - Component for handle `<a>` elements
- [`label`](https://github.com/GrapesJS/grapesjs/blob/dev/src/dom_components/model/ComponentLabel.ts) - Component for handle properly `<label>` elements
- [`video`](https://github.com/GrapesJS/grapesjs/blob/dev/src/dom_components/model/ComponentVideo.ts) - Component for videos
- [`image`](https://github.com/GrapesJS/grapesjs/blob/dev/src/dom_components/model/ComponentImage.ts) - Component for images
- [`script`](https://github.com/GrapesJS/grapesjs/blob/dev/src/dom_components/model/ComponentScript.ts) - Component for handle `<script>` elements
- [`svg`](https://github.com/GrapesJS/grapesjs/blob/dev/src/dom_components/model/ComponentSvg.ts) - Component for handle SVG elements
- [`comment`](https://github.com/GrapesJS/grapesjs/blob/dev/src/dom_components/model/ComponentComment.ts) - Component for comments (might be useful for email editors)
- [`textnode`](https://github.com/GrapesJS/grapesjs/blob/dev/src/dom_components/model/ComponentTextNode.ts) - Similar to the textnode in DOM definition, so a text element without a tag element.
- [`text`](https://github.com/GrapesJS/grapesjs/blob/dev/src/dom_components/model/ComponentText.ts) - A simple text component that can be edited inline
- [`wrapper`](https://github.com/GrapesJS/grapesjs/blob/dev/src/dom_components/model/ComponentWrapper.ts) - The canvas need to contain a root component, a wrapper, this component was made to identify it
- [`default`](https://github.com/GrapesJS/grapesjs/blob/dev/src/dom_components/model/Component.ts) Default base component
## Define Custom Component Type
@ -219,14 +205,14 @@ Now that we know how components work, we can start exploring the process of crea
<u>The first rule of defining new component types is to place the code inside a plugin</u>. This is necessary if you want to load your custom types at the beginning, before any component initialization (eg. a template loaded from DB). The plugin is loaded before component fetch (eg. in case of Storage use) so it's a perfect place to define component types.
```js
const myNewComponentTypes = editor => {
const myNewComponentTypes = (editor) => {
editor.DomComponents.addType(/* API for component type definition */);
};
const editor = grapesjs.init({
container : '#gjs',
container: '#gjs',
// ...
plugins: [ myNewComponentTypes ],
plugins: [myNewComponentTypes],
});
```
@ -235,7 +221,7 @@ Let's say we want to make the editor understand and handle better `<input>` elem
```js
editor.DomComponents.addType('my-input-type', {
// Make the editor understand when to bind `my-input-type`
isComponent: el => el.tagName === 'INPUT',
isComponent: (el) => el.tagName === 'INPUT',
// Model definition
model: {
@ -244,18 +230,15 @@ editor.DomComponents.addType('my-input-type', {
tagName: 'input',
draggable: 'form, form *', // Can be dropped only inside `form` elements
droppable: false, // Can't drop other elements inside
attributes: { // Default attributes
attributes: {
// Default attributes
type: 'text',
name: 'default-name',
placeholder: 'Insert text here',
},
traits: [
'name',
'placeholder',
{ type: 'checkbox', name: 'required' },
],
}
}
traits: ['name', 'placeholder', { type: 'checkbox', name: 'required' }],
},
},
});
```
@ -265,8 +248,6 @@ With this code, the editor will be able to understand simple text `<input>`s, as
To understand better how Traits work you should read its [dedicated page](Traits.html) but we highly suggest to read it after you've finished reading this one
:::
### isComponent
Let's see in detail what we have done so far. The first thing to notice is the `isComponent` function, we have already mentioned its usage in [this](#component-recognition-and-component-type-stack) section and we need it to make the editor understand `<input>` during the component recognition step.
@ -274,7 +255,7 @@ It receives only the `el` argument, which is the parsed HTMLElement node and exp
```js
// ...after editor initialization
editor.addComponents(`<input name="my-test" title="hello"/>`)
editor.addComponents(`<input name="my-test" title="hello"/>`);
```
The resultant Component Definition will be
@ -320,13 +301,12 @@ If you do this:
```js
// ...
// Print elements
isComponent: el => {
console.log(el);
return el.tagName === 'INPUT';
isComponent: (el) => {
console.log(el);
return el.tagName === 'INPUT';
},
// ...
editor.addComponents(`<div>
// ...
editor.addComponents(`<div>
I'm a text node
<!-- I'm a comment node -->
<img alt="Image here"/>
@ -336,7 +316,6 @@ editor.addComponents(`<div>
You will see printing all the nodes, so doing something like this `el.getAttribute('...')` in your `isComponent` (which will work on the `div` but not on the `text node`), without an appropriate check, will break the code.
It's also important to understand that `isComponent` is executed only if the parsing is required (eg. by adding components as HTML string or initializing the editor with `fromElement`). In case the type is already defined, there is no need for the `isComponent` to be executed.
Let's see some examples:
@ -356,12 +335,10 @@ editor.addComponents('<some-element data-gjs-type="some-component">...');
If you define the Component Type without using `isComponent`, the only way for the editor to see that component will be with an explicitly declared type (via an object `{ type: '...' }` or using `data-gjs-type`).
### Model
Now that we got how `isComponent` works we can start to explore the `model` property.
The `model` is probably the one you'll use the most as is what is used for the description of your component and the first thing you can see is its `defaults` key which just stands for *default component properties* and it reflects the already described [Component Definition]
The `model` is probably the one you'll use the most as is what is used for the description of your component and the first thing you can see is its `defaults` key which just stands for _default component properties_ and it reflects the already described [Component Definition]
The model defines also what you will see as the resultant HTML (the export code) and you've probably noticed the use of `tagName` (if not specified the `div` will be used) and `attributes` properties on the model.
@ -421,7 +398,6 @@ modelComponent.set({
// ...
});
// Some helpers
// Get all attributes
@ -434,17 +410,13 @@ modelComponent.addAttributes({ title: 'Test' });
modelComponent.setAttributes({ title: 'Test' });
// Get the collection of all inner components
modelComponent.components().forEach(
inner => console.log(inner.props())
);
modelComponent.components().forEach((inner) => console.log(inner.props()));
// Update the inner content with an HTML string/Component Definitions
const addedComponents = modelComponent.components(`<div>...</div>`);
// Find components by query string
modelComponent.find(`.query-string[example=value]`).forEach(
inner => console.log(inner.props())
);
modelComponent.find(`.query-string[example=value]`).forEach((inner) => console.log(inner.props()));
```
You'll notice that, on any change, the component in the canvas and its export code are changing accordingly
@ -486,7 +458,7 @@ editor.DomComponents.addType('my-input-type', {
handleTitleChange() {
console.log('Attribute title updated: ', this.getAttributes().title);
},
}
},
});
```
@ -494,20 +466,19 @@ You'll find other lifecycle methods, like `init`, [below](#lifecycle-hooks)
Now let's go back to our input component integration and see another useful part for the component customization
### View
Generally, when you create a component in GrapesJS you expect to see in the canvas the preview of what you've defined in the model. Indeed, by default, the editor does the exact thing and updates the element in the canvas when something in the model changes (eg. attributes, tag, etc.) to obtain the classic **WYSIWYG** (What You See Is What You Get) experience. Unfortunately, not always the simplest thing is the right one, by building components for the builder you will notice that sometimes you'll need something more:
* You want to improve the experience of editing of the component.
A perfect example is the TextComponent, its view is enriched with a built-in RTE (Rich Text Editor) which enables the user to edit the text faster by double-clicking on it.
So you'll probably feel a need adding actions to react on some DOM events or even custom UI elements (eg. buttons) around the component.
- You want to improve the experience of editing of the component.
A perfect example is the TextComponent, its view is enriched with a built-in RTE (Rich Text Editor) which enables the user to edit the text faster by double-clicking on it.
* The DOM representation of the component acts differently from what you'd expect, so you need to change some behavior.
An example could be a VideoComponent which, for example, is loaded from Youtube via iframe. Once the iframe is loaded, everything inside it is in a different context, the editor is not able to see it, indeed if you point your cursor on the iframe you'll interact with the video and not the editor, so you can't even select your component. To workaround this "issue", in the render, we disabled the pointer interaction with the iframe and wrapped it with another element (without the wrapper the editor would select the parent component). Obviously, all of these changes have nothing to do with the final code, the result will always be a simple iframe
So you'll probably feel a need adding actions to react on some DOM events or even custom UI elements (eg. buttons) around the component.
* You need to customize the content or fill it with some data from the server
- The DOM representation of the component acts differently from what you'd expect, so you need to change some behavior.
An example could be a VideoComponent which, for example, is loaded from Youtube via iframe. Once the iframe is loaded, everything inside it is in a different context, the editor is not able to see it, indeed if you point your cursor on the iframe you'll interact with the video and not the editor, so you can't even select your component. To workaround this "issue", in the render, we disabled the pointer interaction with the iframe and wrapped it with another element (without the wrapper the editor would select the parent component). Obviously, all of these changes have nothing to do with the final code, the result will always be a simple iframe
- You need to customize the content or fill it with some data from the server
For all of these cases, you can use the `view` in your Component Type Definition. The `<input>` component is probably not the best use case for this scenario but we'll try to cover most of the cases with an example below
@ -549,12 +520,12 @@ editor.DomComponents.addType('my-input-type', {
// If you attach listeners on outside objects remember to unbind
// them in `removed` function in order to avoid memory leaks
this.onDocClick = this.onDocClick.bind(this);
document.addEventListener('click', this.onDocClick)
document.addEventListener('click', this.onDocClick);
},
// Callback triggered when the element is removed from the canvas
removed() {
document.removeEventListener('click', this.onDocClick)
document.removeEventListener('click', this.onDocClick);
},
// Do something with the content once the element is rendered.
@ -578,15 +549,11 @@ editor.DomComponents.addType('my-input-type', {
// None of the DOM change is stored in your template data,
// if you need to store something, update the model properties
el.appendChild(asyncContent);
}
},
},
});
```
## Update Component Type
Updating component types is quite easy, let's see how:
@ -608,7 +575,7 @@ domc.addType('some-component', {
},
init() {
// Ovverride `init` function in `some-component`
}
},
},
// Update the view, if you need
@ -616,8 +583,6 @@ domc.addType('some-component', {
});
```
### Extend Component Type
Sometimes you would need to create a new type by extending another one. Just use `extend` and `extendView` indicating the component to extend.
@ -630,6 +595,7 @@ comps.addType('my-new-component', {
view: { ... }, // Will extend the view from 'other-defined-component'
});
```
```js
comps.addType('my-new-component', {
isComponent: el => {/* ... */},
@ -640,15 +606,16 @@ comps.addType('my-new-component', {
});
```
### Extend parent functions
When you need to reuse functions, from the parent you're extending, you can avoid writing this:
```js
domc.getType('parent-type').model.prototype.init.apply(this, arguments);
```
by using `extendFn` and `extendFnView` options:
```js
domc.addType('new-type', {
extend: 'parent-type',
@ -657,22 +624,20 @@ domc.addType('new-type', {
init() {
// do something
},
}
},
});
```
The same would be for the view by using `extendFnView`
The same would be for the view by using `extendFnView`
:::tip
If you need you can also get all the current component types by using `getTypes`
```js
editor.DomComponents.getTypes().forEach(compType => console.log(compType.id))
editor.DomComponents.getTypes().forEach((compType) => console.log(compType.id));
```
:::
:::
## Lifecycle Hooks
@ -684,17 +649,17 @@ use of them for a more generic use case or also listen to them inside other comp
Let's see below the flow of all hooks:
* **Local hook**: `model.init()` method, executed once the model of the component is initialized
* **Global hook**: `component:create` event, called right after `model.init()`. The model is passed as an argument to the callback function.
- **Local hook**: `model.init()` method, executed once the model of the component is initialized
- **Global hook**: `component:create` event, called right after `model.init()`. The model is passed as an argument to the callback function.
Es. `editor.on('component:create', model => console.log('created', model))`
* **Local hook**: `view.init()` method, executed once the view of the component is initialized
* **Local hook**: `view.onRender()` method, executed once the component is rendered on the canvas
* **Global hook**: `component:mount` event, called right after `view.onRender()`. The model is passed as an argument to the callback function.
* **Local hook**: `model.updated()` method, executes when some property of the model is updated.
* **Global hook**: `component:update` event, called after `model.updated()`. The model is passed as an argument to the callback function.
- **Local hook**: `view.init()` method, executed once the view of the component is initialized
- **Local hook**: `view.onRender()` method, executed once the component is rendered on the canvas
- **Global hook**: `component:mount` event, called right after `view.onRender()`. The model is passed as an argument to the callback function.
- **Local hook**: `model.updated()` method, executes when some property of the model is updated.
- **Global hook**: `component:update` event, called after `model.updated()`. The model is passed as an argument to the callback function.
You can also listen to specific property change via `component:update:{propertyName}`
* **Local hook**: `model.removed()` method, executed when the component is removed.
* **Global hook**: `component:remove` event, called after `model.removed()`. The model is passed as an argument to the callback function.
- **Local hook**: `model.removed()` method, executed when the component is removed.
- **Global hook**: `component:remove` event, called after `model.removed()`. The model is passed as an argument to the callback function.
Below you can find an example usage of all the hooks
@ -710,15 +675,14 @@ editor.DomComponents.addType('test-component', {
// Here we can listen global hooks with editor.on('...')
},
updated(property, value, prevValue) {
console.log('Local hook: model.updated',
'property', property, 'value', value, 'prevValue', prevValue);
console.log('Local hook: model.updated', 'property', property, 'value', value, 'prevValue', prevValue);
},
removed() {
console.log('Local hook: model.removed');
},
handlePropChange() {
console.log('The value of testprop', this.get('testprop'));
}
},
},
view: {
init() {
@ -737,16 +701,14 @@ editor.BlockManager.add('test-component', {
});
// Global hooks
editor.on(`component:create`, model => console.log('Global hook: component:create', model.get('type')));
editor.on(`component:mount`, model => console.log('Global hook: component:mount', model.get('type')));
editor.on(`component:update:testprop`, model => console.log('Global hook: component:update:testprop', model.get('type')));
editor.on(`component:remove`, model => console.log('Global hook: component:remove', model.get('type')));
editor.on(`component:create`, (model) => console.log('Global hook: component:create', model.get('type')));
editor.on(`component:mount`, (model) => console.log('Global hook: component:mount', model.get('type')));
editor.on(`component:update:testprop`, (model) =>
console.log('Global hook: component:update:testprop', model.get('type')),
);
editor.on(`component:remove`, (model) => console.log('Global hook: component:remove', model.get('type')));
```
## Components & CSS
::: warning
@ -780,6 +742,7 @@ domc.addType('component-css', {
},
});
```
This approach allows the editor to group these styles ([CssRule] instances) and remove them accordingly in case all references of the same component are removed.
::: danger Important caveat
@ -802,7 +765,7 @@ domc.addType('cmp-a', {
.cmp-css-a { color: darkgreen }
}
`,
}
},
},
});
domc.addType('cmp-b', {
@ -816,18 +779,14 @@ domc.addType('cmp-b', {
.cmp-css-b { color: darkblue }
}
`,
}
},
},
});
domc.addType('component-css', {
model: {
defaults: {
attributes: { class: 'cmp-css' },
components: [
'<span>Component with styles<span>',
{ type: 'cmp-a' },
{ type: 'cmp-b' },
],
components: ['<span>Component with styles<span>', { type: 'cmp-a' }, { type: 'cmp-b' }],
styles: `
.cmp-css { color: red }
@media (max-width: 992px) {
@ -838,8 +797,10 @@ domc.addType('component-css', {
},
});
```
::: tip Component-first styling
By default, when you select a component in the canvas and apply styles on it, changes will be applied on its existent classes. This will result on changing of all the components with those applied classes. If you need the style to be applied only on the specific selected component you have to select componentFirst strategy in this way.
```js
grapesjs.init({
...
@ -848,23 +809,18 @@ grapesjs.init({
},
})
```
:::
### External CSS
If you need to load external component-specific CSS, you have to rely on the `script` property. For more details please refer to [Components & JS](Components-js.html).
## Components & JS
If you want to know how to create Components with javascript attached (eg. counters, galleries, slideshows, etc.) check the dedicated page
[Components & JS](Components-js.html)
## Tips
### JSX syntax
@ -873,6 +829,7 @@ If you're importing big chunks of HTML string into the editor (eg. defined via B
By default, GrapesJS understands objects generated from React JSX preset, so, if you're working in the React app probably you're already using JSX and you don't need to do anything else, your environment is already configured to parse JSX in javascript files.
So, instead of writing this:
```js
// I'm adding a string, so the parsing and the component recognition steps will be executed
editor.addComponents(`<div>
@ -881,7 +838,9 @@ editor.addComponents(`<div>
</span>
</div>`);
```
or this
```js
// I'm passing the Component Definition, so heavy steps will be skipped but the code is less readable
editor.addComponents({
@ -891,21 +850,26 @@ editor.addComponents({
],
});
```
you can use this format
```js
editor.addComponents(<div>
<custom-component data-gjs-prop="someValue" title="foo">
Hello!
</custom-component>
</div>);
editor.addComponents(
<div>
<custom-component data-gjs-prop="someValue" title="foo">
Hello!
</custom-component>
</div>,
);
```
Another cool feature you will get by using JSX is the ability to pass component types as element tags `<custom-component>` instead of `data-gjs-type="custom-component"`
#### Setup JSX syntax
For those who are not using React you have the following options:
* GrapesJS has an option, `config.domComponents.processor`, thats allows you to easily implement other JSX presets. This scenario is useful if you work with a framework different from React but that uses JSX (eg. Vue). In that case, the result object from JSX pragma function (React uses `React.createElement`) will be different (you can log the JSX to see the result object) and you have to transform that in GrapesJS [Component Definition] object. Below an example of usage
- GrapesJS has an option, `config.domComponents.processor`, thats allows you to easily implement other JSX presets. This scenario is useful if you work with a framework different from React but that uses JSX (eg. Vue). In that case, the result object from JSX pragma function (React uses `React.createElement`) will be different (you can log the JSX to see the result object) and you have to transform that in GrapesJS [Component Definition] object. Below an example of usage
```js
grapesjs.init({
@ -926,27 +890,24 @@ grapesjs.init({
})
```
* In case you need to support JSX from scratch (you don't use a framework which supports JSX) you have, at first, implement the parser which transforms JSX in your files in something JS-readable.
- In case you need to support JSX from scratch (you don't use a framework which supports JSX) you have, at first, implement the parser which transforms JSX in your files in something JS-readable.
For Babel users, it's just a matter of adding few plugins: `@babel/plugin-syntax-jsx` and `@babel-plugin-transform-react`. Then update your `.babelrc` file
```json
{
“plugins”: [
“@babel/plugin-syntax-jsx”,
“@babel/plugin-transform-react-jsx”
]
}
```
You can also customize the pragma function which executes the transformation `[“@babel/plugin-transform-react-jsx”, { “pragma”: “customCreateEl” }]`, by default `React.createElement` is used (you'll need a React instance available in the file to make it work).
A complete example of this approach can be found [here](https://codesandbox.io/s/x07xf)
```json
{
“plugins”: [
“@babel/plugin-syntax-jsx”,
“@babel/plugin-transform-react-jsx”
]
}
```
You can also customize the pragma function which executes the transformation `[“@babel/plugin-transform-react-jsx”, { “pragma”: “customCreateEl” }]`, by default `React.createElement` is used (you'll need a React instance available in the file to make it work).
A complete example of this approach can be found [here](https://codesandbox.io/s/x07xf)
[Component Definition]: <#component-definition>
[Component]: </api/component.html>
[CssRule]: </api/css_rule.html>
[Component API]: </api/component.html>
[Component Definition]: #component-definition
[Component]: /api/component.html
[CssRule]: /api/css_rule.html
[Component API]: /api/component.html

76
docs/modules/I18n.md

@ -14,8 +14,6 @@ The module was added recently so we're open to receive support in [translating s
[[toc]]
## Configuration
By default, the editor includes only the English language, if you need other languages you have to import them manually.
@ -39,8 +37,6 @@ const editor = grapesjs.init({
Now the editor will be translated in Italian for those browsers which default language is Italian (by default `detectLocale` option is enabled)
## Update strings
If you need to change some default language strings you can easily update them by using [I18n API](/api/i18n.html).
@ -67,11 +63,12 @@ So now to update it you'll do this
```js
editor.I18n.addMessages({
en: { // indicate the locale to update
styleManager: {
empty: 'New empty state message',
}
}
en: {
// indicate the locale to update
styleManager: {
empty: 'New empty state message',
},
},
});
```
@ -114,17 +111,17 @@ you'd this
```js
editor.I18n.addMessages({
en: {
styleManager: {
properties: {
// The key is the property name (or id)
'margin-top': 'Top',
'margin-right': 'Right',
'margin-left': 'Left',
'margin-bottom': 'Bottom',
},
}
}
en: {
styleManager: {
properties: {
// The key is the property name (or id)
'margin-top': 'Top',
'margin-right': 'Right',
'margin-left': 'Left',
'margin-bottom': 'Bottom',
},
},
},
});
```
@ -136,8 +133,6 @@ If you try to update strings, by using API, once the UI is rendered you'll see n
We need to find the way to update the UI
-->
## Adding new language
If you want to support GrapesJS by adding a new language to our repository all you need to do is to follow steps below:
@ -150,8 +145,6 @@ If you want to support GrapesJS by adding a new language to our repository all y
1. By following comments you'll probably notice that some keys are not indicated (eg. `styleManager.properties`), for the reference you can check other locale files
1. Once you've done, you can create a new Pull Request on GitHub from your branch to `dev` by making also a reference to your issue in order to close it automatically once it's merged (your PR message should contain `Closes #1234` where 1234 is the issue ID)
## Plugin development
::: warning
@ -176,10 +169,10 @@ For your plugin specific strings, place them under the plugin name key
```js
// src/locale/en.js
export default {
'grapesjs-plugin-name': {
yourKey: 'Your value',
}
}
'grapesjs-plugin-name': {
yourKey: 'Your value',
},
};
```
In your `index.js` use the `en.js` file and add `i18n` option to allow import of other local files
@ -189,19 +182,19 @@ In your `index.js` use the `en.js` file and add `i18n` option to allow import of
import en from 'locale/en';
export default (editor, opts = {}) => {
const options = {
i18n: {},
// ...
...opts,
};
const options = {
i18n: {},
// ...
...opts,
};
editor.I18n.addMessages({
en,
...options.i18n,
});
}
// ...
editor.I18n.addMessages({
en,
...options.i18n,
});
};
```
The next step would be to compile your locale files into `<rootDir>/locale` directory to make them easily accessible by your users. This folder could be ignored in your git repository be should be deployd to the npm registry
@ -210,7 +203,6 @@ The next step would be to compile your locale files into `<rootDir>/locale` dire
Remember that you can skip all these long steps and init your project with [grapesjs-cli](https://github.com/GrapesJS/cli). This will create all the necessary folders/files/commands for you (during `init` command this step is flagged `true` by default and we recommend to keep it even in case the i18n is not required in your project)
:::
At the end, your plugin users will be able to import other locale files (if they exist) in this way
```js
@ -232,5 +224,5 @@ const editor = grapesjs.init({
});
```
[ISO 639-1]: <https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes>
[`en` locale file]: <https://github.com/GrapesJS/grapesjs/blob/master/src/i18n/locale/en.js>
[ISO 639-1]: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes
[`en` locale file]: https://github.com/GrapesJS/grapesjs/blob/master/src/i18n/locale/en.js

29
docs/modules/Layers.md

@ -14,7 +14,6 @@ This guide is referring to GrapesJS v0.19.5 or higher
[[toc]]
## Configuration
To change the default configurations you have to pass the `layerManager` option with the main configuration object.
@ -30,7 +29,6 @@ const editor = grapesjs.init({
You can check here the full list of available configuration options: [Layer Manager Config](https://github.com/GrapesJS/grapesjs/blob/master/src/navigator/config/config.ts)
Layers are a direct representation of your components, therefore they will only be available once your components are loaded in the editor (eg. you might load your project data from a remote endpoint).
In your configuration, you're able to change the global behavior of layers (eg. make all the layers not sortable) and also decide which component layer should be used as a root.
@ -50,16 +48,10 @@ const editor = grapesjs.init({
The configurations are mainly targeting the default UI provided by GrapesJS core, in case you need more control over the tree of your layers, you can read more in the [Customization](#customization) section below.
## Programmatic usage
If you need to manage layers programmatically you can use its [APIs][Layers API].
## Customization
By using the [Layers API][Layers API] you're able to replace the default UI with your own implementation.
@ -68,28 +60,28 @@ All you have to do is to indicate to the editor your intent to use a custom UI a
```js
const editor = grapesjs.init({
// ...
layerManager: {
custom: true,
// ...
layerManager: {
custom: true,
// ...
},
},
});
// Use this event to append your UI in the default container provided by GrapesJS.
// You can skip this event if you don't rely on the core panels and decide to
// place the UI in some other place.
editor.on('layer:custom', (props) => {
// props.container (HTMLElement) - The default element where you can append your UI
// props.container (HTMLElement) - The default element where you can append your UI
});
// Triggered when the root layer is changed.
editor.on('layer:root', (root) => {
// Update the root of your UI
// Update the root of your UI
});
// Triggered when a component is updated, this allows you to update specific layers.
editor.on('layer:component', (component) => {
// Update the specific layer of your UI
// Update the specific layer of your UI
});
```
@ -97,7 +89,6 @@ In the example below we'll replicate most of the default functionality with our
<demo-viewer value="L24hkgm5" height="500" darkcode/>
<!-- Demo template, here for reference
<style>
.layer-manager {
@ -376,11 +367,9 @@ const app = new Vue({
</script>
-->
## Events
For a complete list of available events, you can check it [here](/api/layer_manager.html#available-events).
[Components]: <Components.html>
[Layers API]: </api/layer_manager.html>
[Components]: Components.html
[Layers API]: /api/layer_manager.html

44
docs/modules/Modal.md

@ -12,8 +12,6 @@ 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.
@ -57,7 +55,7 @@ Modal.setContent('New content');
// Execute one-time callback on modal close
Modal.onceClose(() => {
console.log('My last modal is closed');
console.log('My last modal is closed');
});
```
@ -68,8 +66,8 @@ The fastest and the easiest one is to use your specific CSS for the modal elemen
```css
.gjs-mdl-dialog {
background-color: white;
color: #333;
background-color: white;
color: #333;
}
```
@ -77,17 +75,17 @@ In case you have to customize a specific modal differently, you can rely on your
```js
editor.Modal.open({
title: 'My title',
content: 'My content',
attributes: {
class: 'my-small-modal',
},
title: 'My title',
content: 'My content',
attributes: {
class: 'my-small-modal',
},
});
```
```css
.my-small-modal .gjs-mdl-dialog {
max-width: 300px;
max-width: 300px;
}
```
@ -101,19 +99,18 @@ For more advanced usage, you can completely replace the default modal with one o
```js
const editor = grapesjs.init({
// ...
modal: { custom: true },
// ...
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.
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.
});
```
@ -121,7 +118,6 @@ 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).
For a complete list of available events, you can check it [here](/api/modal_dialog.html#available-events).

64
docs/modules/Pages.md

@ -39,17 +39,17 @@ const editor = grapesjs.init({
height: '100%',
storageManager: false,
pageManager: {
pages: [
{
// without an explicit ID, a random one will be created
id: 'my-first-page',
// CSS or a JSON of styles
styles: '.my-el { color: red }',
// HTML string or a JSON of components
component: '<div class="my-el">Hello world!</div>',
}
]
},
pages: [
{
// without an explicit ID, a random one will be created
id: 'my-first-page',
// CSS or a JSON of styles
styles: '.my-el { color: red }',
// HTML string or a JSON of components
component: '<div class="my-el">Hello world!</div>',
},
],
},
});
```
@ -63,33 +63,30 @@ As you might guess, this is how initializing the editor with multiple pages woul
const editor = grapesjs.init({
// ...
pageManager: {
pages: [
{
id: 'my-first-page',
styles: '.my-page1-el { color: red }',
component: '<div class="my-page1-el">Page 1</div>',
},
{
id: 'my-second-page',
styles: '.my-page2-el { color: blue }',
component: '<div class="my-page2-el">Page 2</div>',
},
]
},
pages: [
{
id: 'my-first-page',
styles: '.my-page1-el { color: red }',
component: '<div class="my-page1-el">Page 1</div>',
},
{
id: 'my-second-page',
styles: '.my-page2-el { color: blue }',
component: '<div class="my-page2-el">Page 2</div>',
},
],
},
});
```
GrapesJS doesn't provide any default UI for the Page Manager but you can easily built one by leveraging its [APIs][Pages API]. Check the [Customization](#customization) section for more details on how to create your own Page Manager UI.
## Programmatic usage
If you need to manage pages programmatically you can use its [APIs][Pages API].
Below are some commonly used methods:
```js
// Get the Pages module first
const pages = editor.Pages;
@ -124,8 +121,6 @@ const cssPage = editor.getCss({ component });
pages.remove('new-page-id');
```
## Customization
By using the [Pages API] it's easy to create your own Page Manager UI.
@ -134,11 +129,11 @@ The simpliest way is to subscribe to the catch-all `page` event, which is trigge
```js
const editor = grapesjs.init({
// ...
// ...
});
editor.on('page', () => {
// Update your UI
// Update your UI
});
```
@ -146,7 +141,6 @@ In the example below you can see an quick implementation of the Page Manager UI.
<demo-viewer value="1y6bgeo3" height="500" darkcode/>
<!-- Demo template, here for reference
<style>
.app-wrap {
@ -282,10 +276,8 @@ const app = new Vue({
</script>
-->
## Events
For a complete list of available events, you can check it [here](/api/pages.html#available-events).
[Pages API]: </api/pages.html>
[Pages API]: /api/pages.html

39
docs/modules/Plugins.md

@ -27,24 +27,22 @@ function myPlugin(editor) {
const editor = grapesjs.init({
container: '#gjs',
plugins: [myPlugin]
plugins: [myPlugin],
});
```
This means plugins can be moved to separate folders to keep thing cleaner or imported from NPM.
```js
import myPlugin from './plugins/myPlugin'
import npmPackage from '@npm/package'
import myPlugin from './plugins/myPlugin';
import npmPackage from '@npm/package';
const editor = grapesjs.init({
container : '#gjs',
plugins: [myPlugin, npmPackage]
container: '#gjs',
plugins: [myPlugin, npmPackage],
});
```
<!--
## Named plugin
@ -91,9 +89,6 @@ Here is a complete generic example:
```
-->
## Plugins with options
It's also possible to pass custom parameters to plugins in to make them more flexible.
@ -123,20 +118,21 @@ export default grapesjs.plugins.add('my-plugin-name', (editor, options) => {
This also works with plugins that aren't named.
-->
```js
const myPluginWithOptions = (editor, options) => {
console.log(options);
// { customField: 'customValue' }
}
};
const editor = grapesjs.init({
container : '#gjs',
container: '#gjs',
plugins: [myPluginWithOptions],
pluginsOpts: {
[myPluginWithOptions]: {
customField: 'customValue'
}
}
customField: 'customValue',
},
},
});
```
@ -162,24 +158,23 @@ import grapesjs, { usePlugin } from 'grapesjs';
import type { Plugin } from 'grapesjs';
interface MyPluginOptions {
opt1: string,
opt2?: number,
opt1: string;
opt2?: number;
}
const myPlugin: Plugin<MyPluginOptions> = (editor, options) => {
// ...
}
// ...
};
grapesjs.init({
// ...
plugins: [
// no need for `pluginsOpts`
usePlugin(myPlugin, { opt1: 'A', opt2: 1 })
]
usePlugin(myPlugin, { opt1: 'A', opt2: 1 }),
],
});
```
## Boilerplate
For fast plugin development, we highly recommend using [grapesjs-cli](https://github.com/GrapesJS/cli) which helps to avoid the hassle of setting up all the dependencies and configurations for development and building (no need to touch Webpack or Babel configurations). For more information check the repository.

44
docs/modules/Selectors.md

@ -6,7 +6,6 @@ title: Selector Manager
<p align="center"><img :src="$withBase('/selector-manager.jpg')" alt="GrapesJS - Selector Manager"/></p>
The [Selector] allows the reuse of styles across all of your [Components] in the project (exactly what classes do in HTML) and the main goal of the Selector Manager is to collect them and indicate the current state of the selection.
::: warning
@ -15,7 +14,6 @@ This guide is referring to GrapesJS v0.17.28 or higher
[[toc]]
## Configuration
To change the default configurations you have to pass the `selectorManager` property with the main configuration object.
@ -31,7 +29,6 @@ const editor = grapesjs.init({
Check the full list of available options here: [Selector Manager Config](https://github.com/GrapesJS/grapesjs/blob/master/src/selector_manager/config/config.ts)
## Initialization
The Selector Manager starts to collect data once componenets and styles are loaded. The default UI is displayed along with the default panels provided by GrapesJS core, in case you need to setup the editor with your own panels we recommend following the [Getting Started] guide.
@ -55,6 +52,7 @@ const editor = grapesjs.init({
`,
});
```
Internally, the example above will provide to Selector Manager 3 selectors: `class-a`, `class-b` and `class-c`.
Without any selected component, the Selector Manager UI is hidden by default (along with the Style Manager). By selecting the `Element A-B-C` you will see the current selection of what will be actually styled.
@ -73,9 +71,6 @@ You can also disable specific selectors and change the state (eg. Hover) in orde
<img :src="$withBase('/sm-disable-selector.jpg')" alt="Disabled selectors" style="display: block; margin: auto"/>
## Component-first selectors
By default, selecting components with classes will indicate their selectors as target style. That means that any change in Style Manager will be applied to all components containing those **Selected** classes.
@ -99,14 +94,9 @@ This option enables also the possibility to style multiple components and the ab
With multiple selection, the Style Manager shows always styles of the last selected component.
:::
## Programmatic usage
If you need to manage your selectors programmatically you can use its [APIs][Selector API].
If you need to manage your selectors programmatically you can use its [APIs][Selector API].
## Customization
@ -116,17 +106,16 @@ All you have to do is to indicate the editor your intent to use a custom UI and
```js
const editor = grapesjs.init({
// ...
selectorManager: {
custom: true,
// ...
selectorManager: {
custom: true,
// ...
},
},
});
editor.on('selector:custom', props => {
// props.container (HTMLElement) - The default element where you can append your UI
// Here you would put the logic to render/update your UI.
editor.on('selector:custom', (props) => {
// props.container (HTMLElement) - The default element where you can append your UI
// Here you would put the logic to render/update your UI.
});
```
@ -134,17 +123,12 @@ In the example below we'll replicate most of the default functionality by using
<demo-viewer value="v8cgkLfr" height="500" darkcode/>
## Events
For a complete list of available events, you can check it [here](/api/selector_manager.html#available-events).
[Selector]: </api/selector.html>
[Style Manager]: <Style-manager.html>
[Components]: <Components.html>
[Getting Started]: </getting-started.html>
[Selector API]: </api/selector_manager.html>
[Selector]: /api/selector.html
[Style Manager]: Style-manager.html
[Components]: Components.html
[Getting Started]: /getting-started.html
[Selector API]: /api/selector_manager.html

81
docs/modules/Storage.md

@ -7,7 +7,7 @@ title: Storage Manager
The Storage Manager is a built-in module that allows the persistence of your project data.
::: warning
This guide requires GrapesJS v0.19.* or higher
This guide requires GrapesJS v0.19.\* or higher
:::
[[toc]]
@ -36,6 +36,7 @@ const editor = grapesjs.init({
```
In case you don't need any persistence, you can disable the module in this way:
```js
const editor = grapesjs.init({
...
@ -45,10 +46,6 @@ const editor = grapesjs.init({
Check the full list of available options here: [Storage Manager Config](https://github.com/GrapesJS/grapesjs/blob/master/src/storage_manager/config/config.ts)
## Project data
The project data is a JSON object containing all the necessary information (styles, pages, etc.) about your project in the editor and is the one used in the storage manager methods in order to store and load your project (locally or remotely in your DB/file).
@ -63,6 +60,7 @@ const projectData = editor.getProjectData();
// Load project data
editor.loadProjectData(projectData);
```
:::
::: danger
@ -73,9 +71,6 @@ The editor is able to parse and use HTML/CSS code, you can use it as part of you
<!-- If necessary, the JSON can be also enriched with your data of choice, but as the data schema might differ in time we highly recommend to store them in your domain specific keys-->
## Storage strategy
Project data are automatically stored every time the amount of changes (`editor.getDirtyCount()`) reaches the number of steps before save (`editor.Storage.getStepsBeforeSave()`). On any successful store of the data, the counter of changes is reset (`editor.clearDirtyCount()`).
@ -90,9 +85,8 @@ const storedProjectData = await editor.store();
// Load data
const loadedProjectData = await editor.load();
```
:::
:::
## Setup local storage
@ -115,8 +109,6 @@ const editor = grapesjs.init({
});
```
## Setup remote storage
Most commonly the data of the project might be saved remotely on your server (DB, file, etc.) therefore you need to setup your server-side API calls in order to store/load project data.
@ -173,12 +165,9 @@ Be sure to configure properly [CORS](https://developer.mozilla.org/en-US/docs/We
Server configuration might differ case to case so usually, it's up to you to know how to configure it properly.
The default remote storage follows a simple REST API approach with project data exchanged as a JSON (`Content-Type: application/json`).
* On **load** (`GET` method), the JSON project data are expected to be returned directly in the response. As from example above, you can use `options.remote.onLoad` to extract the project data if the response contains other metadata.
* On **store** (`POST` method), the editor doesn't expect any particular result but only a valid response from the server (status code `200`).
- On **load** (`GET` method), the JSON project data are expected to be returned directly in the response. As from example above, you can use `options.remote.onLoad` to extract the project data if the response contains other metadata.
- On **store** (`POST` method), the editor doesn't expect any particular result but only a valid response from the server (status code `200`).
<!-- ## Store and load templates
@ -212,17 +201,10 @@ const editor = grapesjs.init({
});
``` -->
## Storage API
The Storage Manager module has also its own [set of APIs](/api/storage_manager.html) that allows you to extend and add new functionalities.
### Define new storage
Defining a new storage is a matter of passing of two asyncronous methods to the `editor.Storage.add` API. For a sake of simplicity, the example below illustrates the API usage for defining the `session` storage by using [sessionStorage API](https://developer.mozilla.org/en-US/docs/Web/API/Window/sessionStorage).
@ -254,8 +236,6 @@ const editor = grapesjs.init({
});
```
### Extend storage
Among other needs, you might need to use existing storages to combine them in a more complex use case.
@ -285,7 +265,7 @@ Storage.add('remote-local', {
### Replace storage
You can also replace already defined storages with other implementations by passing the same storage type in the `Storage.add` method. You can switch, for example, the default `local`, which relies on [localStorage API], with something more scalable like [IndexedDB API].
You can also replace already defined storages with other implementations by passing the same storage type in the `Storage.add` method. You can switch, for example, the default `local`, which relies on [localStorage API], with something more scalable like [IndexedDB API].
It might also be possible that you're already using some HTTP client library (eg. [axios](https://github.com/axios/axios)) which handles for you all the necessary HTTP headers in your application (CSRF token, session data, etc.), so you can simply replace the default `remote` storage with your implementation of choice without caring about the default configurations.
@ -301,8 +281,6 @@ editor.Storage.add('remote', {
});
```
<!-- ### Examples
Here you can find some of the plugins extending the Storage Manager
@ -310,10 +288,6 @@ Here you can find some of the plugins extending the Storage Manager
* [grapesjs-indexeddb] - Storage wrapper for IndexedDB
* [grapesjs-firestore] - Storage wrapper for [Cloud Firestore](https://firebase.google.com/docs/firestore) -->
## Common use cases
### Skip initial load
@ -343,8 +317,8 @@ grapesjs.init({
},
})
```
In case `projectData` is defined, the initial storage load will be automatically skipped.
In case `projectData` is defined, the initial storage load will be automatically skipped.
### HTML code with project data
@ -360,21 +334,21 @@ grapesjs.init({
remote: {
// Enrich the store call
onStore: (data, editor) => {
const pagesHtml = editor.Pages.getAll().map(page => {
const pagesHtml = editor.Pages.getAll().map((page) => {
const component = page.getMainComponent();
return {
html: editor.getHtml({ component }),
css: editor.getCss({ component })
}
css: editor.getCss({ component }),
};
});
return { id: projectID, data, pagesHtml };
},
// If on load, you're returning the same JSON from above...
onLoad: result => result.data,
}
onLoad: (result) => result.data,
},
},
},
})
});
```
### Inline project data
@ -383,19 +357,19 @@ In might be a case where the editor is not connected to any storage but simply r
```html
<form id="my-form">
<input id="project-html" type="hidden"/>
<input id="project-data" type="hidden" value='{"pages": [{"component": "<div>Initial content</div>"}]}'/>
<input id="project-html" type="hidden" />
<input id="project-data" type="hidden" value='{"pages": [{"component": "<div>Initial content</div>"}]}' />
<div id="gjs"></div>
<button type="submit">Submit</button>
</form>
<script>
// Show data on submit
document.getElementById('my-form').addEventListener('submit', event => {
document.getElementById('my-form').addEventListener('submit', (event) => {
event.preventDefault();
const projectDataEl = document.getElementById('project-data');
const projectHtmlEl = document.getElementById('project-html');
alert(`HTML: ${projectHtmlEl.value}\n------\nDATA: ${projectDataEl.value}`)
alert(`HTML: ${projectHtmlEl.value}\n------\nDATA: ${projectDataEl.value}`);
});
// Inline storage
@ -416,7 +390,7 @@ In might be a case where the editor is not connected to any storage but simply r
</head>
${editor.getHtml({ component })}
<html>`;
}
},
});
};
@ -432,19 +406,12 @@ In might be a case where the editor is not connected to any storage but simply r
In the example above we're relying on two hidden inputs, one for containing the project data and the another one for the HTML/CSS.
## Events
For a complete list of available events, you can check it [here](/api/storage_manager.html#available-events).
[grapesjs-indexeddb]: <https://github.com/GrapesJS/storage-indexeddb>
[grapesjs-firestore]: <https://github.com/GrapesJS/storage-firestore>
[localStorage API]: <https://developer.mozilla.org/it/docs/Web/API/Window/localStorage>
[IndexedDB API]: <https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API>
[json-server]: <https://github.com/typicode/json-server>
[grapesjs-indexeddb]: https://github.com/GrapesJS/storage-indexeddb
[grapesjs-firestore]: https://github.com/GrapesJS/storage-firestore
[localStorage API]: https://developer.mozilla.org/it/docs/Web/API/Window/localStorage
[IndexedDB API]: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API
[json-server]: https://github.com/typicode/json-server

135
docs/modules/Style-manager.md

@ -34,10 +34,6 @@ const editor = grapesjs.init({
Check the full list of available options here: [Style Manager Config](https://github.com/GrapesJS/grapesjs/blob/master/src/style_manager/config/config.ts)
## Initialization
The Style Manager module is organized in sectors where each sector contains a list of properties to display. The default Style Manager configuration contains already a list of default common property styles and you can use them by simply skipping the `styleManagerConfig.sectors` option.
@ -103,11 +99,12 @@ sectors: [
// Additonal `number` options
units: ['px', '%'], // Units (available only for the 'number' type)
min: 0, // Min value (available only for the 'number' type)
}
},
],
},
]
];
```
This will render the number input UI and will change the `padding` CSS property on the selected component.
The flexibility of the definition allows you to create easily different UI inputs for any possible CSS property. You're free to decide what will be the best UI for your users. If you take for example a numeric property like `font-size`, you can follow its CSS specification and define it as a `number`
@ -121,6 +118,7 @@ The flexibility of the definition allows you to create easily different UI input
min: 0,
}
```
or you can decide to show it as a `select` and make available only a defined set of values (eg. based on your Design System tokens).
```js
@ -146,7 +144,7 @@ Let's see below the list of all available default types with their relative UI a
Each **Model** describes more in detail available props and their usage.
:::
* `base` - The base type, renders as a simple text input field. **Model**: [Property](/api/property.html)
- `base` - The base type, renders as a simple text input field. **Model**: [Property](/api/property.html)
<img :src="$withBase('/sm-base-type.jpg')"/>
@ -160,11 +158,11 @@ Each **Model** describes more in detail available props and their usage.
},
```
* `color` - Same props as `base` but the UI is a color picker. **Model**: [Property](/api/property.html)
- `color` - Same props as `base` but the UI is a color picker. **Model**: [Property](/api/property.html)
<img :src="$withBase('/sm-type-color.jpg')"/>
* `number` - Number input field for numeric values. **Model**: [PropertyNumber](/api/property_number.html)
- `number` - Number input field for numeric values. **Model**: [PropertyNumber](/api/property_number.html)
<img :src="$withBase('/sm-type-number.jpg')"/>
@ -181,11 +179,12 @@ Each **Model** describes more in detail available props and their usage.
max: 100,
},
```
* `slider` - Same props as `number` but the UI is a slider. **Model**: [PropertyNumber](/api/property_number.html)
- `slider` - Same props as `number` but the UI is a slider. **Model**: [PropertyNumber](/api/property_number.html)
<img :src="$withBase('/sm-type-slider.jpg')"/>
* `select` - Select input with options. **Model**: [PropertySelect](/api/property_select.html)
- `select` - Select input with options. **Model**: [PropertySelect](/api/property_select.html)
<img :src="$withBase('/sm-type-select.jpg')"/>
@ -205,11 +204,11 @@ Each **Model** describes more in detail available props and their usage.
},
```
* `radio` - Same props as `select` but the UI is a radio button. **Model**: [PropertySelect](/api/property_select.html)
- `radio` - Same props as `select` but the UI is a radio button. **Model**: [PropertySelect](/api/property_select.html)
<img :src="$withBase('/sm-type-radio.jpg')"/>
* `composite` - This type is great for CSS shorthand properties, where the final value is a composition of multiple sub properties. **Model**: [PropertyComposite](/api/property_composite.html)
- `composite` - This type is great for CSS shorthand properties, where the final value is a composition of multiple sub properties. **Model**: [PropertyComposite](/api/property_composite.html)
<img :src="$withBase('/sm-type-composite.jpg')"/>
@ -228,7 +227,8 @@ Each **Model** describes more in detail available props and their usage.
]
},
```
* `stack` - This type is great for CSS multiple properties like `text-shadow`, `box-shadow`, `transform`, etc. **Model**: [PropertyStack](/api/property_stack.html)
- `stack` - This type is great for CSS multiple properties like `text-shadow`, `box-shadow`, `transform`, etc. **Model**: [PropertyStack](/api/property_stack.html)
<img :src="$withBase('/sm-type-stack.jpg')"/>
@ -266,26 +266,26 @@ sectors: [
units: ['px', '%'],
},
// If the property doesn't exist it will be converted to a base type
'unknown-property' // -> { type: 'base', property: 'unknown-property' }
'unknown-property', // -> { type: 'base', property: 'unknown-property' }
],
},
]
];
```
::: tip
You can check if the property is available by running
```js
editor.StyleManager.getBuiltIn('property-name');
```
or get the list of all available properties with
```js
editor.StyleManager.getBuiltInAll();
```
:::
:::
## I18n
@ -308,10 +308,10 @@ grapesjs.init({
property: 'display',
default: 'block',
options: [
{id: 'block', label: 'Block'},
{id: 'inline', label: 'Inline'},
{id: 'none', label: 'None'},
]
{ id: 'block', label: 'Block' },
{ id: 'inline', label: 'Inline' },
{ id: 'none', label: 'None' },
],
},
],
},
@ -324,7 +324,7 @@ grapesjs.init({
en: {
styleManager: {
sectors: {
'first-sector-id': 'First sector EN'
'first-sector-id': 'First sector EN',
},
properties: {
width: 'Width EN',
@ -335,22 +335,18 @@ grapesjs.init({
block: 'Block EN',
inline: 'Inline EN',
none: 'None EN',
}
}
}
}
}
},
},
},
},
},
},
});
```
## Component constraints
When you define custom components you can also indicate, via `stylable` and `unstylable` props, which CSS properties should be available for styling. In that case, the Style Manager will only show the available properties. If the sector doesn't contain any available property, it won't be shown.
When you define custom components you can also indicate, via `stylable` and `unstylable` props, which CSS properties should be available for styling. In that case, the Style Manager will only show the available properties. If the sector doesn't contain any available property, it won't be shown.
```js
const customComponents = (editor) => {
@ -359,18 +355,18 @@ const customComponents = (editor) => {
model: {
defaults: {
// When this component is selected, the Style Manager will show only the following properties
stylable: ['width', 'height']
}
}
stylable: ['width', 'height'],
},
},
});
// Component B
editor.Components.addType('cmp-b', {
model: {
defaults: {
// When this component is selected, the Style Manager will hide the following properties
unstylable: ['color']
}
}
unstylable: ['color'],
},
},
});
};
@ -385,31 +381,23 @@ grapesjs.init({
sectors: [
{
name: 'First sector',
properties: [
'width', 'min-width',
'height', 'min-height',
],
properties: ['width', 'min-width', 'height', 'min-height'],
},
{
name: 'Second sector',
properties: [
'color', 'font-size',
],
properties: ['color', 'font-size'],
},
],
},
});
```
## Programmatic usage
For a more advanced usage you can rely on the [Style Manager API] to perform different kind of actions related to the module.
* Managing sectors/properties post-initialization.
- Managing sectors/properties post-initialization.
```js
// Get the module from the editor instance
const sm = editor.StyleManager;
@ -431,7 +419,8 @@ For a more advanced usage you can rely on the [Style Manager API] to perform dif
sm.removeSector('sector-id');
```
* Managing selected targets.
- Managing selected targets.
```js
// Select the first button in the current page
const wrapperCmp = editor.Pages.getSelected().getMainComponent();
@ -447,7 +436,9 @@ For a more advanced usage you can rely on the [Style Manager API] to perform dif
// Update selected targets with a custom style
sm.addStyleTargets({ color: 'red' });
```
* Adding/extending built-in property definitions.
- Adding/extending built-in property definitions.
```js
const myPlugin = (editor) => {
editor.StyleManager.addBuiltIn('new-prop', {
@ -469,11 +460,8 @@ For a more advanced usage you can rely on the [Style Manager API] to perform dif
},
})
```
* [Adding new types](#adding-new-types).
- [Adding new types](#adding-new-types).
## Customization
@ -542,19 +530,18 @@ All you have to do is to indicate the editor your intent to use a custom UI and
```js
const editor = grapesjs.init({
// ...
styleManager: {
custom: true,
// ...
styleManager: {
custom: true,
// ...
},
},
});
editor.on('style:custom', props => {
// props.container (HTMLElement)
// The default element where you can append your
// custom UI in order to render it in the default position.
// Here you would put the logic to render/update your UI by relying on Style Manager API
editor.on('style:custom', (props) => {
// props.container (HTMLElement)
// The default element where you can append your
// custom UI in order to render it in the default position.
// Here you would put the logic to render/update your UI by relying on Style Manager API
});
```
@ -857,14 +844,10 @@ sm.addStyleTargets({ color: 'red' });
</script>
-->
## Events
For a complete list of available events, you can check it [here](/api/style_manager.html#available-events).
[Components]: <Components.html>
[I18n]: <I18n.html>
[Style Manager API]: </api/style_manager.html>
[Components]: Components.html
[I18n]: I18n.html
[Style Manager API]: /api/style_manager.html

197
docs/modules/Traits.md

@ -4,7 +4,7 @@ title: Trait Manager
# Trait Manager
In GrapesJS, Traits define different parameters and behaviors of a component. The user generally will see traits as the *Settings* of a component. A common use of traits is to customize element attributes (eg. `placeholder` for `<input>`) or you can also bind them to the properties of your components and react to their changes.
In GrapesJS, Traits define different parameters and behaviors of a component. The user generally will see traits as the _Settings_ of a component. A common use of traits is to customize element attributes (eg. `placeholder` for `<input>`) or you can also bind them to the properties of your components and react to their changes.
::: warning
This guide is referring to GrapesJS v0.21.9 or higher.<br><br>
@ -13,9 +13,6 @@ To get a better understanding of the content in this guide we recommend reading
[[toc]]
## Add Traits to Components
Generally, you define traits on the definition of your new custom components (or by extending another one). Let's see in this example how to make inputs more customizable by the editor.
@ -28,32 +25,34 @@ We can start by creating a new custom `input` component in this way:
```js
editor.Components.addType('input', {
isComponent: el => el.tagName === 'INPUT',
model: {
defaults: {
traits: [
// Strings are automatically converted to text types
'name', // Same as: { type: 'text', name: 'name' }
'placeholder',
{
type: 'select', // Type of the trait
name: 'type', // (required) The name of the attribute/property to use on component
label: 'Type', // The label you will see in Settings
options: [
{ id: 'text', label: 'Text'},
{ id: 'email', label: 'Email'},
{ id: 'password', label: 'Password'},
{ id: 'number', label: 'Number'},
]
}, {
type: 'checkbox',
name: 'required',
}],
// As by default, traits are bound to attributes, so to define
// their initial value we can use attributes
attributes: { type: 'text', required: true },
},
isComponent: (el) => el.tagName === 'INPUT',
model: {
defaults: {
traits: [
// Strings are automatically converted to text types
'name', // Same as: { type: 'text', name: 'name' }
'placeholder',
{
type: 'select', // Type of the trait
name: 'type', // (required) The name of the attribute/property to use on component
label: 'Type', // The label you will see in Settings
options: [
{ id: 'text', label: 'Text' },
{ id: 'email', label: 'Email' },
{ id: 'password', label: 'Password' },
{ id: 'number', label: 'Number' },
],
},
{
type: 'checkbox',
name: 'required',
},
],
// As by default, traits are bound to attributes, so to define
// their initial value we can use attributes
attributes: { type: 'text', required: true },
},
},
});
```
@ -65,26 +64,26 @@ If you want you can also define traits dynamically via functions, which will be
```js
editor.Components.addType('input', {
isComponent: el => el.tagName === 'INPUT',
model: {
defaults: {
traits(component) {
const result = [];
// Example of some logic
if (component.get('draggable')) {
result.push('name');
} else {
result.push({
type: 'select',
// ....
});
}
return result;
isComponent: (el) => el.tagName === 'INPUT',
model: {
defaults: {
traits(component) {
const result = [];
// Example of some logic
if (component.get('draggable')) {
result.push('name');
} else {
result.push({
type: 'select',
// ....
});
}
return result;
},
},
},
});
```
@ -104,8 +103,8 @@ editor.Components.addType('input', {
handleTypeChange() {
console.log('Input type changed to: ', this.getAttributes().type);
},
}
})
},
});
```
As already mentioned, by default, traits modify attributes of the model, but you can also bind them to the properties by using `changeProp` options.
@ -119,7 +118,7 @@ editor.Components.addType('input', {
{
name: 'placeholder',
changeProp: 1,
}
},
// ...
],
// As we switched from attributes to properties the
@ -132,8 +131,8 @@ editor.Components.addType('input', {
this.on('change:placeholder', this.handlePlhChange);
},
// ...
}
})
},
});
```
### Categories
@ -160,18 +159,18 @@ editor.Components.addType('input', {
{ name: 'trait-6' },
],
},
}
})
},
});
```
## Built-in trait types
GrapesJS comes along with few built-in types that you can use to define your traits:
### Text
Simple text input
```js
{
type: 'text', // If you don't specify the type, the `text` is the default one
@ -181,8 +180,11 @@ Simple text input
placeholder: 'Insert text', // Placeholder to show inside the input
}
```
### Number
Input for numbers
```js
{
type: 'number',
@ -193,8 +195,11 @@ Input for numbers
step: 5, // Number of steps
}
```
### Checkbox
Simple checkbox input
```js
{
type: 'checkbox',
@ -203,8 +208,11 @@ Simple checkbox input
valueFalse: 'NO', // Value to assign when is unchecked, default: `false`
}
```
### Select
Select input with options
```js
{
type: 'select',
@ -215,16 +223,22 @@ Select input with options
]
}
```
### Color
Color picker
```js
{
type: 'color',
// ...
}
```
### Button
Button with a command to assign
```js
{
type: 'button',
@ -247,7 +261,7 @@ The trait is a simple property of the component so to get the complete list of c
```js
const component = editor.getSelected(); // Component selected in canvas
const traits = component.get('traits');
traits.forEach(trait => console.log(trait.props()))
traits.forEach((trait) => console.log(trait.props()));
```
In case you need a single one:
@ -289,9 +303,6 @@ component.addTrait({
component.removeTrait('type');
```
## I18n
To leverage the [I18n module](I18n.html), you can refer to this schema
@ -327,10 +338,6 @@ To leverage the [I18n module](I18n.html), you can refer to this schema
}
```
## Customization
The default types should cover most of the common properties but in case you need a more advanced UI you can [define your own types](#define-new-trait-type) or even create a completely [custom Trait Manager UI](#custom-trait-manager) from scratch.
@ -359,9 +366,9 @@ editor.Components.addType('link', {
name: 'href',
label: 'New href',
},
]
}
}
],
},
},
});
```
@ -373,16 +380,18 @@ editor.Traits.addType('href-next', {
createInput({ trait }) {
// Here we can decide to use properties from the trait
const traitOpts = trait.get('options') || [];
const options = traitOpts.length ? traitOpts : [
{ id: 'url', label: 'URL' },
{ id: 'email', label: 'Email' },
];
const options = traitOpts.length
? traitOpts
: [
{ id: 'url', label: 'URL' },
{ id: 'email', label: 'Email' },
];
// Create a new element container and add some content
const el = document.createElement('div');
el.innerHTML = `
<select class="href-next__type">
${options.map(opt => `<option value="${opt.id}">${opt.label}</option>`).join('')}
${options.map((opt) => `<option value="${opt.id}">${opt.label}</option>`).join('')}
</select>
<div class="href-next__url-inputs">
<input class="href-next__url" placeholder="Insert URL"/>
@ -397,7 +406,7 @@ editor.Traits.addType('href-next', {
const inputsUrl = el.querySelector('.href-next__url-inputs');
const inputsEmail = el.querySelector('.href-next__email-inputs');
const inputType = el.querySelector('.href-next__type');
inputType.addEventListener('change', ev => {
inputType.addEventListener('change', (ev) => {
switch (ev.target.value) {
case 'url':
inputsUrl.style.display = '';
@ -498,7 +507,7 @@ editor.Traits.addType('href-next', {
break;
}
component.addAttributes({ href })
component.addAttributes({ href });
},
});
```
@ -550,15 +559,16 @@ editor.Traits.addType('href-next', {
```
Now the trait will update even on component change like:
```js
editor.getSelected().addAttributes({ href: 'mailto:new-email@test.com?subject=NewSubject' })
editor.getSelected().addAttributes({ href: 'mailto:new-email@test.com?subject=NewSubject' });
```
To recap what we have done so far, to create a custom trait type all you will need are 3 methods:
* `createInput` - Where we define our custom HTML element
* `onEvent` - How to update the component on inputs changes
* `onUpdate` - How to update inputs on component changes
- `createInput` - Where we define our custom HTML element
- `onEvent` - How to update the component on inputs changes
- `onUpdate` - How to update inputs on component changes
#### Result
@ -572,9 +582,9 @@ By looking at the example above might seems like a lot of code, but at the end,
```js
editor.Traits.addType('slider', {
createInput({ trait }) {
const vueInst = new Vue({ render: h => h(VueSlider) }).$mount();
const vueInst = new Vue({ render: (h) => h(VueSlider) }).$mount();
const sliderInst = vueInst.$children[0];
sliderInst.$on('change', ev => this.onChange(ev)); // Use onChange to trigger onEvent
sliderInst.$on('change', (ev) => this.onChange(ev)); // Use onChange to trigger onEvent
this.sliderInst = sliderInst;
return vueInst.$el;
},
@ -596,13 +606,12 @@ editor.Traits.addType('slider', {
The integration with external components is possible by following these simple core points:
1. **Component rendering**: `new Vue({ render: ...`<br/>
Depends on the framework, for example, in React it should be `ReactDOM.render(element, ...`
Depends on the framework, for example, in React it should be `ReactDOM.render(element, ...`
1. **Change propagation**: `sliderInst.$on('change', ev => this.onChange(ev))`<br/>
The framework should have a mechanism to subscribe to changes and the component [should expose that change](https://nightcatsama.github.io/vue-slider-component/#/api/events)<br/>
We've also used `onChange` method which comes handy when you need to trigger manually the `onEvent` event (you should never call directly `onEvent` method, but only via `onChange` when you need)
The framework should have a mechanism to subscribe to changes and the component [should expose that change](https://nightcatsama.github.io/vue-slider-component/#/api/events)<br/>
We've also used `onChange` method which comes handy when you need to trigger manually the `onEvent` event (you should never call directly `onEvent` method, but only via `onChange` when you need)
1. **Property getters/setters**: [`sliderInst.getValue()`](https://nightcatsama.github.io/vue-slider-component/#/api/methods?hash=getvalue)/ [`sliderInst.setValue(value)`](https://nightcatsama.github.io/vue-slider-component/#/api/methods?hash=setvaluevalue)<br/>
The component should allow to read and write data from the instance
The component should allow to read and write data from the instance
### Custom Trait Manager
@ -612,17 +621,16 @@ All you have to do is to indicate to the editor your intent to use a custom UI a
```js
const editor = grapesjs.init({
// ...
traitManager: {
custom: true,
// ...
traitManager: {
custom: true,
// ...
},
},
});
editor.on('trait:custom', props => {
// props.container (HTMLElement) - The default element where you can append your custom UI
// Here you would put the logic to render/update your UI.
editor.on('trait:custom', (props) => {
// props.container (HTMLElement) - The default element where you can append your custom UI
// Here you would put the logic to render/update your UI.
});
```
@ -911,11 +919,8 @@ In the example below we'll replicate most of the default functionality by using
</script>
-->
## Events
For a complete list of available events, you can check it [here](/api/trait_manager.html#available-events).
[Traits API]: </api/trait_manager.html>
[Traits API]: /api/trait_manager.html

82
index.html

@ -1,9 +1,9 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta charset="utf-8" />
<title>GrapesJS</title>
<link rel="stylesheet" href="dist/css/grapes.min.css">
<link rel="stylesheet" href="dist/css/grapes.min.css" />
<script src="grapes.min.js"></script>
<style>
body,
@ -15,18 +15,21 @@
</head>
<body>
<div id="gjs" style="height:0px; overflow:hidden;">
<div id="gjs" style="height: 0px; overflow: hidden">
<div class="panel">
<h1 class="welcome">Welcome to</h1>
<div class="big-title">
<svg class="logo" viewBox="0 0 100 100">
<path d="M40 5l-12.9 7.4 -12.9 7.4c-1.4 0.8-2.7 2.3-3.7 3.9 -0.9 1.6-1.5 3.5-1.5 5.1v14.9 14.9c0 1.7 0.6 3.5 1.5 5.1 0.9 1.6 2.2 3.1 3.7 3.9l12.9 7.4 12.9 7.4c1.4 0.8 3.3 1.2 5.2 1.2 1.9 0 3.8-0.4 5.2-1.2l12.9-7.4 12.9-7.4c1.4-0.8 2.7-2.2 3.7-3.9 0.9-1.6 1.5-3.5 1.5-5.1v-14.9 -12.7c0-4.6-3.8-6-6.8-4.2l-28 16.2"/>
<path
d="M40 5l-12.9 7.4 -12.9 7.4c-1.4 0.8-2.7 2.3-3.7 3.9 -0.9 1.6-1.5 3.5-1.5 5.1v14.9 14.9c0 1.7 0.6 3.5 1.5 5.1 0.9 1.6 2.2 3.1 3.7 3.9l12.9 7.4 12.9 7.4c1.4 0.8 3.3 1.2 5.2 1.2 1.9 0 3.8-0.4 5.2-1.2l12.9-7.4 12.9-7.4c1.4-0.8 2.7-2.2 3.7-3.9 0.9-1.6 1.5-3.5 1.5-5.1v-14.9 -12.7c0-4.6-3.8-6-6.8-4.2l-28 16.2"
/>
</svg>
<span>GrapesJS</span>
</div>
<div class="description">
This is a demo content from index.html. For the development, you shouldn't edit this file, instead you can
copy and rename it to _index.html, on next server start the new file will be served, and it will be ignored by git.
copy and rename it to _index.html, on next server start the new file will be served, and it will be ignored by
git.
</div>
</div>
<style>
@ -37,8 +40,8 @@
padding: 30px 20px;
margin: 150px auto 0px;
background-color: #d983a6;
box-shadow: 0px 3px 10px 0px rgba(0,0,0,0.25);
color:rgba(255,255,255,0.75);
box-shadow: 0px 3px 10px 0px rgba(0, 0, 0, 0.25);
color: rgba(255, 255, 255, 0.75);
font: caption;
font-weight: 100;
}
@ -60,7 +63,7 @@
fill: none;
stroke-linecap: round;
stroke-width: 7;
stroke: #fff
stroke: #fff;
}
.big-title {
@ -74,7 +77,6 @@
font-size: 1rem;
line-height: 1.5rem;
}
</style>
</div>
@ -86,41 +88,73 @@
height: '100%',
fromElement: true,
storageManager: { autoload: 0 },
styleManager : {
sectors: [{
styleManager: {
sectors: [
{
name: 'General',
open: false,
buildProps: ['float', 'display', 'position', 'top', 'right', 'left', 'bottom']
},{
buildProps: ['float', 'display', 'position', 'top', 'right', 'left', 'bottom'],
},
{
name: 'Flex',
open: false,
buildProps: ['flex-direction', 'flex-wrap', 'justify-content', 'align-items', 'align-content', 'order', 'flex-basis', 'flex-grow', 'flex-shrink', 'align-self']
},{
buildProps: [
'flex-direction',
'flex-wrap',
'justify-content',
'align-items',
'align-content',
'order',
'flex-basis',
'flex-grow',
'flex-shrink',
'align-self',
],
},
{
name: 'Dimension',
open: false,
buildProps: ['width', 'height', 'max-width', 'min-height', 'margin', 'padding'],
},{
},
{
name: 'Typography',
open: false,
buildProps: ['font-family', 'font-size', 'font-weight', 'letter-spacing', 'color', 'line-height', 'text-shadow'],
},{
buildProps: [
'font-family',
'font-size',
'font-weight',
'letter-spacing',
'color',
'line-height',
'text-shadow',
],
},
{
name: 'Decorations',
open: false,
buildProps: ['border-radius-c', 'background-color', 'border-radius', 'border', 'box-shadow', 'background'],
},{
buildProps: [
'border-radius-c',
'background-color',
'border-radius',
'border',
'box-shadow',
'background',
],
},
{
name: 'Extra',
open: false,
buildProps: ['transition', 'perspective', 'transform'],
}
},
],
},
});
editor.BlockManager.add('testBlock', {
label: 'Block',
attributes: { class:'gjs-fonts gjs-f-b1' },
content: `<div style="padding-top:50px; padding-bottom:50px; text-align:center">Test block</div>`
})
attributes: { class: 'gjs-fonts gjs-f-b1' },
content: `<div style="padding-top:50px; padding-bottom:50px; text-align:center">Test block</div>`,
});
</script>
</body>
</html>

12
jest.config.js

@ -0,0 +1,12 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
testEnvironment: 'jsdom',
moduleFileExtensions: ['js', 'ts'],
verbose: true,
testEnvironmentOptions: {
url: 'http://localhost/',
},
modulePaths: ['<rootDir>/src'],
testMatch: ['<rootDir>/test/specs/**/*.(t|j)s'],
setupFilesAfterEnv: ['<rootDir>/test/setup.js'],
};

65
package.json

@ -1,7 +1,7 @@
{
"name": "grapesjs",
"description": "Free and Open Source Web Builder Framework",
"version": "0.21.12",
"version": "0.21.13",
"author": "Artur Arseniev",
"license": "BSD-3-Clause",
"homepage": "http://grapesjs.com",
@ -40,20 +40,30 @@
"underscore": "^1.13.1"
},
"devDependencies": {
"@babel/cli": "^7.15.7",
"@babel/preset-typescript": "^7.16.7",
"@types/jest": "^28.1.1",
"@typescript-eslint/parser": "^5.22.0",
"@babel/cli": "7.24.8",
"@babel/preset-typescript": "7.24.7",
"@types/jest": "29.5.12",
"@types/markdown-it": "14.1.2",
"@typescript-eslint/eslint-plugin": "8.0.1",
"@typescript-eslint/parser": "8.0.1",
"@vuepress/plugin-google-analytics": "^1.8.2",
"@vuepress/types": "^1.9.10",
"documentation": "^13.2.5",
"eslint": "^8.12.0",
"eslint": "8.57.0",
"eslint-config-prettier": "9.1.0",
"eslint-plugin-jest": "28.8.0",
"eslint-plugin-prettier": "5.1.3",
"grapesjs-cli": "^4.1.3",
"html-entities": "^1.4.0",
"jest": "^24.9.0",
"jest": "29.7.0",
"jest-environment-jsdom": "29.7.0",
"jsdom": "24.1.1",
"npm-run-all": "^4.1.5",
"prettier": "^2.4.1",
"postcss": "8",
"prettier": "3.3.3",
"sass": "^1.42.1",
"vuepress": "^1.8.2",
"typescript": "5.4.5",
"vuepress": "^1.9.10",
"whatwg-fetch": "^3.6.2"
},
"resolutions": {
@ -71,43 +81,15 @@
"site",
"builder"
],
"babel": {
"env": {
"test": {
"presets": [
"@babel/preset-env",
"@babel/preset-typescript"
]
}
}
},
"jest": {
"moduleFileExtensions": [
"js",
"ts"
],
"verbose": true,
"testURL": "http://localhost/",
"modulePaths": [
"<rootDir>/src"
],
"testMatch": [
"<rootDir>/test/specs/**/*.(t|j)s"
],
"setupFiles": [
"<rootDir>/test/setup.js"
]
},
"scripts": {
"docs": "vuepress dev docs",
"docs:api": "node docs/api.js",
"docs:build-vp": "vuepress build docs",
"docs:build": "npm run docs:api && npm run docs:build-vp",
"docs:deploy": "docs/deploy.sh",
"lint": "eslint . --ext .ts,.js && npm run lint:ts",
"check": "npm run lint && npm run test",
"lint": "eslint .",
"lint:ts": "tsc --noEmit",
"build": "npm run check && npm run build-all && npm run ts:check",
"build": "npm run build-all && npm run ts:check",
"build-all": "run-s build:*",
"build:js": "grapesjs-cli build --patch=false --targets=\"> 1%, ie 11, safari 8, not dead\" --statsOutput=\"stats.json\" --localePath=\"src/i18n/locale\"",
"build:mjs": "BUILD_MODULE=true grapesjs-cli build --dts='skip' --patch=false --targets=\"> 1%, ie 11, safari 8, not dead\"",
@ -118,8 +100,9 @@
"start": "run-p start:*",
"start:js": "grapesjs-cli serve",
"start:css": "npm run build:css -- --watch",
"format": "prettier --single-quote --write './{src,test}/**/*.js'",
"test": "jest",
"format": "prettier . --write",
"format:check": "prettier . --check",
"test": "jest --forceExit",
"test:dev": "jest --watch"
}
}

14
src/abstract/Module.ts

@ -78,7 +78,7 @@ export default abstract class Module<T extends ModuleConfig = ModuleConfig> impl
}
getConfig<P extends keyof T | undefined = undefined, R = P extends keyof T ? T[P] : T>(
name?: P
name?: P,
): R & { pStylePrefix?: string } {
// @ts-ignore
return name ? this.config[name] : this.config;
@ -98,8 +98,8 @@ export default abstract class Module<T extends ModuleConfig = ModuleConfig> impl
this.view?.remove();
this.state?.stopListening();
this.state?.clear();
this.debounced.forEach(d => d.cancel());
this.collections.forEach(c => {
this.debounced.forEach((d) => d.cancel());
this.collections.forEach((c) => {
c.stopListening();
c.reset();
});
@ -122,7 +122,7 @@ export default abstract class Module<T extends ModuleConfig = ModuleConfig> impl
export abstract class ItemManagerModule<
TConf extends ModuleConfig = ModuleConfig,
TCollection extends Collection = Collection
TCollection extends Collection = Collection,
> extends Module<TConf> {
cls: any[] = [];
protected all: TCollection;
@ -134,7 +134,7 @@ export abstract class ItemManagerModule<
all: any,
events?: any,
defaults?: TConf,
opts: { skipListen?: boolean } = {}
opts: { skipListen?: boolean } = {},
) {
super(em, moduleName, defaults);
this.all = all;
@ -220,7 +220,7 @@ export abstract class ItemManagerModule<
entity.on('all', (ev: any, model: any, coll: any, opts: any) => {
const options = opts || coll;
const opt = { event: ev, ...options };
[em, all].map(md => md.trigger(event, model, opt));
[em, all].map((md) => md.trigger(event, model, opt));
});
});
}
@ -284,7 +284,7 @@ export abstract class ItemManagerModule<
}
__destroy() {
this.cls.forEach(coll => {
this.cls.forEach((coll) => {
coll.stopListening();
coll.reset();
});

4
src/abstract/ModuleCategories.ts

@ -19,7 +19,7 @@ export default class Categories extends Collection<Category> {
if (em) {
evUpdate &&
this.on('change', (category, options) =>
em.trigger(evUpdate, { category, changes: category.changedAttributes(), options })
em.trigger(evUpdate, { category, changes: category.changedAttributes(), options }),
);
}
}
@ -27,7 +27,7 @@ export default class Categories extends Collection<Category> {
/** @ts-ignore */
add(model: (CategoryProperties | Category)[] | CategoryProperties | Category, opts?: AddOptions) {
const models = isArray(model) ? model : [model];
models.forEach(md => md && (md.id = normalizeKey(`${md.id}`)));
models.forEach((md) => md && (md.id = normalizeKey(`${md.id}`)));
return super.add(model, opts);
}

4
src/abstract/ModuleCategory.ts

@ -61,7 +61,7 @@ export function getItemsByCategory<T extends ModelWithCategory>(allItems: T[]) {
const categoryMap = new Map<Category, T[]>();
const emptyItem: ItemsByCategory<T> = { items: [] };
allItems.forEach(item => {
allItems.forEach((item) => {
const { category } = item;
if (category) {
@ -78,7 +78,7 @@ export function getItemsByCategory<T extends ModelWithCategory>(allItems: T[]) {
}
});
const categoryWithItems = Array.from(categorySet).map(category => ({
const categoryWithItems = Array.from(categorySet).map((category) => ({
category,
items: categoryMap.get(category) || [],
}));

4
src/abstract/ModuleCollection.ts

@ -15,7 +15,7 @@ export default class ModuleCollection<TModel extends ModuleModel = ModuleModel>
//Note: the undefined case needed because backbonejs not handle the reset() correctly
var models = isArray(model) ? model : !isUndefined(model) ? [model] : undefined;
models = models?.map(m => (m instanceof this.newModel ? m : new this.newModel(this.module, m))) ?? [undefined];
models = models?.map((m) => (m instanceof this.newModel ? m : new this.newModel(this.module, m))) ?? [undefined];
return super.add(isArray(model) ? models : models[0], options);
}
@ -23,7 +23,7 @@ export default class ModuleCollection<TModel extends ModuleModel = ModuleModel>
constructor(
module: ModuleExt<TModel>,
models: TModel[] | Array<Record<string, any>>,
modelConstructor: ModelConstructor<TModel>
modelConstructor: ModelConstructor<TModel>,
) {
super(models, { module, modelConstructor });
}

4
src/abstract/ModuleDomainViews.ts

@ -5,7 +5,7 @@ import { View } from '../common';
export default abstract class ModuleDomainViews<
TCollection extends ModuleCollection,
TItemView extends ModuleView
TItemView extends ModuleView,
> extends ModuleView<TCollection> {
// Defines the View per type
itemsView = '';
@ -68,7 +68,7 @@ export default abstract class ModuleDomainViews<
this.clearItems();
this.$el.empty();
if (this.collection.length) this.collection.each(model => this.add(model, frag));
if (this.collection.length) this.collection.each((model) => this.add(model, frag));
this.$el.append(frag);
this.onRender();

2
src/abstract/ModuleModel.ts

@ -6,7 +6,7 @@ export default class ModuleModel<
TModule extends IBaseModule<any> = Module,
T extends ObjectHash = any,
S = SetOptions,
E = any
E = any,
> extends Model<T, S, E> {
private _module: TModule;

9
src/abstract/ModuleView.ts

@ -5,17 +5,14 @@ import { View } from '../common';
import EditorModel from '../editor/model/Editor';
type ModuleFromModel<TModel extends ModuleModel> = TModel extends ModuleModel<infer M> ? M : unknown;
type ModuleModelExt<TItem extends ModuleModel | ModuleCollection> = TItem extends ModuleCollection<infer M>
? ModuleFromModel<M>
: TItem extends ModuleModel<infer M>
? M
: unknown;
type ModuleModelExt<TItem extends ModuleModel | ModuleCollection> =
TItem extends ModuleCollection<infer M> ? ModuleFromModel<M> : TItem extends ModuleModel<infer M> ? M : unknown;
// type TCollection<TItem extends ModuleModel | ModuleCollection> = TItem extends ModuleCollection ? TItem : unknown;
export default class ModuleView<
TModel extends ModuleModel | ModuleCollection = ModuleModel,
TElement extends Element = HTMLElement
TElement extends Element = HTMLElement,
> extends View<TModel extends ModuleModel ? TModel : undefined, TElement> {
protected get pfx() {
return this.ppfx + (this.config as any).stylePrefix || '';

2
src/asset_manager/view/AssetsView.ts

@ -186,7 +186,7 @@ export default class AssetsView extends View {
const assets = this.$el.find(`.${this.pfx}assets`);
assets.empty();
this.toggleNoAssets(!!this.collection.length);
this.collection.each(model => this.addAsset(model, fragment));
this.collection.each((model) => this.addAsset(model, fragment));
assets.append(fragment);
}

20
src/asset_manager/view/FileUploader.ts

@ -182,7 +182,7 @@ export default class FileUploaderView extends View {
const fetchResult = customFetch
? customFetch(url, fetchOptsResult)
: fetch(url, fetchOptsResult).then((res: any) =>
((res.status / 200) | 0) == 1 ? res.text() : res.text().then((text: string) => Promise.reject(text))
((res.status / 200) | 0) == 1 ? res.text() : res.text().then((text: string) => Promise.reject(text)),
);
return fetchResult
.then((text: string) => this.onUploadResponse(text, clb))
@ -269,7 +269,7 @@ export default class FileUploaderView extends View {
cleanEditorElCls();
if ('draggable' in edEl) {
[edEl, frameEl].forEach(item => {
[edEl, frameEl].forEach((item) => {
item.ondragover = onDragOver;
item.ondragleave = onDragLeave;
item.ondrop = onDrop;
@ -286,7 +286,7 @@ export default class FileUploaderView extends View {
disabled: this.disabled,
multiUpload: this.multiUpload,
pfx,
})
}),
);
this.initDrop();
$el.attr('class', pfx + 'file-uploader');
@ -314,7 +314,7 @@ export default class FileUploaderView extends View {
// and a promise (to track and merge results and errors)
const promise = new Promise((resolve, reject) => {
const reader = new FileReader();
reader.addEventListener('load', event => {
reader.addEventListener('load', (event) => {
let type;
const name = file.name;
@ -357,7 +357,7 @@ export default class FileUploaderView extends View {
};
const image = new Image();
image.addEventListener('error', error => {
image.addEventListener('error', (error) => {
reject(error);
});
image.addEventListener('load', () => {
@ -379,10 +379,10 @@ export default class FileUploaderView extends View {
resolve(reader.result);
}
});
reader.addEventListener('error', error => {
reader.addEventListener('error', (error) => {
reject(error);
});
reader.addEventListener('abort', error => {
reader.addEventListener('abort', (error) => {
reject('Aborted');
});
@ -393,15 +393,15 @@ export default class FileUploaderView extends View {
}
return Promise.all(promises).then(
data => {
(data) => {
response.data = data;
// @ts-ignore
this.onUploadResponse(response, clb);
},
error => {
(error) => {
// @ts-ignore
this.onUploadError(error);
}
},
);
}
}

22
src/block_manager/index.ts

@ -72,9 +72,9 @@ export default class BlockManager extends ItemManagerModule<BlockManagerConfig,
});
// Setup the sync between the global and public collections
this.blocks.on('add', model => this.blocksVisible.add(model));
this.blocks.on('remove', model => this.blocksVisible.remove(model));
this.blocks.on('reset', coll => this.blocksVisible.reset(coll.models));
this.blocks.on('add', (model) => this.blocksVisible.add(model));
this.blocks.on('remove', (model) => this.blocksVisible.remove(model));
this.blocks.on('reset', (coll) => this.blocksVisible.reset(coll.models));
this.__onAllEvent = debounce(() => this.__trgCustom(), 0);
@ -109,13 +109,13 @@ export default class BlockManager extends ItemManagerModule<BlockManagerConfig,
const content = block.getContent ? block.getContent() : block;
this._dragBlock = block;
em.set({ dragResult: null, dragContent: content });
[em, blocks].map(i => i.trigger(events.dragStart, block, ev));
[em, blocks].map((i) => i.trigger(events.dragStart, block, ev));
}
__drag(ev: Event) {
const { em, events, blocks } = this;
const block = this._dragBlock;
[em, blocks].map(i => i.trigger(events.drag, block, ev));
[em, blocks].map((i) => i.trigger(events.drag, block, ev));
}
__endDrag(opts: { component?: Component } = {}) {
@ -148,13 +148,13 @@ export default class BlockManager extends ItemManagerModule<BlockManagerConfig,
em.set({ dragResult: null, dragContent: null });
if (block) {
[em, blocks].map(i => i.trigger(events.dragEnd, cmp, block));
[em, blocks].map((i) => i.trigger(events.dragEnd, cmp, block));
}
}
__getFrameViews(): FrameView[] {
return this.em.Canvas.getFrames()
.map(frame => frame.view!)
.map((frame) => frame.view!)
.filter(Boolean);
}
@ -171,11 +171,11 @@ export default class BlockManager extends ItemManagerModule<BlockManagerConfig,
startDrag(block: Block, ev?: Event) {
this.__startDrag(block, ev);
this.__getFrameViews().forEach(fv => fv.droppable?.startCustom());
this.__getFrameViews().forEach((fv) => fv.droppable?.startCustom());
}
endDrag(cancel?: boolean) {
this.__getFrameViews().forEach(fv => fv.droppable?.endCustom(cancel));
this.__getFrameViews().forEach((fv) => fv.droppable?.endCustom(cancel));
this.__endDrag();
}
@ -350,8 +350,8 @@ export default class BlockManager extends ItemManagerModule<BlockManagerConfig,
destroy() {
const colls = [this.blocks, this.blocksVisible, this.categories];
colls.map(c => c.stopListening());
colls.map(c => c.reset());
colls.map((c) => c.stopListening());
colls.map((c) => c.reset());
this.blocksView?.remove();
}
}

2
src/block_manager/view/BlocksView.ts

@ -184,7 +184,7 @@ export default class BlocksView extends View {
</div>
`;
this.collection.each(model => this.add(model, frag));
this.collection.each((model) => this.add(model, frag));
this.append(frag);
const cls = `${this.blockContClass}s ${ppfx}one-bg ${ppfx}two-color`;
this.$el.addClass(cls);

6
src/canvas/index.ts

@ -528,7 +528,7 @@ export default class CanvasModule extends Module<CanvasConfig> {
const docActive = frame && document.activeElement === frame;
const focused = docActive ? doc && doc.activeElement : document.activeElement;
return focused && !toIgnore.some(item => focused.matches(item));
return focused && !toIgnore.some((item) => focused.matches(item));
}
/**
@ -670,7 +670,7 @@ export default class CanvasModule extends Module<CanvasConfig> {
}
getFrames() {
return this.canvas.frames.map(item => item);
return this.canvas.frames.map((item) => item);
}
/**
@ -861,6 +861,6 @@ export default class CanvasModule extends Module<CanvasConfig> {
this.canvasView?.remove();
//[this.canvas, this.canvasView].forEach(i => (i = {}));
//@ts-ignore
['model', 'droppable'].forEach(i => (this[i] = {}));
['model', 'droppable'].forEach((i) => (this[i] = {}));
}
}

2
src/canvas/model/Canvas.ts

@ -51,7 +51,7 @@ export default class Canvas extends ModuleModel<CanvasModule> {
const { em } = this;
em.setSelected();
em.get('readyCanvas') && em.stopDefault(); // We have to stop before changing current frames
prev?.getFrames().map(frame => frame.disable());
prev?.getFrames().map((frame) => frame.disable());
this.set('frames', page.getFrames());
this.updateDevice({ frame: page.getMainFrame() });
}

6
src/canvas/model/Frame.ts

@ -179,7 +179,9 @@ export default class Frame extends ModuleModel<CanvasModule> {
}
getHeadByAttr(attr: string, value: any, tag: string) {
return this.head.filter(item => item.attributes && item.attributes[attr] == value && (!tag || tag === item.tag))[0];
return this.head.filter(
(item) => item.attributes && item.attributes[attr] == value && (!tag || tag === item.tag),
)[0];
}
removeHeadByAttr(attr: string, value: any, tag: string) {
@ -262,7 +264,7 @@ export default class Frame extends ModuleModel<CanvasModule> {
if (obj[key] === value) delete obj[key];
});
forEach(['attributes', 'head'], prop => {
forEach(['attributes', 'head'], (prop) => {
if (isEmpty(obj[prop])) delete obj[prop];
});

8
src/canvas/model/Frames.ts

@ -15,7 +15,7 @@ export default class Frames extends ModuleCollection<Frame> {
this.on('add', this.onAdd);
this.on('reset', this.onReset);
this.on('remove', this.onRemove);
this.forEach(frame => this.onAdd(frame));
this.forEach((frame) => this.onAdd(frame));
}
onAdd(frame: Frame) {
@ -24,7 +24,7 @@ export default class Frames extends ModuleCollection<Frame> {
onReset(m: Frame, opts?: { previousModels?: Frame[] }) {
const prev = opts?.previousModels || [];
prev.map(p => this.onRemove(p));
prev.map((p) => this.onRemove(p));
}
onRemove(frame: Frame) {
@ -33,7 +33,7 @@ export default class Frames extends ModuleCollection<Frame> {
}
initRefs() {
this.forEach(frame => frame.initRefs());
this.forEach((frame) => frame.initRefs());
}
itemLoaded() {
@ -52,6 +52,6 @@ export default class Frames extends ModuleCollection<Frame> {
}
listenToLoadItems(on: boolean) {
this.forEach(item => item[on ? 'on' : 'off']('loaded', this.itemLoaded));
this.forEach((item) => item[on ? 'on' : 'off']('loaded', this.itemLoaded));
}
}

6
src/canvas/view/CanvasView.ts

@ -118,7 +118,7 @@ export default class CanvasView extends ModuleView<Canvas> {
{
...config,
canvasView: this,
}
},
);
}
@ -127,7 +127,7 @@ export default class CanvasView extends ModuleView<Canvas> {
const currFrame = this.em.getCurrentFrame();
scroll &&
component.views?.forEach(view => {
component.views?.forEach((view) => {
view.frameView === currFrame && view.scrollIntoView(scroll);
});
}
@ -525,7 +525,7 @@ export default class CanvasView extends ModuleView<Canvas> {
'borderBottomWidth',
'borderLeftWidth',
];
marginPaddingOffsets.forEach(offset => {
marginPaddingOffsets.forEach((offset) => {
result[offset] = parseFloat(styles[offset]) * zoom;
});

45
src/canvas/view/FrameView.ts

@ -99,18 +99,18 @@ export default class FrameView extends ModuleView<Frame, HTMLIFrameElement> {
const attrStr = (attr: any = {}) =>
Object.keys(attr)
.sort()
.map(i => `[${i}="${attr[i]}"]`)
.map((i) => `[${i}="${attr[i]}"]`)
.join('');
const find = (items: any[], stack: any[], res: any[]) => {
items.forEach(item => {
items.forEach((item) => {
const { tag, attributes } = item;
const has = stack.some(s => s.tag === tag && attrStr(s.attributes) === attrStr(attributes));
const has = stack.some((s) => s.tag === tag && attrStr(s.attributes) === attrStr(attributes));
!has && res.push(item);
});
};
find(current, prev, toAdd);
find(prev, current, toRemove);
toRemove.forEach(stl => {
toRemove.forEach((stl) => {
const el = headEl.querySelector(`${stl.tag}${attrStr(stl.attributes)}`);
el?.parentNode?.removeChild(el);
});
@ -122,7 +122,7 @@ export default class FrameView extends ModuleView<Frame, HTMLIFrameElement> {
}
getCanvasModel(): Canvas {
return this.em.Canvas.getModel();
return this?.em.Canvas?.getModel();
}
getWindow() {
@ -344,8 +344,8 @@ export default class FrameView extends ModuleView<Frame, HTMLIFrameElement> {
renderStyles(opts: any = {}) {
const head = this.getHead();
const canvas = this.getCanvasModel();
const normalize = (stls: any[]) =>
stls.map(href => ({
const normalize = (stls: any[] = []) =>
stls.map((href) => ({
tag: 'link',
attributes: {
rel: 'stylesheet',
@ -353,19 +353,19 @@ export default class FrameView extends ModuleView<Frame, HTMLIFrameElement> {
},
}));
const prevStyles = normalize(opts.prev || canvas.previous('styles'));
const styles = normalize(canvas.get('styles'));
const styles = normalize(canvas?.get('styles'));
const toRemove: any[] = [];
const toAdd: any[] = [];
const find = (items: any[], stack: any[], res: any[]) => {
items.forEach(item => {
items.forEach((item) => {
const { href } = item.attributes;
const has = stack.some(s => s.attributes.href === href);
const has = stack.some((s) => s.attributes.href === href);
!has && res.push(item);
});
};
find(styles, prevStyles, toAdd);
find(prevStyles, styles, toRemove);
toRemove.forEach(stl => {
toRemove.forEach((stl) => {
const el = head.querySelector(`link[href="${stl.attributes.href}"]`);
el?.parentNode?.removeChild(el);
});
@ -375,7 +375,8 @@ export default class FrameView extends ModuleView<Frame, HTMLIFrameElement> {
renderHead() {
const { model, em } = this;
const { root } = model;
const HeadView = em.Components.getType(typeHead)!.view;
const HeadView = em?.Components?.getType(typeHead)!.view;
if (!HeadView) return;
this.headView = new HeadView({
el: this.getHead(),
model: root.head,
@ -472,10 +473,12 @@ export default class FrameView extends ModuleView<Frame, HTMLIFrameElement> {
${conf.canvasCss || ''}
${conf.protectedCss || ''}
</style>`
</style>`,
);
const { root } = model;
const { view } = em.Components.getType('wrapper')!;
const { view } = em?.Components?.getType('wrapper') || {};
if (!view) return;
this.wrapper = new view({
model: root,
config: {
@ -494,7 +497,7 @@ export default class FrameView extends ModuleView<Frame, HTMLIFrameElement> {
...em.Css.getConfig(),
frameView: this,
},
}).render().el
}).render().el,
);
append(body, this.getJsContainer());
// em.trigger('loaded'); // I need to manage only the first one maybe
@ -502,8 +505,8 @@ export default class FrameView extends ModuleView<Frame, HTMLIFrameElement> {
// Avoid some default behaviours
//@ts-ignore
on(body, 'click', ev => ev && ev.target?.tagName == 'A' && ev.preventDefault());
on(body, 'submit', ev => ev && ev.preventDefault());
on(body, 'click', (ev) => ev && ev.target?.tagName == 'A' && ev.preventDefault());
on(body, 'submit', (ev) => ev && ev.preventDefault());
// When the iframe is focused the event dispatcher is not the same so
// I need to delegate all events to the parent document
@ -512,10 +515,10 @@ export default class FrameView extends ModuleView<Frame, HTMLIFrameElement> {
{ event: 'mousedown mousemove mouseup', class: 'MouseEvent' },
{ event: 'pointerdown pointermove pointerup', class: 'PointerEvent' },
{ event: 'wheel', class: 'WheelEvent', opts: { passive: !config.infiniteCanvas } },
].forEach(obj =>
obj.event.split(' ').forEach(event => {
doc.addEventListener(event, ev => this.el.dispatchEvent(createCustomEvent(ev, obj.class)), obj.opts);
})
].forEach((obj) =>
obj.event.split(' ').forEach((event) => {
doc.addEventListener(event, (ev) => this.el.dispatchEvent(createCustomEvent(ev, obj.class)), obj.opts);
}),
);
this._toggleEffects(true);

8
src/canvas/view/FrameWrapView.ts

@ -79,7 +79,7 @@ export default class FrameWrapView extends ModuleView<Frame> {
this.__clear(opts);
ModuleView.prototype.remove.apply(this, opts);
//@ts-ignore
['frame', 'dragger', 'cv', 'elTools'].forEach(i => (this[i] = 0));
['frame', 'dragger', 'cv', 'elTools'].forEach((i) => (this[i] = 0));
return this;
}
@ -123,7 +123,7 @@ export default class FrameWrapView extends ModuleView<Frame> {
...(!width ? { width: el.offsetWidth } : {}),
...(!height ? { height: el.offsetHeight } : {}),
},
{ silent: 1 }
{ silent: 1 },
);
}
@ -211,7 +211,7 @@ export default class FrameWrapView extends ModuleView<Frame> {
<div class="${ppfx}frame-wrapper__right" data-frame-right></div>
<div class="${ppfx}frame-wrapper__left" data-frame-left></div>
<div class="${ppfx}frame-wrapper__bottom" data-frame-bottom></div>
`
`,
)
.append(frame.el);
const elTools = createEl(
@ -244,7 +244,7 @@ export default class FrameWrapView extends ModuleView<Frame> {
</div>
</div>
<div class="${ppfx}offset-fixed-v"></div>
`
`,
);
this.elTools = elTools;
const twrp = cv?.toolsWrapper;

4
src/canvas/view/FramesView.ts

@ -16,7 +16,7 @@ export default class FramesView extends ModuleDomainViews<Frames, FrameWrapView>
}
onRemoveBefore(items: FrameWrapView[], opts = {}) {
items.forEach(item => item.remove(opts));
items.forEach((item) => item.remove(opts));
}
onRender() {
@ -26,7 +26,7 @@ export default class FramesView extends ModuleDomainViews<Frames, FrameWrapView>
clearItems() {
const items = this.viewCollection || [];
items.forEach(item => item.remove());
items.forEach((item) => item.remove());
this.viewCollection = [];
}

20
src/code_manager/model/CssGenerator.ts

@ -99,7 +99,7 @@ export default class CssGenerator extends Model {
rules = this.matchedRules(model, rules);
}
rules.forEach(rule => {
rules.forEach((rule) => {
const atRule = rule.getAtRule();
if (atRule) {
@ -121,12 +121,12 @@ export default class CssGenerator extends Model {
}
});
this.sortMediaObject(atRules).forEach(item => {
this.sortMediaObject(atRules).forEach((item) => {
let rulesStr = '';
const atRule = item.key;
const mRules = item.value;
mRules.forEach(rule => {
mRules.forEach((rule) => {
const ruleStr = this.buildFromRule(rule, dump, opts);
if (rule.get('singleAtRule')) {
@ -147,7 +147,7 @@ export default class CssGenerator extends Model {
em && clearStyles && rules.remove && rules.remove(dump);
}
return json ? codeJson.filter(r => r) : code;
return json ? codeJson.filter((r) => r) : code;
}
/**
@ -164,7 +164,7 @@ export default class CssGenerator extends Model {
let found;
// This will not render a rule if there is no its component
rule.get('selectors')?.forEach(selector => {
rule.get('selectors')?.forEach((selector) => {
const name = selector.getFullName();
if (this.compCls.indexOf(name) >= 0 || this.ids.indexOf(name) >= 0 || opts.keepUnusedStyles) {
found = 1;
@ -191,13 +191,13 @@ export default class CssGenerator extends Model {
const el = component.getEl();
let result: CssRule[] = [];
rules.forEach(rule => {
rules.forEach((rule) => {
try {
if (
rule
.selectorsToString()
.split(',')
.some(selector => el?.matches(this.__cleanSelector(selector)))
.some((selector) => el?.matches(this.__cleanSelector(selector)))
) {
result.push(rule);
}
@ -235,7 +235,7 @@ export default class CssGenerator extends Model {
const itemsArr: { key: string; value: CssRule[] }[] = [];
each(items, (value, key) => itemsArr.push({ key, value }));
return itemsArr.sort((a, b) => {
const isMobFirst = [a.key, b.key].every(mquery => mquery.indexOf('min-width') !== -1);
const isMobFirst = [a.key, b.key].every((mquery) => mquery.indexOf('min-width') !== -1);
const left = isMobFirst ? a.key : b.key;
const right = isMobFirst ? b.key : a.key;
return this.getQueryLength(left) - this.getQueryLength(right);
@ -244,7 +244,7 @@ export default class CssGenerator extends Model {
sortRules(a: CssRule, b: CssRule) {
const getKey = (rule: CssRule) => rule.get('mediaText') || '';
const isMobFirst = [getKey(a), getKey(b)].every(q => q.indexOf('min-width') !== -1);
const isMobFirst = [getKey(a), getKey(b)].every((q) => q.indexOf('min-width') !== -1);
const left = isMobFirst ? getKey(a) : getKey(b);
const right = isMobFirst ? getKey(b) : getKey(a);
return this.getQueryLength(left) - this.getQueryLength(right);
@ -259,7 +259,7 @@ export default class CssGenerator extends Model {
__cleanSelector(selector: string) {
return selector
.split(' ')
.map(item => item.split(':')[0])
.map((item) => item.split(':')[0])
.join(' ');
}
}

20
src/commands/index.ts

@ -82,7 +82,7 @@ export const getOnComponentDrag = (em: Editor) => (data: any) => em.trigger(even
export const getOnComponentDragEnd =
(em: Editor, targets: Component[], opts: { altMode?: boolean } = {}) =>
(a: any, b: any, data: any) => {
targets.forEach(trg => trg.set('status', trg.get('selectable') ? 'selected' : ''));
targets.forEach((trg) => trg.set('status', trg.get('selectable') ? 'selected' : ''));
em.setSelected(targets);
targets[0].emitUpdate();
em.trigger(`${eventDrag}:end`, data);
@ -115,7 +115,7 @@ export default class CommandsModule extends Module<CommandsConfig & { pStylePref
}
// Load commands passed via configuration
Object.keys(config.defaults!).forEach(k => {
Object.keys(config.defaults!).forEach((k) => {
const obj = config.defaults![k];
if (obj.id) this.add(obj.id, obj);
});
@ -140,7 +140,7 @@ export default class CommandsModule extends Module<CommandsConfig & { pStylePref
const { event } = opts;
const trg = opts.target as Component | undefined;
const trgs = trg ? [trg] : [...ed.getSelectedAll()];
const targets = trgs.map(trg => trg.delegate?.move?.(trg) || trg).filter(Boolean);
const targets = trgs.map((trg) => trg.delegate?.move?.(trg) || trg).filter(Boolean);
const target = targets[targets.length - 1] as Component | undefined;
const nativeDrag = event?.type === 'dragstart';
const modes = ['absolute', 'translate'];
@ -152,7 +152,7 @@ export default class CommandsModule extends Module<CommandsConfig & { pStylePref
const mode = target.get('dmode') || em.get('dmode');
const hideTlb = () => em.stopDefault(defComOptions);
const altMode = includes(modes, mode);
targets.forEach(trg => trg.trigger('disable', { fromMove: true }));
targets.forEach((trg) => trg.trigger('disable', { fromMove: true }));
// Without setTimeout the ghost image disappears
nativeDrag ? setTimeout(hideTlb, 0) : hideTlb();
@ -186,14 +186,14 @@ export default class CommandsModule extends Module<CommandsConfig & { pStylePref
cmdMove.initSorterFromModels(targets);
}
targets.filter(sel => sel.get('selectable')).forEach(sel => sel.set('status', 'freezed-selected'));
targets.filter((sel) => sel.get('selectable')).forEach((sel) => sel.set('status', 'freezed-selected'));
},
};
// Core commands
defaultCommands['core:undo'] = e => e.UndoManager.undo();
defaultCommands['core:redo'] = e => e.UndoManager.redo();
commandsDef.forEach(item => {
defaultCommands['core:undo'] = (e) => e.UndoManager.undo();
defaultCommands['core:redo'] = (e) => e.UndoManager.redo();
commandsDef.forEach((item) => {
const oldCmd = item[2];
const cmd = require(`./view/${item[1]}`).default;
const cmdName = `core:${item[0]}`;
@ -201,7 +201,7 @@ export default class CommandsModule extends Module<CommandsConfig & { pStylePref
if (oldCmd) {
defaultCommands[oldCmd] = cmd;
// Propogate old commands (can be removed once we stop to call old commands)
['run', 'stop'].forEach(name => {
['run', 'stop'].forEach((name) => {
em.on(`${name}:${oldCmd}`, (...args) => em.trigger(`${name}:${cmdName}`, ...args));
});
}
@ -293,7 +293,7 @@ export default class CommandsModule extends Module<CommandsConfig & { pStylePref
};
this.add(id, cmdObj);
// Extend also old name commands if exist
const oldCmd = commandsDef.filter(cmd => `core:${cmd[0]}` === id && cmd[2])[0];
const oldCmd = commandsDef.filter((cmd) => `core:${cmd[0]}` === id && cmd[2])[0];
oldCmd && this.add(oldCmd[2], cmdObj);
}

2
src/commands/view/CanvasMove.ts

@ -70,7 +70,7 @@ export default {
const canvas = this.getCanvas();
const classes = [`${ppfx}is__grab`];
!enable && classes.push(`${ppfx}is__grabbing`);
classes.forEach(cls => canvas.classList[methodCls](cls));
classes.forEach((cls) => canvas.classList[methodCls](cls));
methodsEv[methodEv](document, 'keyup', this.onKeyUp);
methodsEv[methodEv](canvas, 'mousedown', this.enableDragger);
methodsEv[methodEv](document, 'mouseup', this.disableDragger);

2
src/commands/view/ComponentDelete.ts

@ -8,7 +8,7 @@ const command: CommandObject<{ component?: Component }> = {
let components = opts.component || ed.getSelectedAll();
components = isArray(components) ? [...components] : [components];
components.filter(Boolean).forEach(component => {
components.filter(Boolean).forEach((component) => {
if (!component.get('removable')) {
return this.em.logWarning('The element is not removable', {
component,

30
src/commands/view/ComponentDrag.ts

@ -27,7 +27,7 @@ export default {
'getPosition',
'getGuidesStatic',
'renderGuide',
'getGuidesTarget'
'getGuidesTarget',
);
const { target, event, mode, dragger = {} } = opts;
const el = target.getEl();
@ -119,7 +119,7 @@ export default {
debounce(() => {
this.updateGuides();
opts.debug && this.guides?.forEach((item: any) => this.renderGuide(item));
}, 200)
}, 200),
);
}
@ -130,7 +130,7 @@ export default {
let result: any = [];
const el = this.target.getEl();
const { parentNode = {} } = el;
each(parentNode.children, item => (result = result.concat(el !== item ? this.getElementGuides(item) : [])));
each(parentNode.children, (item) => (result = result.concat(el !== item ? this.getElementGuides(item) : [])));
return result.concat(this.getElementGuides(parentNode));
},
@ -225,20 +225,20 @@ export default {
{ type: 'r', x: left + width }, // Right
{ type: 'x', x: left + width / 2 }, // Mid x
{ type: 'y', y: top + height / 2 }, // Mid y
].map(item => ({
].map((item) => ({
...item,
origin: el,
originRect,
guide: opts.debug && this.renderGuide(item),
}));
guides.forEach(item => this.guides?.push(item));
guides.forEach((item) => this.guides?.push(item));
return guides;
},
getTranslate(transform: string, axis = 'x') {
let result = 0;
(transform || '').split(' ').forEach(item => {
(transform || '').split(' ').forEach((item) => {
const itemStr = item.trim();
const fn = `translate${axis.toUpperCase()}(`;
if (itemStr.indexOf(fn) === 0) result = parseFloat(itemStr.replace(fn, ''));
@ -251,7 +251,7 @@ export default {
const val = `${fn}${value})`;
let result = (transform || '')
.split(' ')
.map(item => {
.map((item) => {
const itemStr = item.trim();
if (itemStr.indexOf(fn) === 0) item = val;
return item;
@ -296,7 +296,7 @@ export default {
} else {
const adds: any = { position, width, height };
const style: any = { left, top, __p };
keys(adds).forEach(add => {
keys(adds).forEach((add) => {
const prop = adds[add];
if (prop) style[add] = prop;
});
@ -378,7 +378,7 @@ export default {
},
hideGuidesInfo() {
['X', 'Y'].forEach(item => {
['X', 'Y'].forEach((item) => {
const guide = this[`elGuideInfo${item}`];
if (guide) guide.style.display = 'none';
});
@ -390,7 +390,7 @@ export default {
renderGuideInfo(guides: Guide[] = []) {
const { guidesStatic } = this;
this.hideGuidesInfo();
guides.forEach(item => {
guides.forEach((item) => {
const { origin, x } = item;
const rectOrigin = this.getElementPos(origin);
const axis = isUndefined(x) ? 'y' : 'x';
@ -405,8 +405,8 @@ export default {
// Find the nearest element
const res = guidesStatic
?.filter(stat => stat.type === item.type)
.map(stat => {
?.filter((stat) => stat.type === item.type)
.map((stat) => {
const { left, width, top, height } = stat.originRect;
const statEdge1 = isY ? left : top;
const statEdge2 = isY ? left + width : top + height;
@ -415,9 +415,9 @@ export default {
guide: stat,
};
})
.filter(item => item.gap > 0)
.filter((item) => item.gap > 0)
.sort((a, b) => a.gap - b.gap)
.map(item => item.guide)[0];
.map((item) => item.guide)[0];
if (res) {
const { left, width, top, height, rect } = res.originRect;
@ -458,7 +458,7 @@ export default {
const classes = [`${ppfx}is__grabbing`];
const { Canvas } = editor;
const body = Canvas.getBody();
classes.forEach(cls => body.classList[methodCls](cls));
classes.forEach((cls) => body.classList[methodCls](cls));
Canvas[enable ? 'startAutoscroll' : 'stopAutoscroll']();
},
} as CommandObject<

2
src/commands/view/ComponentEnter.ts

@ -6,7 +6,7 @@ export default {
if (!ed.Canvas.hasFocus()) return;
const toSelect: Component[] = [];
ed.getSelectedAll().forEach(component => {
ed.getSelectedAll().forEach((component) => {
const coll = component.components();
const next = coll && coll.filter((c: any) => c.get('selectable'))[0];
next && toSelect.push(next);

2
src/commands/view/ComponentExit.ts

@ -6,7 +6,7 @@ export default {
if (!ed.Canvas.hasFocus() && !opts.force) return;
const toSelect: Component[] = [];
ed.getSelectedAll().forEach(component => {
ed.getSelectedAll().forEach((component) => {
let next = component.parent();
// Recurse through the parent() chain until a selectable parent is found

2
src/commands/view/ComponentNext.ts

@ -6,7 +6,7 @@ export default {
if (!ed.Canvas.hasFocus()) return;
const toSelect: Component[] = [];
ed.getSelectedAll().forEach(cmp => {
ed.getSelectedAll().forEach((cmp) => {
const parent = cmp.parent();
if (!parent) return;

2
src/commands/view/ComponentPrev.ts

@ -6,7 +6,7 @@ export default {
if (!ed.Canvas.hasFocus()) return;
const toSelect: Component[] = [];
ed.getSelectedAll().forEach(cmp => {
ed.getSelectedAll().forEach((cmp) => {
const parent = cmp.parent();
if (!parent) return;

4
src/commands/view/ComponentStyleClear.ts

@ -12,13 +12,13 @@ export default {
// Find all components in the project, of the target component type
const type = target.get('type');
const wrappers = ed.Pages.getAllWrappers();
const len = flatten(wrappers.map(wrp => wrp.findType(type))).length;
const len = flatten(wrappers.map((wrp) => wrp.findType(type))).length;
// Remove component related styles only if there are no more components
// of that type in the project
if (!len) {
const rules = ed.CssComposer.getAll();
toRemove = rules.filter(rule => rule.get('group') === `cmp:${type}`);
toRemove = rules.filter((rule) => rule.get('group') === `cmp:${type}`);
rules.remove(toRemove);
}

2
src/commands/view/CopyComponent.ts

@ -3,7 +3,7 @@ import { CommandObject } from './CommandAbstract';
export default {
run(ed) {
const em = ed.getModel();
const models = [...ed.getSelectedAll()].map(md => md.delegate?.copy?.(md) || md).filter(Boolean);
const models = [...ed.getSelectedAll()].map((md) => md.delegate?.copy?.(md) || md).filter(Boolean);
models.length && em.set('clipboard', models);
},
} as CommandObject;

2
src/commands/view/OpenAssets.ts

@ -52,7 +52,7 @@ export default {
let assets: Asset[] = am.getAll().filter((i: Asset) => i);
if (types && types.length) {
assets = assets.filter(a => types.indexOf(a.get('type')) !== -1);
assets = assets.filter((a) => types.indexOf(a.get('type')) !== -1);
}
am.render(assets);

12
src/commands/view/PasteComponent.ts

@ -10,7 +10,7 @@ export default {
const lastSelected = ed.getSelected();
if (clp?.length && lastSelected) {
ed.getSelectedAll().forEach(sel => {
ed.getSelectedAll().forEach((sel) => {
const selected = sel.delegate?.copy?.(sel) || sel;
const { collection } = selected;
let added;
@ -33,7 +33,7 @@ export default {
}
added = isArray(added) ? added : [added];
added.forEach(add => ed.trigger('component:paste', add));
added.forEach((add) => ed.trigger('component:paste', add));
});
lastSelected.emitUpdate();
@ -42,10 +42,10 @@ export default {
} as CommandObject;
function doAdd(ed: Editor, clp: Component[], parent: Component, addOpts: any): Component[] | Component {
const copyable = clp.filter(cop => cop.get('copyable'));
const pasteable = copyable.filter(cop => ed.Components.canMove(parent, cop).result);
const copyable = clp.filter((cop) => cop.get('copyable'));
const pasteable = copyable.filter((cop) => ed.Components.canMove(parent, cop).result);
return parent.components().add(
pasteable.map(cop => cop.clone()),
addOpts
pasteable.map((cop) => cop.clone()),
addOpts,
);
}

2
src/commands/view/Preview.ts

@ -26,7 +26,7 @@ export default {
const tlb = canvas.getToolbarEl();
tlb && (tlb.style.display = on ? 'none' : '');
const elP = body.querySelectorAll(`.${this.ppfx}no-pointer`);
each(elP, item => ((item as HTMLElement).style.pointerEvents = on ? 'all' : ''));
each(elP, (item) => ((item as HTMLElement).style.pointerEvents = on ? 'all' : ''));
em[mthEv]('run:tlb-move:before', this.preventDrag);
}
},

8
src/commands/view/SelectComponent.ts

@ -43,7 +43,7 @@ export default {
'onFrameScroll',
'onFrameResize',
'onFrameUpdated',
'onContainerChange'
'onContainerChange',
);
},
@ -98,7 +98,7 @@ export default {
em[method]('frame:updated', this.onFrameUpdated, this);
em[method]('canvas:updateTools', this.onFrameUpdated, this);
em[method](em.Canvas.events.refresh, this.updateAttached, this);
em.Canvas.getFrames().forEach(frame => {
em.Canvas.getFrames().forEach((frame) => {
const { view } = frame;
const win = view?.getWindow();
win && trigger(win, view?.getBody()!);
@ -141,7 +141,7 @@ export default {
let result = {};
if (component) {
component.views?.forEach(view => {
component.views?.forEach((view) => {
const el = view.el;
const pos = this.getElementPos(el);
result = { el, pos, component, view: getViewEl(el) };
@ -156,7 +156,7 @@ export default {
this.currentDoc = null;
this.elHovered = 0;
this.updateToolsLocal();
this.canvas.getFrames().forEach(frame => {
this.canvas.getFrames().forEach((frame) => {
const { view } = frame;
const el = view && view.getToolsEl();
el && this.toggleToolsEl(0, 0, { el });

4
src/commands/view/SelectPosition.ts

@ -63,8 +63,8 @@ export default {
this.cDim.length === 0
? $(this.outsideElem)
: !this.posIsLastEl && this.cDim[this.posIndex]
? $(this.cDim[this.posIndex][5]).parent()
: $(this.outsideElem);
? $(this.cDim[this.posIndex][5]).parent()
: $(this.outsideElem);
this.posTargetModel = this.posTargetEl.data('model');
this.posTargetCollection = this.posTargetEl.data('model-comp');
}

2
src/common/index.ts

@ -61,7 +61,7 @@ export type ElementRect = {
export type CombinedModelConstructorOptions<
E,
M extends Model<any, any, E> = Model
M extends Model<any, any, E> = Model,
> = Backbone.ModelConstructorOptions<M> & E;
export interface ViewOptions<TModel extends Model | undefined = Model, TElement extends Element = HTMLElement>

12
src/css_composer/index.ts

@ -195,7 +195,7 @@ export default class CssComposer extends ItemManagerModule<CssComposerConfig & {
selectors: any,
state?: string,
width?: string,
ruleProps?: Omit<CssRuleProperties, 'selectors'>
ruleProps?: Omit<CssRuleProperties, 'selectors'>,
): CssRule | undefined {
let slc = selectors;
if (isString(selectors)) {
@ -204,7 +204,7 @@ export default class CssComposer extends ItemManagerModule<CssComposerConfig & {
const node = this.em.Parser.parserCss.checkNode({ selectors: singleSel } as any)[0];
slc = sm.get(node.selectors as string[]);
}
return this.rules.find(rule => rule.compare(slc, state, width, ruleProps)) || null;
return this.rules.find((rule) => rule.compare(slc, state, width, ruleProps)) || null;
}
getAll() {
@ -375,8 +375,8 @@ export default class CssComposer extends ItemManagerModule<CssComposerConfig & {
const rules = this.getAll();
if (!selector) return [...rules.models];
const optRuleSel = { sort: true };
const sels = isString(selector) ? selector.split(',').map(s => s.trim()) : selector;
const result = rules.filter(r => sels.indexOf(r.getSelectors().getFullString(null, optRuleSel)) >= 0);
const sels = isString(selector) ? selector.split(',').map((s) => s.trim()) : selector;
const result = rules.filter((r) => sels.indexOf(r.getSelectors().getFullString(null, optRuleSel)) >= 0);
return result;
}
@ -497,7 +497,7 @@ export default class CssComposer extends ItemManagerModule<CssComposerConfig & {
mediaText = this.em.getCurrentMedia();
}
const id = cmp.getId();
const rules = this.getAll().filter(r => {
const rules = this.getAll().filter((r) => {
if (!isUndefined(state) && r.get('state') !== state) return false;
if (!isUndefined(mediaText) && r.get('mediaText') !== mediaText) return false;
return r.getSelectorsString() === `#${id}`;
@ -526,7 +526,7 @@ export default class CssComposer extends ItemManagerModule<CssComposerConfig & {
if (!Object.keys(idMap).length) return changed;
const rules = Array.isArray(rule) ? rule : [rule];
rules.forEach(rule => {
rules.forEach((rule) => {
const sel = rule.selectors;
if (sel && sel.length == 1) {

12
src/css_composer/model/CssRule.ts

@ -128,7 +128,7 @@ export default class CssRule extends StyleableModel<CssRuleProperties> {
clone(): CssRule {
const opts = { ...this.opt };
const attr = { ...this.attributes };
attr.selectors = this.get('selectors')!.map(s => s.clone() as Selector);
attr.selectors = this.get('selectors')!.map((s) => s.clone() as Selector);
// @ts-ignore
return new this.constructor(attr, opts);
}
@ -147,7 +147,7 @@ export default class CssRule extends StyleableModel<CssRuleProperties> {
sels = isString(sels) ? [sels] : sels;
if (Array.isArray(sels)) {
const res = sels.filter(i => i).map(i => (sm ? sm.add(i) : i));
const res = sels.filter((i) => i).map((i) => (sm ? sm.add(i) : i));
sels = new Selectors(res);
}
@ -236,11 +236,11 @@ export default class CssRule extends StyleableModel<CssRuleProperties> {
const { em } = this;
const { atRuleType, mediaText } = this.attributes;
const devices = em?.Devices.getDevices() || [];
const deviceDefault = devices.filter(d => d.getWidthMedia() === '')[0];
const deviceDefault = devices.filter((d) => d.getWidthMedia() === '')[0];
if (atRuleType !== 'media' || !mediaText) {
return deviceDefault || null;
}
return devices.filter(d => d.getWidthMedia() === getMediaLength(mediaText))[0] || null;
return devices.filter((d) => d.getWidthMedia() === getMediaLength(mediaText))[0] || null;
}
/**
@ -254,7 +254,7 @@ export default class CssRule extends StyleableModel<CssRuleProperties> {
const { em } = this;
const stateValue = this.get('state');
const states = em?.Selectors.getStates() || [];
return states.filter(s => s.getName() === stateValue)[0] || null;
return states.filter((s) => s.getName() === stateValue)[0] || null;
}
/**
@ -339,7 +339,7 @@ export default class CssRule extends StyleableModel<CssRuleProperties> {
if (wd && !atRule) atRule = 'media';
const a1: string[] = sel.map((model: any) => model.getFullName());
const a2: string[] = this.get('selectors')?.map(model => model.getFullName())!;
const a2: string[] = this.get('selectors')?.map((model) => model.getFullName())!;
// Check selectors
const a1S = a1.slice().sort();

6
src/css_composer/view/CssRulesView.ts

@ -139,10 +139,10 @@ export default class CssRulesView extends View {
// Create devices related DOM structure, ensure also to have a default container
const prs = em.Devices.getAll().pluck('priority').sort(this.sortRules) as number[];
prs.every(pr => pr) && prs.unshift(0);
prs.forEach(pr => frag.appendChild(createEl('div', { id: getBlockId(cls, pr) })));
prs.every((pr) => pr) && prs.unshift(0);
prs.forEach((pr) => frag.appendChild(createEl('div', { id: getBlockId(cls, pr) })));
collection.each(model => this.addToCollection(model, frag));
collection.each((model) => this.addToCollection(model, frag));
$el.append(frag);
$el.attr('class', cls);
return this;

4
src/device_manager/index.ts

@ -76,7 +76,7 @@ export default class DeviceManager extends ItemManagerModule<
constructor(em: EditorModel) {
super(em, 'DeviceManager', new Devices(), deviceEvents, defaults);
this.devices = this.all;
this.config.devices?.forEach(device => this.add(device, { silent: true }));
this.config.devices?.forEach((device) => this.add(device, { silent: true }));
this.select(this.config.default || this.devices.at(0));
em.on(chnSel, this._onSelect, this);
return this;
@ -146,7 +146,7 @@ export default class DeviceManager extends ItemManagerModule<
*/
get(id: string): Device | undefined {
// Support old API
const byName = this.getAll().filter(d => d.get('name') === id)[0];
const byName = this.getAll().filter((d) => d.get('name') === id)[0];
return byName || this.devices.get(id) || null;
}

2
src/device_manager/model/Device.ts

@ -54,7 +54,7 @@ export default class Device extends Model<DeviceProperties> {
this.get('width') === null && this.set('width', this.get('widthMedia'));
!this.get('priority') && this.set('priority', parseFloat(this.get('widthMedia')!) || 0);
const toCheck: (keyof DeviceProperties)[] = ['width', 'height', 'widthMedia'];
toCheck.forEach(prop => this.checkUnit(prop));
toCheck.forEach((prop) => this.checkUnit(prop));
}
checkUnit(prop: keyof DeviceProperties) {

2
src/device_manager/view/DevicesView.ts

@ -86,7 +86,7 @@ export default class DevicesView extends View {
const { collection, em } = this;
let result = '';
collection.forEach(device => {
collection.forEach((device) => {
const { name, id } = device.attributes;
const label = (em && em.t && em.t(`deviceManager.devices.${id}`)) || name;
result += `<option value="${id || name}">${label}</option>`;

14
src/dom_components/index.ts

@ -495,7 +495,7 @@ export default class ComponentManager extends ItemManagerModule<DomComponentsCon
clear(opts = {}) {
const components = this.getComponents();
//@ts-ignore
components?.filter(Boolean).forEach(i => i.remove(opts));
components?.filter(Boolean).forEach((i) => i.remove(opts));
return this;
}
@ -556,7 +556,7 @@ export default class ComponentManager extends ItemManagerModule<DomComponentsCon
{
typeExtends,
isComponent: compType && !extendType && !isComponent ? modelToExt.isComponent : isComponent || (() => 0),
}
},
);
// Reassign the defaults getter to the model
Object.defineProperty(methods.model!.prototype, 'defaults', {
@ -646,7 +646,7 @@ export default class ComponentManager extends ItemManagerModule<DomComponentsCon
component.set({
status: 'selected',
});
['component:selected', 'component:toggled'].forEach(event => this.em.trigger(event, component, opts));
['component:selected', 'component:toggled'].forEach((event) => this.em.trigger(event, component, opts));
}
}
@ -657,7 +657,7 @@ export default class ComponentManager extends ItemManagerModule<DomComponentsCon
status: '',
state: '',
});
['component:deselected', 'component:toggled'].forEach(event => this.em.trigger(event, component, opts));
['component:deselected', 'component:toggled'].forEach((event) => this.em.trigger(event, component, opts));
}
}
@ -695,7 +695,7 @@ export default class ComponentManager extends ItemManagerModule<DomComponentsCon
const events = [keyUpdate, keyUpdateInside].join(' ');
shallow.on(
events,
debounce(() => shallow?.components(''), 100)
debounce(() => shallow?.components(''), 100),
);
}
this.shallow = shallow;
@ -886,8 +886,8 @@ export default class ComponentManager extends ItemManagerModule<DomComponentsCon
destroy() {
const all = this.allById();
Object.keys(all).forEach(id => all[id] && all[id].remove());
Object.keys(all).forEach((id) => all[id] && all[id].remove());
this.componentView?.remove();
[this.em, this.componentsById, this.componentView].forEach(i => (i = {}));
[this.em, this.componentsById, this.componentView].forEach((i) => (i = {}));
}
}

82
src/dom_components/model/Component.ts

@ -261,7 +261,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
if (parentAttr && parentAttr.propagate && !propagate) {
const newAttr: Partial<ComponentProperties> = {};
const toPropagate = parentAttr.propagate;
toPropagate.forEach(prop => (newAttr[prop] = parent.get(prop as string)));
toPropagate.forEach((prop) => (newAttr[prop] = parent.get(prop as string)));
newAttr.propagate = toPropagate;
this.set({ ...newAttr, ...props });
}
@ -297,7 +297,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
this.views = [];
// Register global updates for collection properties
['classes', 'traits', 'components'].forEach(name => {
['classes', 'traits', 'components'].forEach((name) => {
const events = `add remove ${name !== 'components' ? 'change' : ''}`;
this.listenTo(this.get(name), events.trim(), (...args) => this.emitUpdate(name, ...args));
});
@ -326,7 +326,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
um.add(this.getSelectors());
this.__hasUm = true;
}
opts.recursive && comps.map(c => c.__postAdd(opts));
opts.recursive && comps.map((c) => c.__postAdd(opts));
}
__postRemove() {
@ -341,8 +341,8 @@ export default class Component extends StyleableModel<ComponentProperties> {
__onChange(m: any, opts: any) {
const changed = this.changedAttributes() || {};
keys(changed).forEach(prop => this.emitUpdate(prop));
['status', 'open', 'toolbar', 'traits'].forEach(name => delete changed[name]);
keys(changed).forEach((prop) => this.emitUpdate(prop));
['status', 'open', 'toolbar', 'traits'].forEach((name) => delete changed[name]);
// Propagate component prop changes
if (!isEmptyObj(changed)) {
this.__changesUp(opts);
@ -359,12 +359,12 @@ export default class Component extends StyleableModel<ComponentProperties> {
const pros = { style: newStyles };
em.trigger(event, this, pros);
styleKeys.forEach(key => em.trigger(`${event}:${key}`, this, pros));
styleKeys.forEach((key) => em.trigger(`${event}:${key}`, this, pros));
}
__changesUp(opts: any) {
const { em, frame } = this;
[frame, em].forEach(md => md && md.changesUp(opts));
[frame, em].forEach((md) => md && md.changesUp(opts));
}
__propSelfToParent(props: any) {
@ -477,7 +477,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
find(query: string) {
const result: Component[] = [];
const $els = this.view?.$el.find(query);
$els?.each(i => {
$els?.each((i) => {
const $el = $els.eq(i);
const model = $el.data('model');
model && result.push(model);
@ -499,7 +499,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
findType(type: string) {
const result: Component[] = [];
const find = (components: Components) =>
components.forEach(item => {
components.forEach((item) => {
item.is(type) && result.push(item);
find(item.components());
});
@ -552,7 +552,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
if (!component) return result;
const contains = (components: Components) => {
!result &&
components.forEach(item => {
components.forEach((item) => {
if (item === component) result = !0;
!result && contains(item.components());
});
@ -604,7 +604,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
const attrPrev = { ...this.previous('attributes') };
const diff = shallowDiff(attrPrev, this.get('attributes')!);
keys(diff).forEach(pr => {
keys(diff).forEach((pr) => {
const attrKey = `attributes:${pr}`;
this.trigger(`change:${attrKey}`, this, diff[pr], opts);
this.em?.trigger(`${keyUpdate}:${attrKey}`, this, diff[pr], opts);
@ -638,7 +638,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
...this.getAttributes({ noClass: true }),
...attrs,
},
opts
opts,
);
}
@ -654,7 +654,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
removeAttributes(attrs: string | string[] = [], opts: SetOptions = {}) {
const attrArr = Array.isArray(attrs) ? attrs : [attrs];
const compAttr = this.getAttributes();
attrArr.map(i => delete compAttr[i]);
attrArr.map((i) => delete compAttr[i]);
return this.setAttributes(compAttr, opts);
}
@ -705,7 +705,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
this.rule = cc.setIdRule(this.getId(), prop, { state, ...opts });
const diff = shallowDiff(propOrig, prop);
this.set('style', '', { silent: true });
keys(diff).forEach(pr => this.trigger(`change:style:${pr}`));
keys(diff).forEach((pr) => this.trigger(`change:style:${pr}`));
} else {
prop = super.setStyle.apply(this, arguments as any);
}
@ -732,7 +732,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
if (opts.noClass) {
delete attributes.class;
} else {
this.classes.forEach(cls => classes.push(isString(cls) ? cls : cls.getName()));
this.classes.forEach((cls) => classes.push(isString(cls) ? cls : cls.getName()));
classes.length && (attributes.class = classes.join(' '));
}
@ -818,9 +818,9 @@ export default class Component extends StyleableModel<ComponentProperties> {
const selectors = this.classes;
const type = Selector.TYPE_CLASS;
classes.forEach(classe => {
classes.forEach((classe) => {
const classes = classe.split(' ');
classes.forEach(name => {
classes.forEach((name) => {
const selector = selectors.where({ name, type })[0];
selector && removed.push(selectors.remove(selector));
});
@ -883,7 +883,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
this.__loadTraits();
const attrs = { ...this.get('attributes') };
const traits = this.traits;
traits.each(trait => {
traits.each((trait) => {
if (!trait.changeProp) {
const name = trait.getName();
const value = trait.getInitValue();
@ -903,8 +903,8 @@ export default class Component extends StyleableModel<ComponentProperties> {
this.off(...toListen);
const prevProps = this.previous(prop) || [];
const newProps = this.get(prop) || [];
const prevPropsEv = prevProps.map(e => `change:${e}`).join(' ');
const newPropsEv = newProps.map(e => `change:${e}`).join(' ');
const prevPropsEv = prevProps.map((e) => `change:${e}`).join(' ');
const newPropsEv = newProps.map((e) => `change:${e}`).join(' ');
prevPropsEv && this.off(prevPropsEv, this.__scriptPropsChange);
newPropsEv && this.on(newPropsEv, this.__scriptPropsChange);
// @ts-ignore
@ -934,7 +934,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
*/
append(components: ComponentAdd, opts: AddOptions = {}): Component[] {
const compArr = isArray(components) ? [...components] : [components];
const toAppend = compArr.map(comp => {
const toAppend = compArr.map((comp) => {
if (isString(comp)) {
return comp;
} else {
@ -966,7 +966,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
*/
components<T extends ComponentAdd | undefined>(
components?: T,
opts: AddComponentsOption = {}
opts: AddComponentsOption = {},
): undefined extends T ? Components : Component[] {
const coll = this.get('components')!;
@ -1086,7 +1086,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
traits.setTarget(this);
if (traitsI.length) {
traitsI.forEach(tr => tr.attributes && delete tr.attributes.value);
traitsI.forEach((tr) => tr.attributes && delete tr.attributes.value);
traits.add(traitsI);
}
@ -1135,7 +1135,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
*/
getTrait(id: string) {
return (
this.getTraits().filter(trait => {
this.getTraits().filter((trait) => {
return trait.get('id') === id || trait.get('name') === id;
})[0] || null
);
@ -1183,7 +1183,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
*/
removeTrait(id: string | string[]) {
const ids = isArray(id) ? id : [id];
const toRemove = ids.map(id => this.getTrait(id));
const toRemove = ids.map((id) => this.getTrait(id));
const { traits } = this;
const removed = toRemove.length ? traits.remove(toRemove) : [];
this.em?.trigger('component:toggled');
@ -1223,7 +1223,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
if (!clm) return [];
// @ts-ignore
if (arr.models) return [...arr.models];
arr.forEach(val => res.push(clm.add(val) as Selector));
arr.forEach((val) => res.push(clm.add(val) as Selector));
return res;
}
@ -1272,7 +1272,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
// Clone component specific rules
const newId = `#${cloned.getId()}`;
const rulesToClone = cssc ? cssc.getRules(`#${id}`) : [];
rulesToClone.forEach(rule => {
rulesToClone.forEach((rule) => {
const newRule = rule.clone();
// @ts-ignore
newRule.set('selectors', [newId]);
@ -1303,11 +1303,11 @@ export default class Component extends StyleableModel<ComponentProperties> {
// Inverted, cloned is the instance, the origin is the main symbol
this.set(keySymbols, [cloned]);
cloned.set(keySymbol, this);
[this, cloned].map(i => initSymbol(i));
[this, cloned].map((i) => initSymbol(i));
} else {
// Cloned becomes the main symbol
cloned.set(keySymbols, [this]);
[this, cloned].map(i => initSymbol(i));
[this, cloned].map((i) => initSymbol(i));
this.set(keySymbol, cloned);
}
}
@ -1464,7 +1464,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
__innerHTML(opts: ToHTMLOptions = {}) {
const cmps = this.components();
return !cmps.length ? this.content : cmps.map(c => c.toHTML(opts)).join('');
return !cmps.length ? this.content : cmps.map((c) => c.toHTML(opts)).join('');
}
/**
@ -1503,7 +1503,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
const symbol = obj[keySymbol];
const symbols = obj[keySymbols];
if (symbols && isArray(symbols)) {
obj[keySymbols] = symbols.filter(i => i).map(i => (i.getId ? i.getId() : i));
obj[keySymbols] = symbols.filter((i) => i).map((i) => (i.getId ? i.getId() : i));
}
if (symbol && !isString(symbol)) {
obj[keySymbol] = symbol.getId();
@ -1534,13 +1534,13 @@ export default class Component extends StyleableModel<ComponentProperties> {
delete obj.type;
}
forEach(['attributes', 'style'], prop => {
forEach(['attributes', 'style'], (prop) => {
if (isEmpty(defaults[prop]) && isEmpty(obj[prop])) {
delete obj[prop];
}
});
forEach(['classes', 'components'], prop => {
forEach(['classes', 'components'], (prop) => {
if (!obj[prop] || (isEmpty(defaults[prop]) && !obj[prop].length)) {
delete obj[prop];
}
@ -1592,7 +1592,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
const frm = frame || em?.getCurrentFrameModel();
if (frm) {
view = views.filter(view => view.frameView === frm.view)[0];
view = views.filter((view) => view.frameView === frm.view)[0];
}
return view;
@ -1681,7 +1681,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
onAll(clb: (cmp: Component) => void) {
if (isFunction(clb)) {
clb(this);
this.components().forEach(model => model.onAll(clb));
this.components().forEach((model) => model.onAll(clb));
}
return this;
}
@ -1696,7 +1696,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
*/
forEachChild(clb: (child: Component) => void) {
if (isFunction(clb)) {
this.components().forEach(child => {
this.components().forEach((child) => {
clb(child);
child.forEachChild(clb);
});
@ -1719,7 +1719,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
}
};
const rmOpts = { ...opts };
[this, em].map(i => i.trigger(ComponentsEvents.removeBefore, this, remove, rmOpts));
[this, em].map((i) => i.trigger(ComponentsEvents.removeBefore, this, remove, rmOpts));
!rmOpts.abort && remove();
return this;
}
@ -1889,7 +1889,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
list[nextId] = model;
}
model.components().forEach(i => Component.ensureInList(i));
model.components().forEach((i) => Component.ensureInList(i));
}
static createId(model: Component, opts: any = {}) {
@ -1949,11 +1949,11 @@ export default class Component extends StyleableModel<ComponentProperties> {
components: ComponentDefinitionDefined | ComponentDefinitionDefined[],
styles: CssRuleJSON[] = [],
list: ObjectAny = {},
opts: { keepIds?: string[]; idMap?: PrevToNewIdMap } = {}
opts: { keepIds?: string[]; idMap?: PrevToNewIdMap } = {},
) {
const comps = isArray(components) ? components : [components];
const { keepIds = [], idMap = {} } = opts;
comps.forEach(comp => {
comps.forEach((comp) => {
comp.attributes;
const { attributes = {}, components } = comp;
const { id } = attributes;
@ -1965,7 +1965,7 @@ export default class Component extends StyleableModel<ComponentProperties> {
attributes.id = newId;
// Update passed styles
isArray(styles) &&
styles.forEach(style => {
styles.forEach((style) => {
const { selectors } = style;
selectors.forEach((sel, idx) => {
if (sel === `#${id}`) selectors[idx] = `#${newId}`;

Some files were not shown because too many files changed in this diff

Loading…
Cancel
Save