Browse Source

Merge branch 'master' into issue-2207

pull/2375/head
Emil Sedgh 7 years ago
parent
commit
7cdd45fd90
  1. 38
      .github/lock.yml
  2. 1
      .gitignore
  3. 5
      dist/css/grapes.min.css
  4. BIN
      dist/fonts/FontAwesome.otf
  5. BIN
      dist/fonts/fontawesome-webfont.eot
  6. 2671
      dist/fonts/fontawesome-webfont.svg
  7. BIN
      dist/fonts/fontawesome-webfont.ttf
  8. BIN
      dist/fonts/fontawesome-webfont.woff
  9. BIN
      dist/fonts/fontawesome-webfont.woff2
  10. 1274
      dist/grapes.js
  11. 8
      dist/grapes.min.js
  12. 2
      dist/grapes.min.js.map
  13. 2
      docs/.vuepress/config.js
  14. 5
      docs/.vuepress/override.styl
  15. 68
      docs/.vuepress/public/component-type-stack.svg
  16. BIN
      docs/.vuepress/public/margin-strings.jpg
  17. BIN
      docs/.vuepress/public/sm-empty-state.jpg
  18. 1
      docs/api.js
  19. 1
      docs/api/component.md
  20. 13
      docs/api/device_manager.md
  21. 49
      docs/api/editor.md
  22. 157
      docs/api/i18n.md
  23. 2
      docs/api/rich_text_editor.md
  24. 5
      docs/api/style_manager.md
  25. 2
      docs/getting-started.md
  26. 738
      docs/modules/Components-new.md
  27. 876
      docs/modules/Components.md
  28. 236
      docs/modules/I18n.md
  29. 2
      docs/modules/Plugins.md
  30. 3143
      package-lock.json
  31. 37
      package.json
  32. 28
      scripts/build-locale.js
  33. 12
      src/asset_manager/config/config.js
  34. 10
      src/asset_manager/view/AssetsView.js
  35. 10
      src/asset_manager/view/FileUploader.js
  36. 3
      src/block_manager/view/BlockView.js
  37. 15
      src/block_manager/view/CategoryView.js
  38. 2
      src/canvas/view/CanvasView.js
  39. 2
      src/commands/view/ComponentDrag.js
  40. 2
      src/commands/view/OpenAssets.js
  41. 2
      src/commands/view/OpenStyleManager.js
  42. 7
      src/commands/view/OpenTraitManager.js
  43. 3
      src/commands/view/SelectComponent.js
  44. 4
      src/device_manager/config/config.js
  45. 29
      src/device_manager/index.js
  46. 24
      src/device_manager/view/DevicesView.js
  47. 85
      src/dom_components/model/Component.js
  48. 4
      src/dom_components/model/ComponentVideo.js
  49. 28
      src/dom_components/model/Components.js
  50. 4
      src/dom_components/view/ComponentLabelView.js
  51. 32
      src/dom_components/view/ComponentTextView.js
  52. 14
      src/dom_components/view/ComponentView.js
  53. 13
      src/dom_components/view/ToolbarButtonView.js
  54. 4
      src/dom_components/view/ToolbarView.js
  55. 3
      src/domain_abstract/model/Styleable.js
  56. 14
      src/editor/config/config.js
  57. 25
      src/editor/index.js
  58. 39
      src/editor/model/Editor.js
  59. 20
      src/editor/view/EditorView.js
  60. 20
      src/i18n/config.js
  61. 247
      src/i18n/index.js
  62. 117
      src/i18n/locale/en.js
  63. 103
      src/i18n/locale/it.js
  64. 141
      src/i18n/locale/tr.js
  65. 5
      src/keymaps/messages.js
  66. 23
      src/navigator/view/ItemView.js
  67. 16
      src/panels/config/config.js
  68. 7
      src/panels/view/ButtonView.js
  69. 14
      src/selector_manager/config/config.js
  70. 6
      src/selector_manager/view/ClassTagView.js
  71. 40
      src/selector_manager/view/ClassTagsView.js
  72. 3
      src/style_manager/config/config.js
  73. 4
      src/style_manager/model/PropertyInteger.js
  74. 5
      src/style_manager/view/LayerView.js
  75. 7
      src/style_manager/view/PropertyFileView.js
  76. 10
      src/style_manager/view/PropertyView.js
  77. 17
      src/style_manager/view/SectorView.js
  78. BIN
      src/styles/fonts/FontAwesome.otf
  79. BIN
      src/styles/fonts/fontawesome-webfont.eot
  80. 2671
      src/styles/fonts/fontawesome-webfont.svg
  81. BIN
      src/styles/fonts/fontawesome-webfont.ttf
  82. BIN
      src/styles/fonts/fontawesome-webfont.woff
  83. BIN
      src/styles/fonts/fontawesome-webfont.woff2
  84. 1
      src/styles/scss/main.scss
  85. 16
      src/trait_manager/config/config.js
  86. 11
      src/trait_manager/model/Trait.js
  87. 12
      src/trait_manager/model/TraitFactory.js
  88. 10
      src/trait_manager/view/TraitSelectView.js
  89. 12
      src/trait_manager/view/TraitView.js
  90. 2
      src/utils/Droppable.js
  91. 32
      src/utils/mixins.js
  92. 28
      test/main.test.js
  93. 9
      test/specs/asset_manager/index.js
  94. 52
      test/specs/asset_manager/model/Asset.js
  95. 34
      test/specs/asset_manager/model/AssetImage.js
  96. 34
      test/specs/asset_manager/model/Assets.js
  97. 112
      test/specs/asset_manager/view/AssetImageView.js
  98. 58
      test/specs/asset_manager/view/AssetView.js
  99. 136
      test/specs/asset_manager/view/AssetsView.js
  100. 142
      test/specs/asset_manager/view/FileUploader.js

38
.github/lock.yml

@ -0,0 +1,38 @@
# Configuration for Lock Threads - https://github.com/dessant/lock-threads
# Number of days of inactivity before a closed issue or pull request is locked
daysUntilLock: 365
# Skip issues and pull requests created before a given timestamp. Timestamp must
# follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable
skipCreatedBefore: false
# Issues and pull requests with these labels will be ignored. Set to `[]` to disable
exemptLabels: []
# Label to add before locking, such as `outdated`. Set to `false` to disable
lockLabel: outdated
# Comment to post before locking. Set to `false` to disable
lockComment: >
This thread has been automatically locked since there has not been
any recent activity after it was closed. Please open a new issue for
related bugs.
# Assign `resolved` as the reason for locking. Set to `false` to disable
setLockReason: true
# Limit to only `issues` or `pulls`
# only: issues
# Optionally, specify configuration settings just for `issues` or `pulls`
# issues:
# exemptLabels:
# - help-wanted
# lockLabel: outdated
# pulls:
# daysUntilLock: 30
# Repository to extend settings from
# _extends: repo

1
.gitignore

@ -14,6 +14,7 @@ images/
private/
vendor/
coverage/
/locale/
node_modules/
bower_components/
grapesjs-*.tgz

5
dist/css/grapes.min.css

File diff suppressed because one or more lines are too long

BIN
dist/fonts/FontAwesome.otf

Binary file not shown.

BIN
dist/fonts/fontawesome-webfont.eot

Binary file not shown.

2671
dist/fonts/fontawesome-webfont.svg

File diff suppressed because it is too large

Before

Width:  |  Height:  |  Size: 434 KiB

BIN
dist/fonts/fontawesome-webfont.ttf

Binary file not shown.

BIN
dist/fonts/fontawesome-webfont.woff

Binary file not shown.

BIN
dist/fonts/fontawesome-webfont.woff2

Binary file not shown.

1274
dist/grapes.js

File diff suppressed because it is too large

8
dist/grapes.min.js

File diff suppressed because one or more lines are too long

2
dist/grapes.min.js.map

File diff suppressed because one or more lines are too long

2
docs/.vuepress/config.js

@ -58,6 +58,7 @@ module.exports = {
'/api/': [
'',
['/api/editor', 'Editor'],
['/api/i18n', 'I18n'],
['/api/canvas', 'Canvas'],
['/api/assets', 'Asset Manager'],
['/api/block_manager', 'Block Manager'],
@ -88,6 +89,7 @@ module.exports = {
['/modules/Commands', 'Commands'],
['/modules/Components', 'Components'],
['/modules/Components-js', 'Components & JS'],
['/modules/I18n', 'I18n'],
['/modules/Traits', 'Traits'],
['/modules/Style-manager', 'Style Manager'],
['/modules/Storage', 'Storage Manager'],

5
docs/.vuepress/override.styl

@ -4,6 +4,11 @@ $navBarColor = white
$scrollBarSize = 8px
$pageWidth = 900px
.img-ctr {
margin: 0 auto;
display: block;
}
.navbar {
background-color: rgb(111, 41, 67);
background-image: linear-gradient(120deg, rgb(217, 131, 166), rgb(77, 17, 79));

68
docs/.vuepress/public/component-type-stack.svg

@ -0,0 +1,68 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg width="572px" height="324px" viewBox="0 0 572 324" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
<!-- Generator: Sketch 49.3 (51167) - http://www.bohemiancoding.com/sketch -->
<title>Artboard</title>
<desc>Created with Sketch.</desc>
<defs></defs>
<g stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g transform="translate(222.000000, 89.000000)">
<g>
<rect fill="#E67891" x="0" y="0" width="124" height="47" rx="4"></rect>
<text font-family="Avenir-Book, Avenir" font-size="15" font-weight="normal" fill="#FFFFFF">
<tspan x="22.1375" y="15">type: image</tspan>
</text>
</g>
</g>
<g transform="translate(105.000000, 149.500000) rotate(13.000000) translate(-105.000000, -149.500000) translate(43.000000, 126.000000)">
<g>
<rect fill="#E67891" x="0" y="0" width="124" height="47" rx="4"></rect>
<text font-family="Avenir-Book, Avenir" font-size="15" font-weight="normal" fill="#FFFFFF">
<tspan x="11.8625" y="15">type: new-type</tspan>
</text>
</g>
</g>
<g transform="translate(222.000000, 156.000000)">
<g>
<rect fill="#E67891" x="0" y="0" width="124" height="47" rx="4"></rect>
<text font-family="Avenir-Book, Avenir" font-size="15" font-weight="normal" fill="#FFFFFF">
<tspan x="30.2075" y="15">type: text</tspan>
</text>
</g>
</g>
<g transform="translate(222.000000, 221.000000)">
<g>
<rect fill="#E67891" x="0" y="0" width="124" height="47" rx="4"></rect>
<text font-family="Avenir-Book, Avenir" font-size="15" font-weight="normal" fill="#FFFFFF">
<tspan x="19.6475" y="15">type: default</tspan>
</text>
</g>
</g>
<text font-family="Helvetica-Bold, Helvetica" font-size="15" font-weight="bold" fill="#6E6E6E">
<tspan x="201" y="41">Component Type Stack</tspan>
</text>
<path d="M94,113 C94.4283817,89 126.095048,77 189,77" stroke="#999999"></path>
<polygon fill="#999999" points="203 77.5 189 83 189 72"></polygon>
<polygon fill="#999999" transform="translate(375.000000, 283.500000) rotate(-270.000000) translate(-375.000000, -283.500000) " points="382 283.5 368 289 368 278"></polygon>
<text font-family="Helvetica" font-size="12" font-weight="normal" fill="#6E6E6E">
<tspan x="33.8046875" y="214">New Component Type </tspan>
<tspan x="62.140625" y="228">goes on top</tspan>
</text>
<text font-family="Helvetica" font-size="12" font-weight="normal" fill="#6E6E6E">
<tspan x="396" y="231">The </tspan>
<tspan x="420.011719" y="231" font-family="Helvetica-Bold, Helvetica" font-weight="bold">default </tspan>
<tspan x="462.679688" y="231">is the last </tspan>
<tspan x="396" y="245">available type</tspan>
</text>
<text font-family="Helvetica" font-size="12" font-weight="normal" fill="#6E6E6E">
<tspan x="396" y="141">Each parsed element goes </tspan>
<tspan x="396" y="155">through the stack from TOP </tspan>
<tspan x="396" y="169">to BOTTOM</tspan>
</text>
<path d="M375.5,78.5 L375.5,280.621251" stroke="#999999" stroke-linecap="square"></path>
<text font-family="Helvetica" font-size="12" font-weight="normal">
<tspan x="396" y="86" fill="#E67891">&lt;span&gt;</tspan>
<tspan x="436.037109" y="86" fill="#6E6E6E">New element</tspan>
<tspan x="506.068359" y="86" fill="#E67891">&lt;/span&gt;</tspan>
</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

BIN
docs/.vuepress/public/margin-strings.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.9 KiB

BIN
docs/.vuepress/public/sm-empty-state.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.7 KiB

1
docs/api.js

@ -22,6 +22,7 @@ const cmds = [
['keymaps/index.js', 'keymaps.md'],
['undo_manager/index.js', 'undo_manager.md'],
['canvas/index.js', 'canvas.md'],
['i18n/index.js', 'i18n.md'],
].map(entry =>
`${binRoot}documentation build ${srcRoot}/${entry[0]} -o ${docRoot}/api/${entry[1]} -f md --shallow --markdown-toc false`)
.join(' && ');

1
docs/api/component.md

@ -528,6 +528,7 @@ Set new id on the component
### Parameters
- `id` **[String][1]**
- `opts`
Returns **this**

13
docs/api/device_manager.md

@ -28,16 +28,19 @@ Add new device to the collection. URLs are supposed to be unique
### Parameters
- `name` **[string][5]** Device name
- `width` **[string][5]** Width of the device
- `opts` **[Object][6]** Custom options
- `id` **[String][5]** Device id
- `width` **[String][5]** Width of the device
- `opts` **[Object][6]?** Custom options (optional, default `{}`)
### Examples
```javascript
deviceManager.add('Tablet', '900px');
deviceManager.add('Tablet2', '900px', {
deviceManager.add('tablet', '900px');
deviceManager.add('tablet2', '900px', {
height: '300px',
// At first, GrapesJS tries to localize the name by device id.
// In case is not found, the `name` property is used (or `id` if name is missing)
name: 'Tablet 2',
widthMedia: '810px', // the width that will be used for the CSS media
});
```

49
docs/api/editor.md

@ -151,6 +151,7 @@ Returns CSS built inside canvas
### Parameters
- `opts` **[Object][3]** Options (optional, default `{}`)
- `opts.avoidProtected` **[Boolean][4]** Don't include protected CSS (optional, default `false`)
Returns **[string][2]** CSS string
@ -178,7 +179,7 @@ Set components inside editor's canvas. This method overrides actual components
### Parameters
- `components` **([Array][4]&lt;[Object][3]> | [Object][3] \| [string][2])** HTML string or components model
- `components` **([Array][5]&lt;[Object][3]> | [Object][3] \| [string][2])** HTML string or components model
### Examples
@ -200,9 +201,9 @@ Add components
### Parameters
- `components` **([Array][4]&lt;[Object][3]> | [Object][3] \| [string][2])** HTML string or components model
- `components` **([Array][5]&lt;[Object][3]> | [Object][3] \| [string][2])** HTML string or components model
- `opts` **[Object][3]** Options
- `opts.avoidUpdateStyle` **[Boolean][5]** If the HTML string contains styles,
- `opts.avoidUpdateStyle` **[Boolean][4]** If the HTML string contains styles,
by default, they will be created and, if already exist, updated. When this option
is true, styles already created will not be updated. (optional, default `false`)
@ -218,7 +219,7 @@ editor.addComponents({
});
```
Returns **[Array][4]&lt;Component>**
Returns **[Array][5]&lt;Component>**
## getStyle
@ -232,7 +233,7 @@ Set style inside editor's canvas. This method overrides actual style
### Parameters
- `style` **([Array][4]&lt;[Object][3]> | [Object][3] \| [string][2])** CSS string or style model
- `style` **([Array][5]&lt;[Object][3]> | [Object][3] \| [string][2])** CSS string or style model
### Examples
@ -257,7 +258,7 @@ Returns **Model**
Returns an array of all selected components
Returns **[Array][4]**
Returns **[Array][5]**
## getSelectedToStyle
@ -277,7 +278,7 @@ Select a component
- `el` **(Component | [HTMLElement][6])** Component to select
- `opts` **[Object][3]?** Options
- `opts.scroll` **[Boolean][5]?** Scroll canvas to the selected element
- `opts.scroll` **[Boolean][4]?** Scroll canvas to the selected element
### Examples
@ -296,7 +297,7 @@ Add component to selection
### Parameters
- `el` **(Component | [HTMLElement][6] \| [Array][4])** Component to select
- `el` **(Component | [HTMLElement][6] \| [Array][5])** Component to select
### Examples
@ -312,7 +313,7 @@ Remove component from selection
### Parameters
- `el` **(Component | [HTMLElement][6] \| [Array][4])** Component to select
- `el` **(Component | [HTMLElement][6] \| [Array][5])** Component to select
### Examples
@ -328,7 +329,7 @@ Toggle component selection
### Parameters
- `el` **(Component | [HTMLElement][6] \| [Array][4])** Component to select
- `el` **(Component | [HTMLElement][6] \| [Array][5])** Component to select
### Examples
@ -537,6 +538,30 @@ editor.log('Something done!', { ns: 'from-plugin-x', level: 'info' });
Returns **this**
## t
Translate label
### Parameters
- `args` **...any**
- `key` **[String][2]** Label to translate
- `opts` **[Object][3]?** Options for the translation
- `opts.params` **[Object][3]?** Params for the translation
- `opts.noWarn` **[Boolean][4]?** Avoid warnings in case of missing resources
### Examples
```javascript
editor.t('msg');
// use params
editor.t('msg2', { params: { test: 'hello' } });
// custom local
editor.t('msg2', { params: { test: 'hello' }, l: 'it' });
```
Returns **[String][2]**
## on
Attach event
@ -596,9 +621,9 @@ Returns **[HTMLElement][6]**
[3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[4]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[4]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[5]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[5]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[6]: https://developer.mozilla.org/docs/Web/HTML/Element

157
docs/api/i18n.md

@ -0,0 +1,157 @@
<!-- Generated by documentation.js. Update this documentation by updating the source code. -->
## I18n
You can customize the initial state of the module from the editor initialization, by passing the following [Configuration Object][1]
```js
const editor = grapesjs.init({
i18n: {
locale: 'en',
localeFallback: 'en',
messages: {
it: { hello: 'Ciao', ... },
...
}
}
})
```
Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance
```js
const i18n = editor.I18n;
```
### Events
- `i18n:add` - New set of messages is added
- `i18n:update` - The set of messages is updated
- `i18n:locale` - Locale changed
## getConfig
Get module configurations
Returns **[Object][2]** Configuration object
## setLocale
Update current locale
### Parameters
- `locale` **[String][3]** Locale value
### Examples
```javascript
i18n.setLocale('it');
```
Returns **this**
## getLocale
Get current locale
Returns **[String][3]** Current locale value
## getMessages
Get all messages
### Parameters
- `lang` **[String][3]?** Specify the language of messages to return
- `opts` **[Object][2]?** Options (optional, default `{}`)
- `opts.debug` **[Boolean][4]?** Show warnings in case of missing language
### Examples
```javascript
i18n.getMessages();
// -> { en: { hello: '...' }, ... }
i18n.getMessages('en');
// -> { hello: '...' }
```
Returns **[Object][2]**
## setMessages
Set new set of messages
### Parameters
- `msg` **[Object][2]** Set of messages
### Examples
```javascript
i18n.getMessages();
// -> { en: { msg1: 'Msg 1', msg2: 'Msg 2', } }
i18n.setMessages({ en: { msg2: 'Msg 2 up', msg3: 'Msg 3', } });
// Set replaced
i18n.getMessages();
// -> { en: { msg2: 'Msg 2 up', msg3: 'Msg 3', } }
```
Returns **this**
## addMessages
Update messages
### Parameters
- `msg` **[Object][2]** Set of messages to add
### Examples
```javascript
i18n.getMessages();
// -> { en: { msg1: 'Msg 1', msg2: 'Msg 2', } }
i18n.addMessages({ en: { msg2: 'Msg 2 up', msg3: 'Msg 3', } });
// Set updated
i18n.getMessages();
// -> { en: { msg1: 'Msg 1', msg2: 'Msg 2 up', msg3: 'Msg 3', } }
```
Returns **this**
## t
Translate the locale message
### Parameters
- `key` **[String][3]** Label to translate
- `opts` **[Object][2]?** Options for the translation (optional, default `{}`)
- `opts.params` **[Object][2]?** Params for the translation
- `opts.debug` **[Boolean][4]?** Show warnings in case of missing resources
### Examples
```javascript
obj.setMessages({
en: { msg: 'Msg', msg2: 'Msg {test}'},
it: { msg2: 'Msg {test} it'},
});
obj.t('msg');
// -> outputs `Msg`
obj.t('msg2', { params: { test: 'hello' } }); // use params
// -> outputs `Msg hello`
obj.t('msg2', { l: 'it', params: { test: 'hello' } }); // custom local
// -> outputs `Msg hello it`
```
Returns **[String][3]**
[1]: https://github.com/artf/grapesjs/blob/master/src/i18n/config.js
[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[3]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[4]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean

2
docs/api/rich_text_editor.md

@ -9,7 +9,7 @@ You can customize the initial state of the module from the editor initialization
```js
const editor = grapesjs.init({
rte: {
richTextEditor: {
// options
}
})

5
docs/api/style_manager.md

@ -130,6 +130,8 @@ Add property to the sector identified by id
- `property.properties` **[Array][19]&lt;[Object][16]>** Nested properties for composite and stack type (optional, default `[]`)
- `property.layers` **[Array][19]&lt;[Object][16]>** Layers for stack properties (optional, default `[]`)
- `property.list` **[Array][19]&lt;[Object][16]>** List of possible options for radio and select types (optional, default `[]`)
- `opts` (optional, default `{}`)
- `options` **[Object][16]** Options (optional, default `{}`)
### Examples
@ -146,7 +148,8 @@ var property = styleManager.addProperty('mySector',{
value: '200px',
name: '200',
}],
});
}, { at: 0 });
// With `at: 0` we place the new property at the beginning of the collection
```
Returns **(Property | null)** Added Property or `null` in case sector doesn't exist

2
docs/getting-started.md

@ -597,7 +597,7 @@ editor.setDevice('Mobile');
```
::: tip
Check out the [Device Manager API](api/panels.html) to see all the available methods
Check out the [Device Manager API](api/device_manager.html) to see all the available methods
:::
## Store & load data

738
docs/modules/Components-new.md

@ -1,738 +0,0 @@
---
title: Component Manager
---
# Component Manager
The Component is the base element for template composition. It is atomic, so elements like images, text boxes, maps, etc. fit the definition of a Component. The concept of the component was made to allow the developer to bind different behaviors to different elements. Like for example, opening the Asset Manager on double click of the image.
::: warning
This guide is referring to GrapesJS v0.14.67 or higher
:::
[[toc]]
## How Components work?
Let's see in detail how components work by looking at all steps from adding an HTML string to the editor.
This is how we can add new components to the canvas:
```js
// Append components directly to the canvas
editor.addComponents(`<div>
<img src="https://path/image" />
<span title="foo">Hello world!!!</span>
</div>`);
// or into some, already defined, component.
// For instance, appending to a selected component would be:
editor.getSelected().append(`<div>...`);
// Actually, editor.addComponents is an alias of...
editor.getWrapper().append(`<div>...`);
```
::: tip
If you need to append a component in a specific position, you can use `at` option. To add a component on top of all others (in the same collection) you would use
```js
component.append('<div>...', { at: 0 })
```
or in the middle
```js
const { length } = component.components();
component.append('<div>...', { at: parseInt(length / 2, 10) })
```
:::
### Component Definition
In the first step the HTML string is parsed and trasformed to what is called **Component Definition**, so the result of the input would be:
```js
{
tagName: 'div',
components: [
{
type: 'image',
attributes: { src: 'https://path/image' },
}, {
tagName: 'span',
type: 'text',
attributes: { title: 'foo' },
components: [{
type: 'textnode',
content: 'Hello wdsforld!!!'
}]
}
]
}
```
The real **Component Definition** would be a little bit bigger so we reduced the JSON for the sake of simplicity.
You can notice the result is similar to what is generally called a **Virtual DOM**, a lightweight rappresentation of the DOM element. This actually helps the editor to keep track of the state of our elements and make performance-friendly changes/updates.
The meaning of properties like `tagName`, `attributes` and `components` are quite obvious, but what about `type`?! This particular property specifies the actual **Component** of our **Component Definition** (you check the list of default components [below](#built-in-components)) and if it's omitted, the default one will be used `type: 'default'`.
At this point, a good question would be, how the editor assignes 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 said before, when you pass an HTML string as a component to the editor, that string is parsed and compiled to the [Component Definition](#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 is matter 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.
SVG - ComponentTypeStack
::: tip
If you're importing big chunks of HTML code you might want to improve the performances by skipping the parsing and the component recognition steps by passing directly Component Definiton objects or using the JSX syntax. Read more about it here...TODO
:::
### Component instance
Once the **Component Definition** is ready and the type is assigned, the [Component](api/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.
```js
const component = editor.addComponents(`<div>
<img src="https://path/image" />
<span title="foo">Hello world!!!</span>
</div>`)[0];
```
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 behavies 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();
// Update component content
component.components(`<div>Component 1</div><div>Component 2</div>`);
```
Each component can define its own properties and methods but all of them will always extend, at least, the `default` one (then you will see how to create new custom components and how to extend the already defined) so it's good to check the [Component API](api/component) to see all available properties and methods.
The **main purpose of the Component** is to keep track of its data and to return them when necessary. One common thing you might need to ask from the component is to show its current HTML
```js
const componentHTML = component.toHTML();
```
This will return a string containing the HTML of the component and all of its children.
The component implements also `toJSON` methods so you can get its JSON structure in this way
```js
JSON.stringify(component)
```
::: tip
For storing/loading all the components you should rely on the [Storage Manager](modules/storage)
:::
So, the **Component instance** is responable 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 the **View** of the component. It has nothing to do with the **final 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).
So, by default, the view of components is automatically synced with the data of its models (you can't have a View without a Model). If you update the attribute of the component or append a new one as a child, the view will render it in the canvas.
Unfotunatelly, sometimes, you might need some additional logic to handle better the component result. Think about allowing a user build its `<table>` element, for this specific case you might want to add custom buttons in the canvas, so it'd be easier adding/removing columns/rows. To handle those cases you can rely on the View, where you can add additional DOM component, attach events, etc. All of this will be completely unrelated with the final HTML of the `<table>` (the result the user would expect) as it handled by the Model.
Once the component is rendered (when you actually see it in the canvas) you can always access its View and the DOM element.
```js
const component = editor.getSelected();
// Get the View
const view = component.getView();
// Get the DOM element
const el = component.getEl();
```
So, 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/artf/grapesjs/blob/dev/src/dom_components/model/ComponentTableCell.js) - Component for handle `<td>` and `<th>` elements
* [`row`](https://github.com/artf/grapesjs/blob/dev/src/dom_components/model/ComponentTableRow.js) - Component for handle `<tr>` elements
* [`table`](https://github.com/artf/grapesjs/blob/dev/src/dom_components/model/ComponentTable.js) - Component for handle `<table>` elements
* [`thead`](https://github.com/artf/grapesjs/blob/dev/src/dom_components/model/ComponentTableHead.js) - Component for handle `<thead>` elements
* [`tbody`](https://github.com/artf/grapesjs/blob/dev/src/dom_components/model/ComponentTableBody.js) - Component for handle `<tbody>` elements
* [`tfoot`](https://github.com/artf/grapesjs/blob/dev/src/dom_components/model/ComponentTableFoot.js) - Component for handle `<tfoot>` elements
* [`map`](https://github.com/artf/grapesjs/blob/dev/src/dom_components/model/ComponentMap.js) - Component for handle `<a>` elements
* [`link`](https://github.com/artf/grapesjs/blob/dev/src/dom_components/model/ComponentLink.js) - Component for handle `<a>` elements
* [`label`](https://github.com/artf/grapesjs/blob/dev/src/dom_components/model/ComponentLabel.js) - Component for handle properly `<label>` elements
* [`video`](https://github.com/artf/grapesjs/blob/dev/src/dom_components/model/ComponentVideo.js) - Component for videos
* [`image`](https://github.com/artf/grapesjs/blob/dev/src/dom_components/model/ComponentImage.js) - Component for images
* [`script`](https://github.com/artf/grapesjs/blob/dev/src/dom_components/model/ComponentScript.js) - Component for handle `<script>` elements
* [`svg`](https://github.com/artf/grapesjs/blob/dev/src/dom_components/model/ComponentSvg.js) - Component for handle SVG elements
* [`comment`](https://github.com/artf/grapesjs/blob/dev/src/dom_components/model/ComponentComment.js) - Component for comments (might be useful for email editors)
* [`textnode`](https://github.com/artf/grapesjs/blob/dev/src/dom_components/model/ComponentTextNode.js) - Similar to the textnode in DOM definition, so a text element without a tag element.
* [`text`](https://github.com/artf/grapesjs/blob/dev/src/dom_components/model/ComponentText.js) - A simple text component that can be edited inline
* [`wrapper`](https://github.com/artf/grapesjs/blob/dev/src/dom_components/model/ComponentWrapper.js) - The canvas need to contain a root component, a wrapper, this component was made to identify it
* [`default`](https://github.com/artf/grapesjs/blob/dev/src/dom_components/model/Component.js) - Default base component
## Define new Component Type
Now that we know how components work, we can start exploring the process of creating new **Component Types**.
<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 => {
editor.DomComponents.addType(/* API for component type definition */);
};
const editor = grapesjs.init({
container : '#gjs',
// ...
plugins: [ myNewComponentTypes ],
});
```
Let's say we want to make the editor understand and handle better `<input>` elements. This is how we would start defining our new component type
```js
editor.DomComponents.addType('my-input-type', {
// Make the editor understand when to bind `my-input-type`
isComponent: el => el.tagName === 'INPUT',
// Model definition
model: {
defaults: {
tagName: 'input',
draggable: 'form, form *', // Can be dropped only inside `form` elements
droppable: false, // Can't drop other elements inside it
attributes: { // Default attributes
type: 'text',
name: 'default-name',
placeholder: 'Insert text here',
},
traits: [
'name',
'placeholder',
{ type: 'checkbox', name: 'required' },
],
}
}
});
```
With this code the editor will be able to understand simple text `<input>`s, assign default attributes and show some trait for a better attribute handling.
::: tip
To understand better how Traits work you should read its [dedicated page](Traits.html) but we highly sugggest 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 mentioned it already in [this](#component-recognition-and-component-type-stack) section and we need it to make the editor understand `<input>` during the component recognition step.
It receives only the `el` argument, which is the parsed HTMLElement node and expects a truthy value in case the element satisfies your logic condition. So, if we add this HTML string as component
```js
// ...after editor initialization
editor.addComponents(`<input name="my-test" title="hello"/>`)
```
The resultant Component Definition will be
```js
{
type: 'my-input-type',
attributes: {
name: 'my-test',
title: 'hello',
},
}
```
If you need you can also customize the resultant Component Definition by returning an object as the result:
```js
editor.DomComponents.addType('my-input-type', {
isComponent: el => {
if (el.tagName === 'INPUT') {
// You should explicitly declare the type of your resultant
// object, otherwise the `default` one will be used
const result = { type: 'my-input-type' };
if (/* some other condition */) {
result.attributes = { title: 'Hi' };
}
return result;
}
},
// ...
});
```
**Be aware** that this method will probably receive ANY parsed element from your canvas (eg. on load or on add) and not all the nodes have the same interface (eg. properties/methods).
If you do this:
```js
// ...
// Print elements
isComponent: el => {
console.log(el);
return el.tagName === 'INPUT';
},
// ...
editor.addComponents(`<div>
I'm a text node
<!-- I'm a comment node -->
<img alt="Image here"/>
<input/>
</div>`);
```
You will see printing all the nodes, so doing something like this `el.getAttribute('...')` (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:
```js
// isComponent will be executed on some-element
editor.addComponents('<some-element>...</some-element>');
// isComponent WON'T be executed on ANY provided object
// If the object without `type`, the `default` one will be used
editor.addComponents({
type: 'some-component',
});
// isComponent WON'T be executed as we're forcing the type
editor.addComponents('<some-element data-gjs-type="some-component">...');
```
One more tip, if you define a component type without the `isComponent`, the only way for the editor to see that component will be with a declared type (via object like `{ 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](#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.
One another important property (not used because `<input/>` doesn't need it) might be `components`, which defines default internal components
```js
defaults: {
tagName: 'div',
attributes: { title: 'Hello' },
// Can be a string
components: `
<h1>Header test</h1>
<p>Paragraph test</p>
`,
// A component definiton
components: {
tagName: 'h1',
components: 'Header test',
},
// Array of strings/component definitons
components: [
{
tagName: 'h1',
components: 'Header test',
},
'<p>Paragraph test</p>',
],
// Or a function, which get as an argument the current
// model and expects as the return one of the possible
// values described above
components: model => {
return `<h1>Header test: ${model.get('type')}</h1>`;
},
}
```
#### Read and update the model
You can read and update the model properties wherever you have the reference to it. Here some references to the most useful API
```js
// let's use the selected component
const modelComponent = editor.getSelected();
// All model props
const props = modelComponent.props();
// Get single property
const tagName = modelComponent.get('tagName');
// Update single property
modelComponent.set('tagName', '...');
// Update multiple properties
modelComponent.set({
tagName: '...',
// ...
});
// Some helpers
// Get all attributes
const attrs = modelComponent.getAttributes();
// Add attributes
modelComponent.addAttributes({ title: 'Test' });
// Replace all attributes
modelComponent.setAttributes({ title: 'Test' });
// Get the collection of all inner components
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())
);
```
You'll notice that, on any change, the component in the canvas and its export code are changing accordingly
:::tip
To know all the available methods/properties check the [Component API](/api/component.html)
:::
#### Listen to property changes
If you need to accomplish some kind of action on some property change you can setup listeners in the `init` method
```js
editor.DomComponents.addType('my-input-type', {
// ...
model: {
defaults: {
// ...
someprop: 'initial value',
},
init() {
this.on('change:someprop', this.handlePropChange);
// Listen to any attribute change
this.on('change:attributes', this.handleAttrChange);
// Listen to title attribute change
this.on('change:attributes:title', this.handleTitleChange);
},
handlePropChange() {
const { someprop } = this.props();
console.log('New value of someprop: ', someprop);
},
handleAttrChange() {
console.log('Attributes updated: ', this.getAttributes());
},
handleTitleChange() {
console.log('Attribute title updated: ', this.getAttributes().title);
},
}
});
```
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 simpliest 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.
* The DOM representation of the component acts differently from what you expect, so you need to change some behaviour.
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 event 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). Obviosly, all of this changes has 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 this cases you can use the `view` in your component type defintion. 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
```js
editor.DomComponents.addType('my-input-type', {
// ...
model: {
// ...
},
view: {
// Be default, the tag of the element is the same of the model
tagName: 'div',
// Add easily component specific listeners with `events`
// Being component specific (eg. you can't attach here listeners to window)
// you don't need to care about removing them when the component is removed,
// they will be managed automatically by the editor
events: {
click: 'clickOnElement',
// You can also make use of event delegation
// and listen to events bubbled from some inner element
'dblclick .inner-el': 'innerElClick',
},
innerElClick(ev) {
ev.stopPropagation();
// ...
// If you need you can access the model from any function in the view
this.model.components('Update inner components');
},
// On init you can create listeners, like in the model, or start some other
// function at the beginning
init({ model }) {
// Do something in view on model property change
this.listenTo(model, 'change:prop', this.handlePropChange);
// 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)
},
// Callback triggered when the element is removed from the canvas
removed() {
document.removeEventListener('click', this.onDocClick)
},
// Do something with the content once the element is rendered.
// The DOM element is passed as `el` in the argument object,
// but you can access it from any function via `this.el`
onRender({ el }) {
const btn = document.createElement('button');
btn.value = '+';
// Avoid adding events on inner elements, use `events`
btn.addEventListener('click', () => {});
el.appendChild(btn);
},
// Example of async content
async onRender({ el, model }) {
const asyncContent = await fetchSomething({
someDataFromModel: model.get('someData'),
});
// Rememebr that this changes exist only inside the editor canvas
// None of the DOM change is stored in your template data,
// if you need to store something, use the model properties
el.appendChild(asyncContent);
}
},
});
```
## Update Component Type
Updating component types is quite easy, let's see how:
```js
const domc = editor.DomComponents;
domc.addType('some-component', {
// You can update the isComponent logic or leave the one from `some-component`
// isComponent: (el) => false,
// Update the model, if you need
model: {
// The `defaults` property is handled differently
// and will be merged with the old `defaults`
defaults: {
tagName: '...', // Override an old one
someNewProp: 'Hello', // Add new property
},
init() {
// Ovverride `init` function in `some-component`
}
},
// Update the view, if you need
view: {},
});
```
### 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.
```js
comps.addType('my-new-component', {
isComponent: el => {/* ... */},
extend: 'other-defined-component',
model: { ... }, // Will extend the model from 'other-defined-component'
view: { ... }, // Will extend the view from 'other-defined-component'
});
```
```js
comps.addType('my-new-component', {
isComponent: el => {/* ... */},
extend: 'other-defined-component',
model: { ... }, // Will extend the model from 'other-defined-component'
extendView: 'other-defined-component-2',
view: { ... }, // Will extend the view from 'other-defined-component-2'
});
```
### Extend parent functions
When you need to reuse functions, of the parent you're extending, you can avoid writing something like this:
```js
domc.getType('parent-type').model.prototype.init.apply(this, arguments);
```
by using `extendFn` and `extendFnView` arrays:
```js
domc.addType('new-type', {
extend: 'parent-type',
extendFn: ['init'], // array of model functions to extend from `parent-type`
model: {
init() {
// do something;
},
}
});
```
The same would be for the view by using `extendFnView`
:::tip
If you need you can also read all the current component types by using `getTypes`
```js
editor.DomComponents.getTypes().forEach(compType => console.log(compType.id))
```
:::
## Lifecycle Hooks
Each component triggers different lifecycle hooks, which allows you to add custom actions at their specific stages.
We can distinguish 2 different types of hooks: **global** and **local**.
You define **local** hooks when you create/extend a component type (usually via some `model`/`view` method) and the reason is to react to an event of that
particular component type. Instead, the **global** one, will be called indistinctly on any component (you listen to them via `editor.on`) and you can make
use of them for a more generic use case or also listen to them inside other components.
Let's see below the flow of all hooks:
* **Local hook**: `model.init()` method, executed once the model of the component is initiliazed
* **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 initiliazed
* **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.
Below you can find an example usage of all the hooks
```js
editor.DomComponents.addType('test-component', {
model: {
defaults: {
testprop: 1,
},
init() {
console.log('Local hook: model.init');
this.listenTo(this, 'change:testprop', this.handlePropChange);
// 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);
},
removed() {
console.log('Local hook: model.removed');
},
handlePropChange() {
console.log('The value of testprop', this.get('testprop'));
}
},
view: {
init() {
console.log('Local hook: view.init');
},
onRender() {
console.log('Local hook: view.onRender');
},
},
});
// A block for the custom component
editor.BlockManager.add('test-component', {
label: 'Test Component',
content: '<div data-gjs-type="test-component">Test Component</div>',
});
// 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')));
```
## 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)

876
docs/modules/Components.md

File diff suppressed because it is too large

236
docs/modules/I18n.md

@ -0,0 +1,236 @@
---
title: I18n (Internalization)
---
# Internalization
The **I18n** module allows the internalization and updates of strings in the editor UI
::: warning
This guide is referring to GrapesJS v0.15.9 or higher
The module was added recently so we're open to receive support in [translating strings in other languages](#adding-new-language). Your help will be much appreciated!
:::
[[toc]]
## Configuration
By default, the editor includes only the English language, if you need other languages you have to import them manually.
**Note**: The language code is defined in the [ISO 639-1] standard.
```js
import grapesjs from 'grapesjs';
import it from 'grapesjs/locale/it';
import tr from 'grapesjs/locale/tr';
const editor = grapesjs.init({
...
i18n: {
// locale: 'en', // default locale
// detectLocale: true, // by default, the editor will detect the language
// localeFallback: 'en', // default fallback
messages: { it, tr },
}
});
```
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).
To find the correth path of the string you can check the [`en` locale file] and follow its inner path inside the locale object.
Let's say we want to update the default message of the empty state in Style Manager when no elements are selected.
<img :src="$withBase('/sm-empty-state.jpg')">
From the `en` locale file you can see it by following the path below
```js
{
...
styleManager: {
empty: 'Select an element before using Style Manager',
...
},
...
}
```
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',
}
}
});
```
Even if the UI shows correctly the updated message, we highly suggest to do all the API calls wrapped in a [plugin](Plugins.html)
```js
const myPlugin = editor => {
editor.I18n.addMessages({ ... });
// ...
}
grapesjs.init({
// ...
plugins: [myPlugin],
});
```
### Generated strings
Not all the strings are indicated in the `en` local file as some of them can be generated from `id`s, `name`s, etc.
If you look back at the `styleManager` path from the `en` file you'll notice the empty `properties` key
```js
...
styleManager: {
...
properties: {
// float: 'Float',
},
...
},
...
```
This object is used to translate property names inside StyleManager, so if you need, for instance, to change the auto-generated names for the `margin` properties
<img :src="$withBase('/margin-strings.jpg')">
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',
},
}
}
});
```
<!--
### Updates post rendering
If you try to update strings, by using API, once the UI is rendered you'll see no changes.
...
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:
1. First of all, be sure to check the language file in [`src/i18n/locale`](https://github.com/artf/grapesjs/blob/master/src/i18n/locale) doesn't exist already
1. [Open a new issue](https://github.com/artf/grapesjs/issues/new?title=XX%20Language%20support) to avoid overlap with other contributos. To be sure, check also no one else has opened already an issue for the same language
1. Start a new branch from `dev`
1. Copy (in the same folder) and rename the [`en` locale file] to the name of your language of choice (be sure to be compliant to [ISO 639-1])
1. Now you can start translating strings
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
This section is dedicated **only** to plugin developers and can also be skipped in case you use [grapesjs-cli](https://github.com/artf/grapesjs-cli) to init your plugin project
:::
If you're developing a plugin for GrapesJS and you need to support some string localization or simply change the default one, we recommend the following structure.
```
plugin-dir
- package.json
- README.md
- ...
- src
- index.js
- locale // create the locale foldar in your src
- en.js // All default strings should be placed here
```
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',
}
}
```
In your `index.js` use the `en.js` file and add `i18n` option to allow import of other local files
```js
// src/index.js
import en from 'locale/en';
export default (editor, opts = {}) => {
const options = {
i18n: {},
// ...
...opts,
};
// ...
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
::: warning
Remember that you can skip all these long steps and init your project with [grapesjs-cli](https://github.com/artf/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
import grapesjs from 'grapesjs';
// Import from your plugin
import yourPlugin from 'grapesjs-your-plugin';
import ch from 'grapesjs-your-plugin/locale/ch';
import fr from 'grapesjs-your-plugin/locale/fr';
const editor = grapesjs.init({
...
plugins: [ yourPlugin ],
pluginsOpts: {
[yourPlugin]: {
i18n: { ch, fr }
}
}
});
```
[ISO 639-1]: <https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes>
[`en` locale file]: <https://github.com/artf/grapesjs/blob/master/src/i18n/locale/en.js>

2
docs/modules/Plugins.md

@ -142,7 +142,7 @@ In this example, the plugin name is `my-plugin-name` and can't be used by other
## Boilerplate
If you want to start with a production-ready boilerplate for a named plugin, you might want to try [grapesjs-plugin-boilerplate](https://github.com/artf/grapesjs-plugin-boilerplate) which you can clone and start developing a named plugin immediately. For more informations check the repository
For fast plugin development, we highly recommend using [grapesjs-cli](https://github.com/artf/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 o Babel configurations). For more information check the repository
## Popular Plugins

3143
package-lock.json

File diff suppressed because it is too large

37
package.json

@ -1,7 +1,7 @@
{
"name": "grapesjs",
"description": "Free and Open Source Web Builder Framework",
"version": "0.15.5",
"version": "0.15.9",
"author": "Artur Arseniev",
"license": "BSD-3-Clause",
"homepage": "http://grapesjs.com",
@ -16,38 +16,38 @@
"url": "https://github.com/artf/grapesjs.git"
},
"dependencies": {
"@babel/runtime": "^7.5.5",
"@babel/runtime": "^7.7.1",
"backbone": "1.3.3",
"backbone-undo": "^0.2.5",
"cash-dom": "^1.3.7",
"codemirror": "^5.48.2",
"codemirror": "^5.49.2",
"codemirror-formatting": "^1.0.0",
"font-awesome": "^4.7.0",
"keymaster": "^1.6.2",
"promise-polyfill": "^8.1.3",
"spectrum-colorpicker": "^1.8.0",
"underscore": "^1.9.1"
},
"devDependencies": {
"@babel/core": "^7.5.5",
"@babel/plugin-transform-runtime": "^7.5.5",
"@babel/preset-env": "^7.5.5",
"@babel/cli": "^7.7.0",
"@babel/core": "^7.7.0",
"@babel/plugin-transform-runtime": "^7.6.2",
"@babel/preset-env": "^7.7.1",
"babel-loader": "^8.0.6",
"documentation": "^8.1.2",
"eslint": "^6.1.0",
"eslint": "^6.6.0",
"html-webpack-plugin": "^3.2.0",
"husky": "^2.7.0",
"jest": "^24.8.0",
"jest": "^24.9.0",
"lint-staged": "^8.2.1",
"node-sass": "^4.12.0",
"node-sass": "^4.13.0",
"npm-run-all": "^4.1.5",
"prettier": "^1.18.2",
"sinon": "^7.4.1",
"sinon": "^7.5.0",
"string-replace-loader": "^2.2.0",
"vuepress": "^0.10.2",
"webpack": "^4.39.1",
"webpack-cli": "^3.3.6",
"webpack-dev-server": "^3.8.0",
"webpack": "^4.41.2",
"webpack-cli": "^3.3.10",
"webpack-dev-server": "^3.9.0",
"whatwg-fetch": "^3.0.0"
},
"keywords": [
@ -70,8 +70,7 @@
"> 1%",
"ie 11",
"safari 8"
],
"modules": false
]
}
]
],
@ -97,6 +96,9 @@
"modulePaths": [
"./src"
],
"testMatch": [
"<rootDir>/test/specs/**/*.js"
],
"setupFiles": [
"<rootDir>/test/setup.js"
],
@ -119,8 +121,9 @@
"check": "npm run lint && npm run test",
"build": "npm run check && npm run v:patch && npm run build-dev && webpack --env=prod",
"build-n": "npm run check && npm run build:css && webpack --env=prod",
"build-dev": "webpack --env=dev && npm run build:css",
"build-dev": "webpack --env=dev && npm run build:css && npm run build:locale",
"build:css": "node-sass src/styles/scss/main.scss dist/css/grapes.min.css --output-style compressed",
"build:locale": "rm -rf ./locale && node scripts/build-locale.js && babel locale -d locale --copy-files --no-comments",
"v:patch": "npm version --no-git-tag-version patch",
"start": "run-p start:*",
"start:css": "npm run build:css -- -w",

28
scripts/build-locale.js

@ -0,0 +1,28 @@
const fs = require('fs');
const path = require('path');
const localeDir = './locale';
const localeSrcDir = './src/i18n/locale';
const copyRecursiveSync = (src, dest) => {
const exists = fs.existsSync(src);
const isDir = exists && fs.statSync(src).isDirectory();
if (isDir) {
fs.mkdirSync(dest);
fs.readdirSync(src).forEach((file) => {
copyRecursiveSync(path.join(src, file), path.join(dest, file));
});
} else if (exists) {
fs.createReadStream(src).pipe(fs.createWriteStream(dest));
}
};
copyRecursiveSync(localeSrcDir, localeDir);
// Create locale/index.js file
let result = '';
fs.readdirSync(localeDir).forEach(file => {
const name = file.replace('.js', '');
result += `export { default as ${name} } from './${name}'\n`;
});
fs.writeFileSync(`${localeDir}/index.js`, result);

12
src/asset_manager/config/config.js

@ -49,12 +49,6 @@ export default {
// }
autoAdd: 1,
// Text on upload input
uploadText: 'Drop files here or click to upload',
// Label for the add button
addBtnText: 'Add image',
// To upload your assets, the module uses Fetch API, with this option you
// overwrite it with something else.
// It should return a Promise
@ -96,12 +90,6 @@ export default {
// Any dropzone content to append inside dropzone element
dropzoneContent: '',
// Default title for the asset manager modal
modalTitle: 'Select Image',
//Default placeholder for input
inputPlaceholder: 'http://path/to/the/image.jpg',
//method called before upload, on return false upload is canceled.
// @example
// beforeUpload: (files) => {

10
src/asset_manager/view/AssetsView.js

@ -5,17 +5,16 @@ export default Backbone.View.extend({
submit: 'handleSubmit'
},
template(view) {
const pfx = view.pfx;
const ppfx = view.ppfx;
template({ pfx, ppfx, em, ...view }) {
return `
<div class="${pfx}assets-cont">
<div class="${pfx}assets-header">
<form class="${pfx}add-asset">
<div class="${ppfx}field ${pfx}add-field">
<input placeholder="${view.config.inputPlaceholder}"/>
<input placeholder="${em && em.t('assetManager.inputPlh')}"/>
</div>
<button class="${ppfx}btn-prim">${view.config.addBtnText}</button>
<button class="${ppfx}btn-prim">${em &&
em.t('assetManager.addButton')}</button>
<div style="clear:both"></div>
</form>
</div>
@ -30,6 +29,7 @@ export default Backbone.View.extend({
this.config = o.config;
this.pfx = this.config.stylePrefix || '';
this.ppfx = this.config.pStylePrefix || '';
this.em = this.config.em;
const coll = this.collection;
this.listenTo(coll, 'reset', this.renderAssets);
this.listenTo(coll, 'add', this.addToAsset);

10
src/asset_manager/view/FileUploader.js

@ -18,6 +18,7 @@ export default Backbone.View.extend(
this.options = opts;
const c = opts.config || {};
this.config = c;
this.em = this.config.em;
this.pfx = c.stylePrefix || '';
this.ppfx = c.pStylePrefix || '';
this.target = this.options.globalCollection || {};
@ -245,17 +246,18 @@ export default Backbone.View.extend(
},
render() {
this.$el.html(
const { $el, pfx, em } = this;
$el.html(
this.template({
title: this.config.uploadText,
title: em && em.t('assetManager.uploadTitle'),
uploadId: this.uploadId,
disabled: this.disabled,
multiUpload: this.multiUpload,
pfx: this.pfx
pfx
})
);
this.initDrop();
this.$el.attr('class', this.pfx + 'file-uploader');
$el.attr('class', pfx + 'file-uploader');
return this;
}
},

3
src/block_manager/view/BlockView.js

@ -139,7 +139,8 @@ export default Backbone.View.extend({
render() {
const { em, el, ppfx, model } = this;
const className = `${ppfx}block`;
const label = model.get('label');
const label =
(em && em.t(`blockManager.labels.${model.id}`)) || model.get('label');
const render = model.get('render');
const media = model.get('media');
el.className += ` ${className} ${ppfx}one-bg ${ppfx}four-color-h`;

15
src/block_manager/view/CategoryView.js

@ -14,7 +14,8 @@ export default Backbone.View.extend({
initialize(o = {}, config = {}) {
this.config = config;
const pfx = this.config.pStylePrefix || '';
const pfx = config.pStylePrefix || '';
this.em = config.em;
this.pfx = pfx;
this.caretR = 'fa fa-caret-right';
this.caretD = 'fa fa-caret-down';
@ -69,13 +70,17 @@ export default Backbone.View.extend({
},
render() {
this.el.innerHTML = this.template({
const { em, el, $el, model } = this;
const label =
em.t(`blockManager.categories.${model.id}`) || model.get('label');
el.innerHTML = this.template({
pfx: this.pfx,
label: this.model.get('label')
label
});
this.el.className = this.className;
this.$el.css({ order: this.model.get('order') });
el.className = this.className;
$el.css({ order: model.get('order') });
this.updateVisibility();
return this;
}
});

2
src/canvas/view/CanvasView.js

@ -440,7 +440,7 @@ export default Backbone.View.extend({
const id = model.getId();
if (!view.scriptContainer) {
view.scriptContainer = $(`<div id="${id}">`);
view.scriptContainer = $(`<div data-id="${id}">`);
this.getJsContainer().appendChild(view.scriptContainer.get(0));
}

2
src/commands/view/ComponentDrag.js

@ -120,6 +120,8 @@ export default {
},
updateGuides(guides) {
const { editor } = this;
(guides || this.guides).forEach(item => {
const { origin } = item;
const { top, height, left, width } = editor.Canvas.getElementPos(origin);

2
src/commands/view/OpenAssets.js

@ -4,7 +4,7 @@ export default {
const am = editor.AssetManager;
const config = am.getConfig();
const amContainer = am.getContainer();
const title = opts.modalTitle || config.modalTitle || '';
const title = opts.modalTitle || editor.t('assetManager.modalTitle') || '';
const types = opts.types;
const accept = opts.accept;

2
src/commands/view/OpenStyleManager.js

@ -33,7 +33,7 @@ export default {
const pfx = smConfig.stylePrefix;
// Create header
this.$header = $(
`<div class="${pfx}header">${smConfig.textNoElement}</div>`
`<div class="${pfx}header">${em.t('styleManager.empty')}</div>`
);
this.$cn.append(this.$header);

7
src/commands/view/OpenTraitManager.js

@ -5,6 +5,7 @@ const $ = Backbone.$;
export default {
run(editor, sender) {
this.sender = sender;
const em = editor.getModel();
var config = editor.Config;
var pfx = config.stylePrefix;
@ -18,11 +19,13 @@ export default {
this.$cn2 = $('<div></div>');
this.$cn.append(this.$cn2);
this.$header = $('<div>').append(
`<div class="${confTm.stylePrefix}header">${confTm.textNoElement}</div>`
`<div class="${confTm.stylePrefix}header">${em.t(
'traitManager.empty'
)}</div>`
);
this.$cn.append(this.$header);
this.$cn2.append(
`<div class="${pfx}traits-label">${confTm.labelContainer}</div>`
`<div class="${pfx}traits-label">${em.t('traitManager.label')}</div>`
);
this.$cn2.append(tmView.render().el);
var panels = editor.Panels;

3
src/commands/view/SelectComponent.js

@ -518,7 +518,8 @@ export default {
this.toolbar = new Toolbar(toolbar);
var toolbarView = new ToolbarView({
collection: this.toolbar,
editor: this.editor
editor: this.editor,
em
});
toolbarEl.appendChild(toolbarView.render().el);
}

4
src/device_manager/config/config.js

@ -1,5 +1,3 @@
export default {
devices: [],
deviceLabel: 'Device'
devices: []
};

29
src/device_manager/index.js

@ -58,7 +58,8 @@ export default () => {
if (!(name in c)) c[name] = defaults[name];
}
devices = new Devices(c.devices);
devices = new Devices();
(c.devices || []).forEach(dv => this.add(dv.id || dv.name, dv.width, dv));
view = new DevicesView({
collection: devices,
config: c
@ -68,21 +69,27 @@ export default () => {
/**
* Add new device to the collection. URLs are supposed to be unique
* @param {string} name Device name
* @param {string} width Width of the device
* @param {Object} opts Custom options
* @return {Device} Added device
* @param {String} id Device id
* @param {String} width Width of the device
* @param {Object} [opts] Custom options
* @returns {Device} Added device
* @example
* deviceManager.add('Tablet', '900px');
* deviceManager.add('Tablet2', '900px', {
* deviceManager.add('tablet', '900px');
* deviceManager.add('tablet2', '900px', {
* height: '300px',
* // At first, GrapesJS tries to localize the name by device id.
* // In case is not found, the `name` property is used (or `id` if name is missing)
* name: 'Tablet 2',
* widthMedia: '810px', // the width that will be used for the CSS media
* });
*/
add(name, width, opts) {
var obj = opts || {};
obj.name = name;
obj.width = width;
add(id, width, opts = {}) {
const obj = {
...opts,
id,
name: opts.name || id,
width: width
};
return devices.add(obj);
},

24
src/device_manager/view/DevicesView.js

@ -67,25 +67,29 @@ export default Backbone.View.extend({
* @private
*/
getOptions() {
var result = '';
this.collection.each(device => {
var name = device.get('name');
result += '<option value="' + name + '">' + name + '</option>';
const { collection, em } = this;
let result = '';
collection.each(device => {
const { name, id } = device.attributes;
const label = (em && em.t && em.t(`deviceManager.devices.${id}`)) || name;
result += `<option value="${name}">${label}</option>`;
});
return result;
},
render() {
var pfx = this.ppfx;
this.$el.html(
const { em, ppfx, $el, el } = this;
$el.html(
this.template({
ppfx: pfx,
deviceLabel: this.config.deviceLabel
ppfx,
deviceLabel: em && em.t && em.t('deviceManager.device')
})
);
this.devicesEl = this.$el.find('.' + pfx + 'devices');
this.devicesEl = $el.find(`.${ppfx}devices`);
this.devicesEl.append(this.getOptions());
this.el.className = pfx + 'devices-c';
el.className = `${ppfx}devices-c`;
return this;
}
});

85
src/dom_components/model/Component.js

@ -184,6 +184,7 @@ const Component = Backbone.Model.extend(Styleable).extend(
this.listenTo(this, 'change:script', this.scriptUpdated);
this.listenTo(this, 'change:tagName', this.tagUpdated);
this.listenTo(this, 'change:attributes', this.attrUpdated);
this.listenTo(this, 'change:attributes:id', this._idUpdated);
this.set('status', '');
// Register global updates for collection properties
@ -325,19 +326,8 @@ const Component = Backbone.Model.extend(Styleable).extend(
* Emit changes for each updated attribute
* @private
*/
attrUpdated() {
this.setAttributes(this.get('attributes'), { silent: 1 });
},
/**
* Update attributes of the component
* @param {Object} attrs Key value attributes
* @return {this}
* @example
* component.setAttributes({ id: 'test', 'data-key': 'value' });
*/
setAttributes(attrs, opts = {}) {
attrs = { ...attrs };
attrUpdated(m, v, opts = {}) {
const attrs = this.get('attributes');
// Handle classes
const classes = attrs.class;
@ -349,13 +339,22 @@ const Component = Backbone.Model.extend(Styleable).extend(
style && this.setStyle(style);
delete attrs.style;
this.set('attributes', attrs, opts);
const attrPrev = { ...this.previous('attributes') };
const diff = shallowDiff(attrPrev, attrs);
const diff = shallowDiff(attrPrev, this.get('attributes'));
keys(diff).forEach(pr =>
this.trigger(`change:attributes:${pr}`, this, diff[pr])
this.trigger(`change:attributes:${pr}`, this, diff[pr], opts)
);
},
/**
* Update attributes of the component
* @param {Object} attrs Key value attributes
* @return {this}
* @example
* component.setAttributes({ id: 'test', 'data-key': 'value' });
*/
setAttributes(attrs, opts = {}) {
this.set('attributes', { ...attrs }, opts);
return this;
},
@ -406,8 +405,9 @@ const Component = Backbone.Model.extend(Styleable).extend(
const { opt } = this;
if (em && em.getConfig('avoidInlineStyle') && !opt.temporary) {
const style = this.get('style') || {};
prop = isString(prop) ? this.parseStyle(prop) : prop;
prop = { ...prop, ...this.get('style') };
prop = { ...prop, ...style };
const state = this.get('state');
const cc = em.get('CssComposer');
const propOrig = this.getStyle();
@ -865,12 +865,17 @@ const Component = Backbone.Model.extend(Styleable).extend(
* @return {String}
* */
getName() {
let customName = this.get('name') || this.get('custom-name');
let tag = this.get('tagName');
const { em } = this;
const { type, tagName } = this.attributes;
const customName = this.get('name') || this.get('custom-name');
let tag = tagName;
tag = tag == 'div' ? 'box' : tag;
let name = this.get('type') || tag;
let name = type || tag;
name = name.charAt(0).toUpperCase() + name.slice(1);
return customName || name;
const i18nPfx = 'domComponents.names.';
const i18nStr =
em && (em.t(`${i18nPfx}${type}`) || em.t(`${i18nPfx}${tagName}`));
return i18nStr || customName || name;
},
/**
@ -1018,10 +1023,10 @@ const Component = Backbone.Model.extend(Styleable).extend(
* @param {String} id
* @return {this}
*/
setId(id) {
setId(id, opts) {
const attrs = { ...this.get('attributes') };
attrs.id = id;
this.set('attributes', attrs);
this.set('attributes', attrs, opts);
return this;
},
@ -1139,6 +1144,40 @@ const Component = Backbone.Model.extend(Styleable).extend(
const selector = rule && rule.get('selectors').at(0);
selector && selector.set('name', newId);
return this;
},
_getStyleRule({ id } = {}) {
const { em } = this;
const idS = id || this.getId();
return em && em.get('CssComposer').getIdRule(idS);
},
_getStyleSelector(opts) {
const rule = this._getStyleRule(opts);
return rule && rule.get('selectors').at(0);
},
_idUpdated(m, v, opts = {}) {
if (opts.idUpdate) return;
const { ccid } = this;
const { id } = this.get('attributes') || {};
const idPrev = (this.previous('attributes') || {}).id || ccid;
const list = Component.getList(this);
// If the ID already exists I need to rollback to the old one
if (list[id]) {
return this.setId(idPrev, { idUpdate: 1 });
}
// Remove the old ID reference and add the new one
delete list[idPrev];
list[id] = this;
this.ccid = id;
// Update the style selector name
const selector = this._getStyleSelector({ id: idPrev });
selector && selector.set({ name: id, label: id });
}
},
{

4
src/dom_components/model/ComponentVideo.js

@ -182,8 +182,8 @@ export default Component.extend(
{
label: 'Poster',
name: 'poster',
placeholder: 'eg. ./media/image.jpg',
changeProp: 1
placeholder: 'eg. ./media/image.jpg'
// changeProp: 1
},
this.getAutoplayTrait(),
this.getLoopTrait(),

28
src/dom_components/model/Components.js

@ -71,9 +71,11 @@ export default Backbone.Collection.extend({
});
}
(isArray(models) ? models : [models])
const isMult = isArray(models);
models = (isMult ? models : [models])
.filter(i => i)
.forEach(model => this.processDef(model));
.map(model => this.processDef(model));
models = isMult ? models : models[0];
return Backbone.Collection.prototype.add.apply(this, [models, opt]);
},
@ -81,18 +83,26 @@ export default Backbone.Collection.extend({
/**
* Process component definition.
*/
processDef(model) {
processDef(mdl) {
// Avoid processing Models
if (mdl.cid && mdl.ccid) return mdl;
const { em, config = {} } = this;
const { processor } = config;
const modelPr = processor && processor(model);
if (modelPr) {
each(model, (val, key) => delete model[key]);
extend(model, modelPr);
let model = mdl;
if (processor) {
model = { ...model }; // Avoid 'Cannot delete property ...'
const modelPr = processor(model);
if (modelPr) {
each(model, (val, key) => delete model[key]);
extend(model, modelPr);
}
}
// React JSX
// React JSX preset
if (model.$$typeof && typeof model.props == 'object') {
model = { ...model };
model.props = { ...model.props };
const domc = em.get('DomComponents');
const parser = em.get('Parser');
const { parserHtml } = parser;

4
src/dom_components/view/ComponentLabelView.js

@ -1,3 +1,5 @@
import ComponentLinkView from './ComponentLinkView';
export default ComponentLinkView.extend({});
export default ComponentLinkView.extend({
tagName: 'span' // Avoid Firefox bug with label editing #2332
});

32
src/dom_components/view/ComponentTextView.js

@ -45,7 +45,6 @@ export default ComponentView.extend({
}
}
this.rteEnabled = 1;
this.toggleEvents(1);
},
@ -67,17 +66,34 @@ export default ComponentView.extend({
this.syncContent();
}
this.rteEnabled = 0;
this.toggleEvents();
},
/**
* get content from RTE
* @return string
*/
getContent() {
const { rte } = this;
const { activeRte } = rte || {};
let content = '';
if (activeRte && typeof activeRte.getContent === 'function') {
content = activeRte.getContent();
} else {
content = this.getChildrenContainer().innerHTML;
}
return content;
},
/**
* Merge content from the DOM to the model
*/
syncContent(opts = {}) {
const { model, rte, rteEnabled } = this;
if (!rteEnabled && !opts.force) return;
const content = this.getChildrenContainer().innerHTML;
const content = this.getContent();
const comps = model.components();
const contentOpt = { fromDisable: 1, ...opts };
comps.length && comps.reset(null, opts);
@ -141,14 +157,17 @@ export default ComponentView.extend({
* @param {Boolean} enable
*/
toggleEvents(enable) {
var method = enable ? 'on' : 'off';
const { em } = this;
const mixins = { on, off };
this.em.setEditing(enable);
const method = enable ? 'on' : 'off';
em.setEditing(enable);
this.rteEnabled = !!enable;
// The ownerDocument is from the frame
var elDocs = [this.el.ownerDocument, document];
mixins.off(elDocs, 'mousedown', this.disableEditing);
mixins[method](elDocs, 'mousedown', this.disableEditing);
em[method]('toolbar:run:before', this.disableEditing);
// Avoid closing edit mode on component click
this.$el.off('mousedown', this.disablePropagation);
@ -161,8 +180,9 @@ export default ComponentView.extend({
while (el) {
el.draggable = enable ? !1 : !0;
// Note: el.parentNode is sometimes null here
el = el.parentNode;
el.tagName == 'BODY' && (el = 0);
el && el.tagName == 'BODY' && (el = 0);
}
}
}

14
src/dom_components/view/ComponentView.js

@ -276,12 +276,14 @@ export default Backbone.View.extend({
const defaultAttr = {
'data-gjs-type': type || 'default',
...(draggableComponents && { draggable: true }),
...(highlightable && { 'data-highlightable': 1 }),
...(textable && {
contenteditable: 'false',
'data-gjs-textable': 'true'
})
...(draggableComponents ? { draggable: true } : {}),
...(highlightable ? { 'data-highlightable': 1 } : {}),
...(textable
? {
contenteditable: 'false',
'data-gjs-textable': 'true'
}
: {})
};
// Remove all current attributes

13
src/dom_components/view/ToolbarButtonView.js

@ -13,8 +13,10 @@ export default Backbone.View.extend({
return this.model.get('attributes');
},
initialize(opts) {
this.editor = opts.config.editor;
initialize(opts = {}) {
const { config = {} } = opts;
this.em = config.em;
this.editor = config.editor;
},
handleClick(event) {
@ -35,10 +37,8 @@ export default Backbone.View.extend({
* https://github.com/artf/grapesjs/issues/2207
*/
const {
left,
top
} = this.editor.Canvas.getFrameEl().getBoundingClientRect();
const { editor } = this;
const { left, top } = editor.Canvas.getFrameEl().getBoundingClientRect();
const calibrated = {
...event,
@ -46,6 +46,7 @@ export default Backbone.View.extend({
clientY: event.clientY - top
};
em.trigger('toolbar:run:before');
this.execCommand(calibrated);
},

4
src/dom_components/view/ToolbarView.js

@ -4,8 +4,8 @@ import ToolbarButtonView from './ToolbarButtonView';
export default DomainViews.extend({
itemView: ToolbarButtonView,
initialize(opts) {
this.config = { editor: opts.editor || '' };
initialize(opts = {}) {
this.config = { editor: opts.editor || '', em: opts.em };
this.listenTo(this.collection, 'reset', this.render);
}
});

3
src/domain_abstract/model/Styleable.js

@ -22,7 +22,8 @@ export default {
* @return {Object}
*/
getStyle() {
return { ...this.get('style') };
const style = this.get('style') || {};
return { ...style };
},
/**

14
src/editor/config/config.js

@ -123,7 +123,8 @@ export default {
// element's `style` attribute. Unfortunately, inline styling doesn't allow
// use of media queries (@media) or even pseudo selectors (eg. :hover).
// When `avoidInlineStyle` is true all styles are inserted inside the css rule
avoidInlineStyle: 0,
// @deprecated Don't use this option, we don't support inline styling anymore
avoidInlineStyle: 1,
// Avoid default properties from storable JSON data, like `components` and `styles`.
// With this option enabled your data will be smaller (usefull if need to
@ -147,9 +148,16 @@ export default {
// To get more about this feature read: https://github.com/artf/grapesjs/issues/1936
dragMode: 0,
// Import asynchronously CSS to use as icons
cssIcons:
'https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css',
// Dom element
el: '',
// Configurations for I18n
i18n: {},
// Configurations for Undo Manager
undoManager: {},
@ -193,20 +201,24 @@ export default {
deviceManager: {
devices: [
{
id: 'desktop',
name: 'Desktop',
width: ''
},
{
id: 'tablet',
name: 'Tablet',
width: '768px',
widthMedia: '992px'
},
{
id: 'mobileLandscape',
name: 'Mobile landscape',
width: '568px',
widthMedia: '768px'
},
{
id: 'mobilePortrait',
name: 'Mobile portrait',
width: '320px',
widthMedia: '480px'

25
src/editor/index.js

@ -125,6 +125,12 @@ export default (config = {}) => {
*/
editor: em,
/**
* @property {I18n}
* @private
*/
I18n: em.get('I18n'),
/**
* @property {DomComponents}
* @private
@ -282,6 +288,7 @@ export default (config = {}) => {
/**
* Returns CSS built inside canvas
* @param {Object} [opts={}] Options
* @param {Boolean} [opts.avoidProtected=false] Don't include protected CSS
* @return {string} CSS string
*/
getCss(opts) {
@ -648,6 +655,24 @@ export default (config = {}) => {
return this;
},
/**
* Translate label
* @param {String} key Label to translate
* @param {Object} [opts] Options for the translation
* @param {Object} [opts.params] Params for the translation
* @param {Boolean} [opts.noWarn] Avoid warnings in case of missing resources
* @returns {String}
* @example
* editor.t('msg');
* // use params
* editor.t('msg2', { params: { test: 'hello' } });
* // custom local
* editor.t('msg2', { params: { test: 'hello' }, l: 'it' });
*/
t(...args) {
return em.t(...args);
},
/**
* Attach event
* @param {string} event Event name

39
src/editor/model/Editor.js

@ -1,10 +1,18 @@
import { isUndefined, isArray, contains, toArray, keys } from 'underscore';
import {
isUndefined,
isFunction,
isArray,
contains,
toArray,
keys
} from 'underscore';
import Backbone from 'backbone';
import Extender from 'utils/extender';
import { getModel } from 'utils/mixins';
const deps = [
require('utils'),
require('i18n'),
require('keymaps'),
require('undo_manager'),
require('storage_manager'),
@ -192,9 +200,9 @@ export default Backbone.Model.extend({
if (Mod.storageKey && Mod.store && Mod.load && sm) {
cfg.stm = sm;
const storables = this.get('storables');
storables.push(Mod);
this.set('storables', storables);
// DomComponents should be load before CSS Composer
const mth = name == 'domComponents' ? 'unshift' : 'push';
this.get('storables')[mth](Mod);
}
cfg.em = this;
@ -513,26 +521,23 @@ export default Backbone.Model.extend({
* @private
*/
getCacheLoad(force, clb) {
var f = force ? 1 : 0;
if (this.cacheLoad && !f) return this.cacheLoad;
var sm = this.get('StorageManager');
var load = [];
if (this.cacheLoad && !force) return this.cacheLoad;
const sm = this.get('StorageManager');
const load = [];
if (!sm) return {};
this.get('storables').forEach(m => {
var key = m.storageKey;
key = typeof key === 'function' ? key() : key;
var keys = key instanceof Array ? key : [key];
keys.forEach(k => {
load.push(k);
});
let key = m.storageKey;
key = isFunction(key) ? key() : key;
const keys = isArray(key) ? key : [key];
keys.forEach(k => load.push(k));
});
sm.load(load, res => {
this.cacheLoad = res;
clb && clb(res);
setTimeout(() => this.trigger('storage:load', res), 0);
setTimeout(() => this.trigger('storage:load', res));
});
},
@ -629,6 +634,10 @@ export default Backbone.Model.extend({
return this.set('dmode', value);
},
t(...args) {
return this.get('I18n').t(...args);
},
/**
* Returns true if the editor is in absolute mode
* @returns {Boolean}

20
src/editor/view/EditorView.js

@ -1,4 +1,6 @@
import Backbone from 'backbone';
import { appendStyles } from 'utils/mixins';
const $ = Backbone.$;
export default Backbone.View.extend({
@ -16,24 +18,22 @@ export default Backbone.View.extend({
},
render() {
const model = this.model;
const el = this.$el;
const conf = this.conf;
const contEl = $(conf.el || `body ${conf.container}`);
const { model, $el, conf } = this;
const pfx = conf.stylePrefix;
el.empty();
const contEl = $(conf.el || `body ${conf.container}`);
appendStyles(conf.cssIcons, { unique: 1, prepand: 1 });
$el.empty();
if (conf.width) contEl.css('width', conf.width);
if (conf.height) contEl.css('height', conf.height);
el.append(model.get('Canvas').render());
el.append(this.pn.render());
el.attr('class', `${pfx}editor ${pfx}one-bg ${pfx}two-color`);
$el.append(model.get('Canvas').render());
$el.append(this.pn.render());
$el.attr('class', `${pfx}editor ${pfx}one-bg ${pfx}two-color`);
contEl
.addClass(`${pfx}editor-cont`)
.empty()
.append(el);
.append($el);
return this;
}

20
src/i18n/config.js

@ -0,0 +1,20 @@
import en from './locale/en';
export default {
// Locale value
locale: 'en',
// Fallback locale
localeFallback: 'en',
// Detect locale by checking browser language
detectLocale: 1,
// Show warnings when some of the i18n resources are missing
debug: 0,
// Messages to translate
messages: {
en
}
};

247
src/i18n/index.js

@ -0,0 +1,247 @@
/**
* You can customize the initial state of the module from the editor initialization, by passing the following [Configuration Object](https://github.com/artf/grapesjs/blob/master/src/i18n/config.js)
* ```js
* const editor = grapesjs.init({
* i18n: {
* locale: 'en',
* localeFallback: 'en',
* messages: {
* it: { hello: 'Ciao', ... },
* ...
* }
* }
* })
* ```
*
* Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance
*
* ```js
* const i18n = editor.I18n;
* ```
*
* ### Events
* * `i18n:add` - New set of messages is added
* * `i18n:update` - The set of messages is updated
* * `i18n:locale` - Locale changed
*
* @module I18n
*/
import { isUndefined, isString } from 'underscore';
import config from './config';
const isObj = el => !Array.isArray(el) && el !== null && typeof el === 'object';
const deepAssign = (...args) => {
const target = { ...args[0] };
for (let i = 1; i < args.length; i++) {
const source = { ...args[i] };
for (let key in source) {
const targValue = target[key];
const srcValue = source[key];
if (isObj(targValue) && isObj(srcValue)) {
target[key] = deepAssign(targValue, srcValue);
} else {
target[key] = srcValue;
}
}
}
return target;
};
export default () => {
return {
name: 'I18n',
config,
/**
* Initialize module
* @param {Object} config Configurations
* @private
*/
init(opts = {}) {
this.config = {
...config,
...opts,
messages: {
...config.messages,
...(opts.messages || {})
}
};
if (this.config.detectLocale) {
this.config.locale = this._localLang();
}
this.em = opts.em;
return this;
},
/**
* Get module configurations
* @returns {Object} Configuration object
*/
getConfig() {
return this.config;
},
/**
* Update current locale
* @param {String} locale Locale value
* @returns {this}
* @example
* i18n.setLocale('it');
*/
setLocale(locale) {
const { em, config } = this;
const evObj = { value: locale, valuePrev: config.locale };
em && em.trigger('i18n:locale', evObj);
config.locale = locale;
return this;
},
/**
* Get current locale
* @returns {String} Current locale value
*/
getLocale() {
return this.config.locale;
},
/**
* Get all messages
* @param {String} [lang] Specify the language of messages to return
* @param {Object} [opts] Options
* @param {Boolean} [opts.debug] Show warnings in case of missing language
* @returns {Object}
* @example
* i18n.getMessages();
* // -> { en: { hello: '...' }, ... }
* i18n.getMessages('en');
* // -> { hello: '...' }
*/
getMessages(lang, opts = {}) {
const { messages } = this.config;
lang &&
!messages[lang] &&
this._debug(`'${lang}' i18n lang not found`, opts);
return lang ? messages[lang] : messages;
},
/**
* Set new set of messages
* @param {Object} msg Set of messages
* @returns {this}
* @example
* i18n.getMessages();
* // -> { en: { msg1: 'Msg 1', msg2: 'Msg 2', } }
* i18n.setMessages({ en: { msg2: 'Msg 2 up', msg3: 'Msg 3', } });
* // Set replaced
* i18n.getMessages();
* // -> { en: { msg2: 'Msg 2 up', msg3: 'Msg 3', } }
*/
setMessages(msg) {
const { em, config } = this;
config.messages = msg;
em && em.trigger('i18n:update', msg);
return this;
},
/**
* Update messages
* @param {Object} msg Set of messages to add
* @returns {this}
* @example
* i18n.getMessages();
* // -> { en: { msg1: 'Msg 1', msg2: 'Msg 2', } }
* i18n.addMessages({ en: { msg2: 'Msg 2 up', msg3: 'Msg 3', } });
* // Set updated
* i18n.getMessages();
* // -> { en: { msg1: 'Msg 1', msg2: 'Msg 2 up', msg3: 'Msg 3', } }
*/
addMessages(msg) {
const { em } = this;
const { messages } = this.config;
em && em.trigger('i18n:add', msg);
this.setMessages(deepAssign(messages, msg));
return this;
},
/**
* Translate the locale message
* @param {String} key Label to translate
* @param {Object} [opts] Options for the translation
* @param {Object} [opts.params] Params for the translation
* @param {Boolean} [opts.debug] Show warnings in case of missing resources
* @returns {String}
* @example
* obj.setMessages({
* en: { msg: 'Msg', msg2: 'Msg {test}'},
* it: { msg2: 'Msg {test} it'},
* });
* obj.t('msg');
* // -> outputs `Msg`
* obj.t('msg2', { params: { test: 'hello' } }); // use params
* // -> outputs `Msg hello`
* obj.t('msg2', { l: 'it', params: { test: 'hello' } }); // custom local
* // -> outputs `Msg hello it`
*/
t(key, opts = {}) {
const { config } = this;
const param = opts.params || {};
const locale = opts.l || this.getLocale();
const localeFlb = opts.lFlb || config.localeFallback;
let result = this._getMsg(key, locale, opts);
// Try with fallback
if (!result) result = this._getMsg(key, localeFlb, opts);
!result &&
this._debug(`'${key}' i18n key not found in '${locale}' lang`, opts);
result =
result && isString(result) ? this._addParams(result, param) : result;
return result;
},
_localLang() {
const nav = window.navigator || {};
const lang = nav.language || nav.userLanguage;
return lang ? lang.split('-')[0] : 'en';
},
_addParams(str, params) {
const reg = new RegExp(`\{([\\w\\d-]*)\}`, 'g');
return str.replace(reg, (m, val) => params[val] || '').trim();
},
_getMsg(key, locale, opts = {}) {
const msgSet = this.getMessages(locale, opts);
// Lang set is missing
if (!msgSet) return;
let result = msgSet[key];
// Check for nested getter
if (!result && key.indexOf('.') > 0) {
result = key.split('.').reduce((lang, key) => {
if (isUndefined(lang)) return;
return lang[key];
}, msgSet);
}
return result;
},
_debug(str, opts = {}) {
const { em, config } = this;
(opts.debug || config.debug) && em && em.logWarning(str);
}
};
};

117
src/i18n/locale/en.js

@ -0,0 +1,117 @@
const traitInputAttr = { placeholder: 'eg. Text here' };
export default {
assetManager: {
addButton: 'Add image',
inputPlh: 'http://path/to/the/image.jpg',
modalTitle: 'Select Image',
uploadTitle: 'Drop files here or click to upload'
},
// Here just as a reference, GrapesJS core doesn't contain any block,
// so this should be omitted from other local files
blockManager: {
labels: {
// 'block-id': 'Block Label',
},
categories: {
// 'category-id': 'Category Label',
}
},
domComponents: {
names: {
'': 'Box',
wrapper: 'Body',
text: 'Text',
comment: 'Comment',
image: 'Image',
video: 'Video',
label: 'Label',
link: 'Link',
map: 'Map',
tfoot: 'Table foot',
tbody: 'Table body',
thead: 'Table head',
table: 'Table',
row: 'Table row',
cell: 'Table cell'
}
},
deviceManager: {
device: 'Device',
devices: {
desktop: 'Desktop',
tablet: 'Tablet',
mobileLandscape: 'Mobile Landscape',
mobilePortrait: 'Mobile Portrait'
}
},
panels: {
buttons: {
titles: {
preview: 'Preview',
fullscreen: 'Fullscreen',
'sw-visibility': 'View components',
'export-template': 'View code',
'open-sm': 'Open Style Manager',
'open-tm': 'Settings',
'open-layers': 'Open Layer Manager',
'open-blocks': 'Open Blocks'
}
}
},
selectorManager: {
label: 'Classes',
selected: 'Selected',
emptyState: '- State -',
states: {
hover: 'Hover',
active: 'Click',
'nth-of-type(2n)': 'Even/Odd'
}
},
styleManager: {
empty: 'Select an element before using Style Manager',
layer: 'Layer',
fileButton: 'Images',
sectors: {
general: 'General',
layout: 'Layout',
typography: 'Typography',
decorations: 'Decorations',
extra: 'Extra',
flex: 'Flex',
dimension: 'Dimension'
},
// The core library generates the name by their `property` name
properties: {
// float: 'Float',
}
},
traitManager: {
empty: 'Select an element before using Trait Manager',
label: 'Component settings',
traits: {
// The core library generates the name by their `name` property
labels: {
// id: 'Id',
// alt: 'Alt',
// title: 'Title',
// href: 'Href',
},
// In a simple trait, like text input, these are used on input attributes
attributes: {
id: traitInputAttr,
alt: traitInputAttr,
title: traitInputAttr,
href: { placeholder: 'eg. https://google.com' }
},
// In a trait like select, these are used to translate option names
options: {
target: {
false: 'This window',
_blank: 'New window'
}
}
}
}
};

103
src/i18n/locale/it.js

@ -0,0 +1,103 @@
const traitInputAttr = { placeholder: 'es. Testo' };
export default {
assetManager: {
addButton: 'Aggiungi immagine',
inputPlh: 'http://percorso/immagine.jpg',
modalTitle: 'Seleziona immagine',
uploadTitle: 'Trascina qui i tuoi file o clicca per caricarli'
},
domComponents: {
names: {
'': 'Elemento',
wrapper: 'Contenitore',
text: 'Testo',
comment: 'Commento',
image: 'Immagine',
video: 'Video',
label: 'Label',
link: 'Link',
map: 'Mappa',
tfoot: 'Tabella piede',
tbody: 'Tabella corpo',
thead: 'Tabella testa',
table: 'Tabella',
row: 'Tabella riga',
cell: 'Tabella colonna'
}
},
deviceManager: {
device: 'Dispositivo',
devices: {
desktop: 'Desktop',
tablet: 'Tablet',
mobileLandscape: 'Mobile panoramica',
mobilePortrait: 'Mobile'
}
},
panels: {
buttons: {
titles: {
preview: 'Anteprima',
fullscreen: 'Schermo intero',
'sw-visibility': 'Mostra componenti',
'export-template': 'Mostra codice',
'open-sm': 'Mostra Style Manager',
'open-tm': 'Configurazioni',
'open-layers': 'Mostra Livelli',
'open-blocks': 'Mostra Blocchi'
}
}
},
selectorManager: {
label: 'Classi',
selected: 'Selezionato',
emptyState: '- Stati -',
states: {
hover: 'Hover',
active: 'Click',
'nth-of-type(2n)': 'Pari/Dispari'
}
},
styleManager: {
empty: 'Seleziona un elemento prima di usare il Style Manager',
layer: 'Livello',
fileButton: 'Immagini',
sectors: {
general: 'Generale',
layout: 'Layout',
typography: 'Tipografia',
decorations: 'Decorazioni',
extra: 'Extra',
flex: 'Flex',
dimension: 'Dimensioni'
},
// The core library generates the name by their `property` name
properties: {
// float: 'Float',
}
},
traitManager: {
empty: 'Seleziona un elemento prima di usare il Trait Manager',
label: 'Configurazione componente',
traits: {
labels: {
id: 'Id',
alt: 'Alt',
title: 'Titolo'
},
attributes: {
id: traitInputAttr,
alt: traitInputAttr,
title: traitInputAttr,
href: { placeholder: 'es. https://google.com' }
},
options: {
target: {
false: 'Questa finestra',
_blank: 'Nuova finestra'
}
}
}
}
};

141
src/i18n/locale/tr.js

@ -0,0 +1,141 @@
export default {
assetManager: {
addButton: 'Görsel Ekle',
modalTitle: 'Görsel Seçin',
uploadTitle: 'Dosya yüklemek için buraya sürükleyin veya tıklayın'
},
deviceManager: {
device: 'Cihaz',
devices: {
desktop: 'Masaüstü',
tablet: 'Tablet',
mobileLandscape: 'Mobil Yatay',
mobilePortrait: 'Mobil Dikey'
}
},
panels: {
buttons: {
titles: {
preview: 'Önizleme',
fullscreen: 'Tam Ekran',
'sw-visibility': 'Bileşenleri Göster',
'export-template': 'Kodu Göster',
'open-sm': 'Stil Düzenleyiciyi Aç',
'open-tm': 'Ayarlar',
'open-layers': 'Katmanlar',
'open-blocks': 'Bloklar'
}
}
},
selectorManager: {
selected: 'Seçili',
emptyState: '- DURUM -',
label: 'Sınıflar'
},
styleManager: {
empty: 'Stilini düzenlemek istediğiniz öğeyi seçiniz',
layer: 'Katman',
sectors: {
general: 'Genel',
layout: 'Düzen',
typography: 'Tipografi',
decorations: 'Dekorasyon',
extra: 'Ekstra',
flex: 'Flex',
dimension: 'Boyut'
},
properties: {
float: 'Kaydır',
display: 'Görünüm',
position: 'Pozisyon',
top: 'Üst',
right: 'Sağ',
left: 'Sol',
bottom: 'Alt',
width: 'Genişlik',
height: 'Yükseklik',
'max-width': 'Maks. Genişlik',
'max-height': 'Maks. Yükseklik',
margin: 'Margin',
'margin-top': 'Margin Üst',
'margin-right': 'Margin Sağ',
'margin-left': 'Margin Sol',
'margin-bottom': 'Margin Alt',
padding: 'Padding',
'padding-top': 'Padding Üst',
'padding-left': 'Padding Sol',
'padding-right': 'Padding Sağ',
'padding-bottom': 'Padding Alt',
'font-family': 'Font Tipi',
'font-size': 'Font Boyutu',
'font-weight': 'Font Kalınlığı',
'letter-spacing': 'Harf Boşluğu',
color: 'Renk',
'line-height': 'Satır Boşluğu',
'text-align': 'Yazı Hizalaması',
'text-shadow': 'Yazı Gölgesi',
'text-shadow-h': 'Yazı Gölgesi - Yatay',
'text-shadow-v': 'Yazı Gölgesi - Dikey',
'text-shadow-blur': 'Yazı Gölgesi Bulanıklığı',
'text-shadow-color': 'Yazı Gölgesi Rengi',
'border-top-left': 'Kenar Üst Sol',
'border-top-right': 'Kenar Üst Sağ',
'border-bottom-left': 'Kenar Alt Sol',
'border-bottom-right': 'Kenar Alt Sağ',
'border-radius-top-left': 'Köşe Yumuşuması Üst Sol',
'border-radius-top-right': 'Köşe Yumuşuması Üst Sağ',
'border-radius-bottom-left': 'Köşe Yumuşuması Alt Sol',
'border-radius-bottom-right': 'Köşe Yumuşuması Alt Sağ',
'border-radius': 'Köşe Yumuşaması',
border: 'Kenar',
'border-width': 'Kenar Kalınlığı',
'border-style': 'Kenar Stili',
'border-color': 'Kenar Rengi',
'box-shadow': 'Kutu Gölgesi',
'box-shadow-h': 'Kutu Gölgesi - Yatay',
'box-shadow-v': 'Kutu Gölgesi - Dikey',
'box-shadow-blur': 'Kutu Gölgesi Bulanıklığı',
'box-shadow-spread': 'Kutu Gölgesi Dağılımı',
'box-shadow-color': 'Kutu Gölgesi Rengi',
'box-shadow-type': 'Kutu Gölgesi Tipi',
background: 'Arkaplan',
'background-image': 'Arkaplan Resmi',
'background-repeat': 'Arkaplan Tekrarı',
'background-position': 'Arkaplan Pozisyonu',
'background-attachment': 'Arkaplan Eklentisi',
'background-size': 'Arkaplan Boyutu',
transition: 'Geçiş',
'transition-property': 'Geçiş Özelliği',
'transition-duration': 'Geçiş Süresi',
'transition-timing-function': 'Geçiş Zamanlaması Metodu',
perspective: 'Perspektif',
transform: 'Boyutlama',
'transform-rotate-x': 'Yatay Yönlendirme',
'transform-rotate-y': 'Dikey Yönlendirme',
'transform-rotate-z': 'Hacimsel Yönlendirme',
'transform-scale-x': 'Dikey Oran',
'transform-scale-y': 'Yatay Oran',
'transform-scale-z': 'Hacimsel Oran',
'flex-direction': 'Flex Yönü',
'flex-wrap': 'Flex Kesme',
'justify-content': 'İçeriği Sığdır',
'align-items': 'Öğeleri Hizala',
'align-content': 'İçeriği Hizala',
order: 'Sıra',
'flex-basis': 'Flex Bazı',
'flex-grow': 'Flex Büyüme',
'flex-shrink': 'Flex Küçülme',
'align-self': 'Kendini Hizala',
'background-color': 'Arkaplan Rengi'
}
},
traitManager: {
empty: 'Özelliklerini düzenlemek istediğiniz öğeyi seçiniz',
label: 'Bileşen Özellikleri',
traits: {
labels: {},
attributes: {},
options: {}
}
}
};

5
src/keymaps/messages.js

@ -0,0 +1,5 @@
export default {
en: {
hello: 'Hello'
}
};

23
src/navigator/view/ItemView.js

@ -51,7 +51,7 @@ export default Backbone.View.extend({
</div>
</div>
</div>
<div class="${this.clsCount}">${count || ''}</div>
<div class="${this.clsCount}" data-count>${count || ''}</div>
<div class="${this.clsMove}" data-toggle-move>
<i class="fa fa-arrows"></i>
</div>
@ -295,26 +295,21 @@ export default Backbone.View.extend({
* @return void
* */
checkChildren() {
const model = this.model;
const { model, clsNoChild } = this;
const count = this.countChildren(model);
const pfx = this.pfx;
const noChildCls = this.clsNoChild;
const title = this.$el
.children(`.${this.clsTitleC}`)
.children(`.${this.clsTitle}`);
let { cnt } = this;
if (!this.cnt) {
this.cnt = this.$el.children(`.${this.clsCount}`);
if (!cnt) {
cnt = this.$el.children('[data-count]').get(0);
this.cnt = cnt;
}
if (count) {
title.removeClass(noChildCls);
this.cnt.html(count);
} else {
title.addClass(noChildCls);
this.cnt.empty();
model.set('open', 0);
}
title[count ? 'removeClass' : 'addClass'](clsNoChild);
if (cnt) cnt.innerHTML = count || '';
!count && model.set('open', 0);
},
/**

16
src/panels/config/config.js

@ -1,11 +1,11 @@
var swv = 'sw-visibility';
var expt = 'export-template';
var osm = 'open-sm';
var otm = 'open-tm';
var ola = 'open-layers';
var obl = 'open-blocks';
var ful = 'fullscreen';
var prv = 'preview';
const swv = 'sw-visibility';
const expt = 'export-template';
const osm = 'open-sm';
const otm = 'open-tm';
const ola = 'open-layers';
const obl = 'open-blocks';
const ful = 'fullscreen';
const prv = 'preview';
export default {
stylePrefix: 'pn-',

7
src/panels/view/ButtonView.js

@ -55,7 +55,12 @@ export default Backbone.View.extend({
* @return void
* */
updateAttributes() {
this.$el.attr(this.model.get('attributes'));
const { em, model, $el } = this;
const attr = model.get('attributes') || {};
const title = em && em.t && em.t(`panels.buttons.titles.${model.id}`);
$el.attr(attr);
title && $el.attr({ title });
this.updateClassName();
},

14
src/selector_manager/config/config.js

@ -9,20 +9,8 @@ export default {
// Default selectors
selectors: [],
// Label for selectors
label: 'Classes',
// Label for states
statesLabel: '- State -',
selectedLabel: 'Selected',
// States
states: [
{ name: 'hover', label: 'Hover' },
{ name: 'active', label: 'Click' },
{ name: 'nth-of-type(2n)', label: 'Even/Odd' }
],
states: [{ name: 'hover' }, { name: 'active' }, { name: 'nth-of-type(2n)' }],
// Custom selector name escaping strategy, eg.
// name => name.replace(' ', '_')

6
src/selector_manager/view/ClassTagView.js

@ -4,9 +4,9 @@ const inputProp = 'contentEditable';
export default Backbone.View.extend({
template() {
const pfx = this.pfx;
const ppfx = this.ppfx;
const label = this.model.get('label') || '';
const { pfx, model } = this;
const label = model.get('label') || '';
return `
<span id="${pfx}checkbox" class="fa" data-tag-status></span>
<span id="${pfx}tag-label" data-tag-name>${label}</span>

40
src/selector_manager/view/ClassTagsView.js

@ -90,16 +90,20 @@ export default Backbone.View.extend({
* @private
*/
getStateOptions() {
var strInput = '';
for (var i = 0; i < this.states.length; i++) {
strInput +=
'<option value="' +
this.states[i].name +
'">' +
this.states[i].label +
'</option>';
}
return strInput;
const { states, em } = this;
let result = [];
states.forEach(state =>
result.push(
`<option value="${state.name}">${em.t(
`selectorManager.states.${state.name}`
) ||
state.label ||
state.name}</option>`
)
);
return result.join('');
},
/**
@ -304,21 +308,19 @@ export default Backbone.View.extend({
},
render() {
const ppfx = this.ppfx;
const config = this.config;
const $el = this.$el;
const { em, pfx, ppfx, $el } = this;
$el.html(
this.template({
selectedLabel: config.selectedLabel,
statesLabel: config.statesLabel,
label: config.label,
pfx: this.pfx,
ppfx: this.ppfx
selectedLabel: em.t('selectorManager.selected'),
statesLabel: em.t('selectorManager.emptyState'),
label: em.t('selectorManager.label'),
pfx,
ppfx
})
);
this.$input = $el.find('input#' + this.newInputId);
this.$addBtn = $el.find('#' + this.addBtnId);
this.$classes = $el.find('#' + this.pfx + 'tags-c');
this.$classes = $el.find('#' + pfx + 'tags-c');
this.$states = $el.find('#' + this.stateInputId);
this.$statesC = $el.find('#' + this.stateInputC);
this.$states.append(this.getStateOptions());

3
src/style_manager/config/config.js

@ -7,9 +7,6 @@ export default {
// With the empty value, nothing will be rendered
appendTo: '',
// Text to show in case no element selected
textNoElement: 'Select an element before using Style Manager',
// Hide the property in case it's not stylable for the
// selected component (each component has 'stylable' property)
hideNotStylable: true,

4
src/style_manager/model/PropertyInteger.js

@ -21,7 +21,8 @@ export default Property.extend({
max: ''
},
init() {
initialize(props = {}, opts = {}) {
Property.callParentInit(Property, this, props, opts);
const unit = this.get('unit');
const units = this.get('units');
this.input = new InputNumber({ model: this });
@ -29,6 +30,7 @@ export default Property.extend({
if (units.length && !unit) {
this.set('unit', units[0]);
}
Property.callInit(this, props, opts);
},
clearValue(opts = {}) {

5
src/style_manager/view/LayerView.js

@ -10,8 +10,8 @@ export default Backbone.View.extend({
},
template(model) {
const { pfx, ppfx } = this;
const label = `Layer ${model.get('index')}`;
const { pfx, ppfx, em } = this;
const label = `${em && em.t('styleManager.layer')} ${model.get('index')}`;
return `
<div id="${pfx}move" class="${ppfx}no-touch-actions" data-move-layer>
@ -33,6 +33,7 @@ export default Backbone.View.extend({
let model = this.model;
this.stackModel = o.stackModel || {};
this.config = o.config || {};
this.em = this.config.em;
this.pfx = this.config.stylePrefix || '';
this.ppfx = this.config.pStylePrefix || '';
this.sorter = o.sorter || null;

7
src/style_manager/view/PropertyFileView.js

@ -6,15 +6,14 @@ const $ = Backbone.$;
export default PropertyView.extend({
templateInput() {
const pfx = this.pfx;
const ppfx = this.ppfx;
const assetsLabel = this.config.assetsLabel || 'Images';
const { pfx, em } = this;
return `
<div class="${pfx}field ${pfx}file">
<div id='${pfx}input-holder'>
<div class="${pfx}btn-c">
<button class="${pfx}btn" id="${pfx}images" type="button">
${assetsLabel}
${em.t('styleManager.fileButton')}
</button>
</div>
<div style="clear:both;"></div>

10
src/style_manager/view/PropertyView.js

@ -19,14 +19,14 @@ export default Backbone.View.extend({
},
templateLabel(model) {
const pfx = this.pfx;
const icon = model.get('icon');
const info = model.get('info');
const parent = model.parent;
const { pfx, em } = this;
const { parent } = model;
const { icon = '', info = '', id, name } = model.attributes;
const label = (em && em.t(`styleManager.properties.${id}`)) || name;
return `
<span class="${pfx}icon ${icon}" title="${info}">
${model.get('name')}
${label}
</span>
${!parent ? `<b class="${pfx}clear" ${clearProp}>&Cross;</b>` : ''}
`;

17
src/style_manager/view/SectorView.js

@ -15,6 +15,7 @@ export default Backbone.View.extend({
initialize(o) {
this.config = o.config || {};
this.em = this.config.em;
this.pfx = this.config.stylePrefix || '';
this.target = o.target || {};
this.propTarget = o.propTarget || {};
@ -78,17 +79,13 @@ export default Backbone.View.extend({
},
render() {
const { pfx, model } = this;
const { id } = model.attributes;
this.$el.html(
this.template({
pfx,
label: model.get('name')
})
);
this.$caret = this.$el.find(`#${pfx}caret`);
const { pfx, model, em, $el } = this;
const { id, name } = model.attributes;
const label = (em && em.t(`styleManager.sectors.${id}`)) || name;
$el.html(this.template({ pfx, label }));
this.$caret = $el.find(`#${pfx}caret`);
this.renderProperties();
this.$el.attr('class', `${pfx}sector ${pfx}sector__${id} no-select`);
$el.attr('class', `${pfx}sector ${pfx}sector__${id} no-select`);
this.updateOpen();
return this;
},

BIN
src/styles/fonts/FontAwesome.otf

Binary file not shown.

BIN
src/styles/fonts/fontawesome-webfont.eot

Binary file not shown.

2671
src/styles/fonts/fontawesome-webfont.svg

File diff suppressed because it is too large

Before

Width:  |  Height:  |  Size: 434 KiB

BIN
src/styles/fonts/fontawesome-webfont.ttf

Binary file not shown.

BIN
src/styles/fonts/fontawesome-webfont.woff

Binary file not shown.

BIN
src/styles/fonts/fontawesome-webfont.woff2

Binary file not shown.

1
src/styles/scss/main.scss

@ -1,6 +1,5 @@
/* stylelint-disable */
@import "node_modules/spectrum-colorpicker/spectrum";
@import "node_modules/font-awesome/scss/font-awesome";
@import "node_modules/codemirror/lib/codemirror";
@import "node_modules/codemirror/theme/hopscotch";

16
src/trait_manager/config/config.js

@ -5,20 +5,6 @@ export default {
// With the empty value, nothing will be rendered
appendTo: '',
labelContainer: 'Component settings',
// Placeholder label for text input types
labelPlhText: 'eg. Text here',
// Placeholder label for href input
labelPlhHref: 'eg. https://google.com',
// Default options for the target input
optionsTarget: [
{ value: '', name: 'This window' },
{ value: '_blank', name: 'New window' }
],
// Text to show in case no element selected
textNoElement: 'Select an element before using Trait Manager'
optionsTarget: [{ value: false }, { value: '_blank' }]
};

11
src/trait_manager/model/Trait.js

@ -64,12 +64,19 @@ export default Backbone.Model.extend({
const target = this.target;
const name = this.get('name');
if (isUndefined(value)) return;
let valueToSet = value;
if (value === 'false') {
valueToSet = false;
} else if (value === 'true') {
valueToSet = true;
}
if (this.get('changeProp')) {
target.set(name, value, opts);
target.set(name, valueToSet, opts);
} else {
const attrs = { ...target.get('attributes') };
attrs[name] = value;
attrs[name] = valueToSet;
target.set('attributes', attrs, opts);
}
},

12
src/trait_manager/model/TraitFactory.js

@ -21,18 +21,6 @@ export default (config = {}) => ({
break;
}
// Define placeholder
switch (prop) {
case 'title':
case 'alt':
case 'id':
obj.placeholder = config.labelPlhText;
break;
case 'href':
obj.placeholder = config.labelPlhHref;
break;
}
// Define options
switch (prop) {
case 'target':

10
src/trait_manager/view/TraitSelectView.js

@ -26,7 +26,8 @@ export default TraitView.extend({
*/
getInputEl() {
if (!this.$input) {
const { model } = this;
const { model, em } = this;
const propName = model.get('name');
const opts = model.get('options') || [];
let input = '<select>';
@ -46,13 +47,14 @@ export default TraitView.extend({
style = el.style ? el.style.replace(/"/g, '&quot;') : '';
attrs += style ? ` style="${style}"` : '';
}
input += `<option value="${value}"${attrs}>${name}</option>`;
const resultName =
em.t(`traitManager.traits.options.${propName}.${value}`) || name;
input += `<option value="${value}"${attrs}>${resultName}</option>`;
});
input += '</select>';
this.$input = $(input);
let val = model.getTargetValue() || model.get('value');
const val = model.getTargetValue();
!isUndefined(val) && this.$input.val(val);
}

12
src/trait_manager/view/TraitView.js

@ -135,8 +135,12 @@ export default Backbone.View.extend({
* @private
*/
getLabel() {
const { em } = this;
const { label, name } = this.model.attributes;
return capitalize(label || name).replace(/-/g, ' ');
return (
em.t(`traitManager.traits.labels.${name}`) ||
capitalize(label || name).replace(/-/g, ' ')
);
},
/**
@ -153,13 +157,17 @@ export default Backbone.View.extend({
*/
getInputEl() {
if (!this.$input) {
const md = this.model;
const { em, model } = this;
const md = model;
const { name } = model.attributes;
const plh = md.get('placeholder') || md.get('default') || '';
const type = md.get('type') || 'text';
const min = md.get('min');
const max = md.get('max');
const value = this.getModelValue();
const input = $(`<input type="${type}" placeholder="${plh}">`);
const i18nAttr = em.t(`traitManager.traits.attributes.${name}`) || {};
input.attr(i18nAttr);
if (!isUndefined(value)) {
md.set({ value }, { silent: true });

2
src/utils/Droppable.js

@ -34,7 +34,7 @@ export default class Droppable {
this.counter = 0;
this.over = 0;
dragStop && dragStop(cancel);
em.runDefault();
em.runDefault({ preserveSelected: 1 });
em.trigger('canvas:dragend', ev);
}

32
src/utils/mixins.js

@ -1,4 +1,4 @@
import { keys, isUndefined, isElement } from 'underscore';
import { keys, isUndefined, isElement, isArray } from 'underscore';
const elProt = window.Element.prototype;
const matches =
@ -7,6 +7,33 @@ const matches =
elProt.mozMatchesSelector ||
elProt.msMatchesSelector;
/**
* Import styles asynchronously
* @param {String|Array<String>} styles
*/
const appendStyles = (styles, opts = {}) => {
const stls = isArray(styles) ? [...styles] : [styles];
if (stls.length) {
const href = stls.shift();
if (!opts.unique || !document.querySelector(`link[href="${href}"]`)) {
const { head } = document;
const link = document.createElement('link');
link.href = href;
link.rel = 'stylesheet';
if (opts.prepand) {
head.insertBefore(link, head.firstChild);
} else {
head.appendChild(link);
}
}
appendStyles(stls);
}
};
/**
* Returns shallow diff between 2 objects
* @param {Object} objOrig
@ -201,5 +228,6 @@ export {
normalizeFloat,
getPointerEvent,
getUnitFromValue,
capitalize
capitalize,
appendStyles
};

28
test/main.test.js

@ -1,28 +0,0 @@
import grapesjs from './../src';
describe('Main', () => {
describe('Startup', () => {
test('Main object should be loaded', () => {
expect(grapesjs).toBeTruthy();
});
});
const path = './specs/';
require(`${path}keymaps`);
require(`${path}asset_manager`);
require(`${path}block_manager`);
require(`${path}code_manager`);
require(`${path}commands`);
require(`${path}css_composer`);
require(`${path}device_manager`);
require(`${path}dom_components`);
require(`${path}modal`);
require(`${path}panels`);
require(`${path}parser`);
require(`${path}plugin_manager`);
require(`${path}selector_manager`);
require(`${path}storage_manager`);
require(`${path}style_manager`);
require(`${path}trait_manager`);
require(`${path}grapesjs`);
});

9
test/specs/asset_manager/index.js

@ -114,13 +114,4 @@ describe('Asset Manager', () => {
});
});
});
require('./model/Asset').run();
require('./model/AssetImage').run();
require('./model/Assets').run();
require('./view/AssetView').run();
require('./view/AssetImageView').run();
require('./view/AssetsView').run();
require('./view/FileUploader').run();
});

52
test/specs/asset_manager/model/Asset.js

@ -1,33 +1,29 @@
import Asset from 'asset_manager/model/Asset';
module.exports = {
run() {
describe('Asset', () => {
test('Object exists', () => {
expect(Asset).toBeTruthy();
});
describe('Asset', () => {
test('Object exists', () => {
expect(Asset).toBeTruthy();
});
test('Has default values', () => {
var obj = new Asset({});
expect(obj.get('type')).toBeFalsy();
expect(obj.get('src')).toBeFalsy();
expect(obj.getExtension()).toBeFalsy();
expect(obj.getFilename()).toBeFalsy();
});
test('Has default values', () => {
var obj = new Asset({});
expect(obj.get('type')).toBeFalsy();
expect(obj.get('src')).toBeFalsy();
expect(obj.getExtension()).toBeFalsy();
expect(obj.getFilename()).toBeFalsy();
});
test('Test getFilename', () => {
var obj = new Asset({ type: 'image', src: 'ch/eck/t.e.s.t' });
expect(obj.getFilename()).toEqual('t.e.s.t');
var obj = new Asset({ type: 'image', src: 'ch/eck/1234abc' });
expect(obj.getFilename()).toEqual('1234abc');
});
test('Test getFilename', () => {
var obj = new Asset({ type: 'image', src: 'ch/eck/t.e.s.t' });
expect(obj.getFilename()).toEqual('t.e.s.t');
var obj = new Asset({ type: 'image', src: 'ch/eck/1234abc' });
expect(obj.getFilename()).toEqual('1234abc');
});
test('Test getExtension', () => {
var obj = new Asset({ type: 'image', src: 'ch/eck/t.e.s.t' });
expect(obj.getExtension()).toEqual('t');
var obj = new Asset({ type: 'image', src: 'ch/eck/1234abc.' });
expect(obj.getExtension()).toEqual('');
});
});
}
};
test('Test getExtension', () => {
var obj = new Asset({ type: 'image', src: 'ch/eck/t.e.s.t' });
expect(obj.getExtension()).toEqual('t');
var obj = new Asset({ type: 'image', src: 'ch/eck/1234abc.' });
expect(obj.getExtension()).toEqual('');
});
});

34
test/specs/asset_manager/model/AssetImage.js

@ -1,22 +1,18 @@
import AssetImage from 'asset_manager/model/AssetImage';
module.exports = {
run() {
describe('AssetImage', () => {
test('Object exists', () => {
expect(AssetImage).toBeTruthy();
});
describe('AssetImage', () => {
test('Object exists', () => {
expect(AssetImage).toBeTruthy();
});
test('Has default values', () => {
var obj = new AssetImage({});
expect(obj.get('type')).toEqual('image');
expect(obj.get('src')).toBeFalsy();
expect(obj.get('unitDim')).toEqual('px');
expect(obj.get('height')).toEqual(0);
expect(obj.get('width')).toEqual(0);
expect(obj.getExtension()).toBeFalsy();
expect(obj.getFilename()).toBeFalsy();
});
});
}
};
test('Has default values', () => {
var obj = new AssetImage({});
expect(obj.get('type')).toEqual('image');
expect(obj.get('src')).toBeFalsy();
expect(obj.get('unitDim')).toEqual('px');
expect(obj.get('height')).toEqual(0);
expect(obj.get('width')).toEqual(0);
expect(obj.getExtension()).toBeFalsy();
expect(obj.getFilename()).toBeFalsy();
});
});

34
test/specs/asset_manager/model/Assets.js

@ -1,25 +1,21 @@
import Assets from 'asset_manager/model/Assets';
module.exports = {
run() {
describe('Assets', () => {
var obj;
describe('Assets', () => {
var obj;
beforeEach(() => {
obj = new Assets();
});
beforeEach(() => {
obj = new Assets();
});
afterEach(() => {
obj = null;
});
afterEach(() => {
obj = null;
});
test('Object exists', () => {
expect(obj).toBeTruthy();
});
test('Object exists', () => {
expect(obj).toBeTruthy();
});
test('Collection is empty', () => {
expect(obj.length).toEqual(0);
});
});
}
};
test('Collection is empty', () => {
expect(obj.length).toEqual(0);
});
});

112
test/specs/asset_manager/view/AssetImageView.js

@ -1,70 +1,66 @@
import AssetImageView from 'asset_manager/view/AssetImageView';
import Assets from 'asset_manager/model/Assets';
module.exports = {
run() {
let obj;
let obj;
describe('AssetImageView', () => {
beforeEach(() => {
var coll = new Assets();
var model = coll.add({ type: 'image', src: '/test' });
obj = new AssetImageView({
collection: new Assets(),
config: {},
model
});
document.body.innerHTML = '<div id="fixtures"></div>';
document.body.querySelector('#fixtures').appendChild(obj.render().el);
});
describe('AssetImageView', () => {
beforeEach(() => {
var coll = new Assets();
var model = coll.add({ type: 'image', src: '/test' });
obj = new AssetImageView({
collection: new Assets(),
config: {},
model
});
document.body.innerHTML = '<div id="fixtures"></div>';
document.body.querySelector('#fixtures').appendChild(obj.render().el);
});
afterEach(() => {
obj = null;
document.body.innerHTML = '';
});
afterEach(() => {
obj = null;
document.body.innerHTML = '';
});
test('Object exists', () => {
expect(AssetImageView).toBeTruthy();
});
test('Object exists', () => {
expect(AssetImageView).toBeTruthy();
});
describe('Asset should be rendered correctly', () => {
test('Has preview box', () => {
var $asset = obj.$el;
expect($asset.find('.preview').length).toEqual(1);
});
describe('Asset should be rendered correctly', () => {
test('Has preview box', () => {
var $asset = obj.$el;
expect($asset.find('.preview').length).toEqual(1);
});
test('Has meta box', () => {
var $asset = obj.$el;
expect($asset.find('.meta').length).toEqual(1);
});
test('Has meta box', () => {
var $asset = obj.$el;
expect($asset.find('.meta').length).toEqual(1);
});
test('Has close button', () => {
var $asset = obj.$el;
expect($asset.find('[data-toggle=asset-remove]').length).toEqual(1);
});
});
test('Has close button', () => {
var $asset = obj.$el;
expect($asset.find('[data-toggle=asset-remove]').length).toEqual(1);
});
});
test('Could be selected', () => {
var spy = jest.spyOn(obj, 'updateTarget');
obj.$el.trigger('click');
expect(obj.$el.attr('class')).toContain('highlight');
expect(spy).toHaveBeenCalled();
});
test('Could be selected', () => {
var spy = jest.spyOn(obj, 'updateTarget');
obj.$el.trigger('click');
expect(obj.$el.attr('class')).toContain('highlight');
expect(spy).toHaveBeenCalled();
});
test('Could be chosen', () => {
sinon.stub(obj, 'updateTarget');
var spy = jest.spyOn(obj, 'updateTarget');
obj.$el.trigger('dblclick');
expect(spy).toHaveBeenCalled();
//obj.updateTarget.calledOnce.should.equal(true);
});
test('Could be chosen', () => {
sinon.stub(obj, 'updateTarget');
var spy = jest.spyOn(obj, 'updateTarget');
obj.$el.trigger('dblclick');
expect(spy).toHaveBeenCalled();
//obj.updateTarget.calledOnce.should.equal(true);
});
test('Could be removed', () => {
var spy = sinon.spy();
obj.model.on('remove', spy);
obj.onRemove({ stopImmediatePropagation() {} });
expect(spy.called).toEqual(true);
});
});
}
};
test('Could be removed', () => {
var spy = sinon.spy();
obj.model.on('remove', spy);
obj.onRemove({ stopImmediatePropagation() {} });
expect(spy.called).toEqual(true);
});
});

58
test/specs/asset_manager/view/AssetView.js

@ -1,39 +1,35 @@
import Assets from 'asset_manager/model/Assets';
import AssetView from 'asset_manager/view/AssetView';
module.exports = {
run() {
describe('AssetView', () => {
let testContext;
describe('AssetView', () => {
let testContext;
beforeEach(() => {
testContext = {};
});
beforeEach(() => {
testContext = {};
});
beforeEach(() => {
var coll = new Assets();
var model = coll.add({ src: 'test' });
testContext.view = new AssetView({
config: {},
model
});
document.body.innerHTML = '<div id="fixtures"></div>';
document.body
.querySelector('#fixtures')
.appendChild(testContext.view.render().el);
});
beforeEach(() => {
var coll = new Assets();
var model = coll.add({ src: 'test' });
testContext.view = new AssetView({
config: {},
model
});
document.body.innerHTML = '<div id="fixtures"></div>';
document.body
.querySelector('#fixtures')
.appendChild(testContext.view.render().el);
});
afterEach(() => {
testContext.view.remove();
});
afterEach(() => {
testContext.view.remove();
});
test('Object exists', () => {
expect(AssetView).toBeTruthy();
});
test('Object exists', () => {
expect(AssetView).toBeTruthy();
});
test('Has correct prefix', () => {
expect(testContext.view.pfx).toEqual('');
});
});
}
};
test('Has correct prefix', () => {
expect(testContext.view.pfx).toEqual('');
});
});

136
test/specs/asset_manager/view/AssetsView.js

@ -2,86 +2,82 @@ import AssetsView from 'asset_manager/view/AssetsView';
import FileUploader from 'asset_manager/view/FileUploader';
import Assets from 'asset_manager/model/Assets';
module.exports = {
run() {
describe('AssetsView', () => {
var obj;
var coll;
describe('AssetsView', () => {
var obj;
var coll;
beforeEach(() => {
coll = new Assets([]);
obj = new AssetsView({
config: {},
collection: coll,
globalCollection: new Assets([]),
fu: new FileUploader({})
});
obj = obj;
document.body.innerHTML = '<div id="fixtures"></div>';
obj.render();
document.body.querySelector('#fixtures').appendChild(obj.el);
});
beforeEach(() => {
coll = new Assets([]);
obj = new AssetsView({
config: {},
collection: coll,
globalCollection: new Assets([]),
fu: new FileUploader({})
});
obj = obj;
document.body.innerHTML = '<div id="fixtures"></div>';
obj.render();
document.body.querySelector('#fixtures').appendChild(obj.el);
});
afterEach(() => {
obj.collection.reset();
});
afterEach(() => {
obj.collection.reset();
});
test('Object exists', () => {
expect(AssetsView).toBeTruthy();
});
test('Object exists', () => {
expect(AssetsView).toBeTruthy();
});
test('Collection is empty', () => {
expect(obj.getAssetsEl().innerHTML).toBeFalsy();
});
test('Collection is empty', () => {
expect(obj.getAssetsEl().innerHTML).toBeFalsy();
});
test('Add new asset', () => {
sinon.stub(obj, 'addAsset');
coll.add({ src: 'test' });
expect(obj.addAsset.calledOnce).toEqual(true);
});
test('Add new asset', () => {
sinon.stub(obj, 'addAsset');
coll.add({ src: 'test' });
expect(obj.addAsset.calledOnce).toEqual(true);
});
test('Render new asset', () => {
coll.add({ src: 'test' });
expect(obj.getAssetsEl().innerHTML).toBeTruthy();
});
test('Render new asset', () => {
coll.add({ src: 'test' });
expect(obj.getAssetsEl().innerHTML).toBeTruthy();
});
test('Render correctly new image asset', () => {
coll.add({ type: 'image', src: 'test' });
var asset = obj.getAssetsEl().firstChild;
expect(asset.tagName).toEqual('DIV');
expect(asset.innerHTML).toBeTruthy();
});
test('Render correctly new image asset', () => {
coll.add({ type: 'image', src: 'test' });
var asset = obj.getAssetsEl().firstChild;
expect(asset.tagName).toEqual('DIV');
expect(asset.innerHTML).toBeTruthy();
});
test('Clean collection from asset', () => {
var model = coll.add({ src: 'test' });
coll.remove(model);
expect(obj.getAssetsEl().innerHTML).toBeFalsy();
});
test('Clean collection from asset', () => {
var model = coll.add({ src: 'test' });
coll.remove(model);
expect(obj.getAssetsEl().innerHTML).toBeFalsy();
});
test('Deselect works', () => {
coll.add([{}, {}]);
var $asset = obj.$el.children().first();
$asset.attr('class', obj.pfx + 'highlight');
coll.trigger('deselectAll');
expect($asset.attr('class')).toBeFalsy();
});
test('Deselect works', () => {
coll.add([{}, {}]);
var $asset = obj.$el.children().first();
$asset.attr('class', obj.pfx + 'highlight');
coll.trigger('deselectAll');
expect($asset.attr('class')).toBeFalsy();
});
test('Returns not empty assets element', () => {
expect(obj.getAssetsEl()).toBeTruthy();
});
test('Returns not empty assets element', () => {
expect(obj.getAssetsEl()).toBeTruthy();
});
test('Returns not empty url input', () => {
expect(obj.getAddInput()).toBeTruthy();
});
test('Returns not empty url input', () => {
expect(obj.getAddInput()).toBeTruthy();
});
test('Add image asset from input string', () => {
obj.getAddInput().value = 'test';
obj.handleSubmit({
preventDefault() {}
});
var asset = obj.options.globalCollection.at(0);
expect(asset.get('src')).toEqual('test');
});
test('Add image asset from input string', () => {
obj.getAddInput().value = 'test';
obj.handleSubmit({
preventDefault() {}
});
}
};
var asset = obj.options.globalCollection.at(0);
expect(asset.get('src')).toEqual('test');
});
});

142
test/specs/asset_manager/view/FileUploader.js

@ -1,97 +1,75 @@
import FileUploader from 'asset_manager/view/FileUploader';
module.exports = {
run() {
describe('File Uploader', () => {
let obj;
describe('File Uploader', () => {
let obj;
beforeEach(() => {
obj = new FileUploader({ config: {} });
document.body.innerHTML = '<div id="fixtures"></div>';
document.body.querySelector('#fixtures').appendChild(obj.render().el);
});
beforeEach(() => {
obj = new FileUploader({ config: {} });
document.body.innerHTML = '<div id="fixtures"></div>';
document.body.querySelector('#fixtures').appendChild(obj.render().el);
});
afterEach(() => {
obj.remove();
});
afterEach(() => {
obj.remove();
});
test('Object exists', () => {
expect(FileUploader).toBeTruthy();
});
test('Object exists', () => {
expect(FileUploader).toBeTruthy();
});
test('Has correct prefix', () => {
expect(obj.pfx).toBeFalsy();
});
test('Has correct prefix', () => {
expect(obj.pfx).toBeFalsy();
});
describe('Should be rendered correctly', () => {
test('Has title', () => {
expect(obj.$el.find('#title').length).toEqual(1);
});
test('Title is empty', () => {
expect(obj.$el.find('#title').html()).toEqual('');
});
describe('Should be rendered correctly', () => {
test('Has title', () => {
expect(obj.$el.find('#title').length).toEqual(1);
});
test('Has file input', () => {
expect(obj.$el.find('input[type=file]').length).toEqual(1);
});
test('Title is empty', () => {
expect(obj.$el.find('#title').html()).toEqual('');
});
test('File input is enabled', () => {
expect(obj.$el.find('input[type=file]').prop('disabled')).toEqual(
true
);
});
});
test('Has file input', () => {
expect(obj.$el.find('input[type=file]').length).toEqual(1);
});
describe('Interprets configurations correctly', () => {
test('Has correct title', () => {
var view = new FileUploader({
config: {
uploadText: 'Test'
}
});
view.render();
expect(view.$el.find('#title').html()).toEqual('Test');
});
test('File input is enabled', () => {
expect(obj.$el.find('input[type=file]').prop('disabled')).toEqual(true);
});
});
test('Could be disabled', () => {
var view = new FileUploader({
config: {
disableUpload: true,
upload: 'something'
}
});
view.render();
expect(view.$el.find('input[type=file]').prop('disabled')).toEqual(
true
);
});
describe('Interprets configurations correctly', () => {
test('Could be disabled', () => {
var view = new FileUploader({
config: {
disableUpload: true,
upload: 'something'
}
});
view.render();
expect(view.$el.find('input[type=file]').prop('disabled')).toEqual(true);
});
test('Handles multiUpload false', () => {
var view = new FileUploader({
config: {
multiUpload: false
}
});
view.render();
expect(
view.$el.find('input[type=file]').prop('multiple')
).toBeFalsy();
});
test('Handles multiUpload false', () => {
var view = new FileUploader({
config: {
multiUpload: false
}
});
view.render();
expect(view.$el.find('input[type=file]').prop('multiple')).toBeFalsy();
});
test('Handles embedAsBase64 parameter', () => {
var view = new FileUploader({
config: {
embedAsBase64: true
}
});
view.render();
expect(view.$el.find('input[type=file]').prop('disabled')).toEqual(
false
);
expect(view.uploadFile).toEqual(FileUploader.embedAsBase64);
});
test('Handles embedAsBase64 parameter', () => {
var view = new FileUploader({
config: {
embedAsBase64: true
}
});
view.render();
expect(view.$el.find('input[type=file]').prop('disabled')).toEqual(false);
expect(view.uploadFile).toEqual(FileUploader.embedAsBase64);
});
}
};
});
});

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

Loading…
Cancel
Save