mirror of https://github.com/artf/grapesjs.git
158 changed files with 12588 additions and 15478 deletions
@ -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 |
|||
File diff suppressed because one or more lines are too long
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 434 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 8.9 KiB |
|
After Width: | Height: | Size: 6.7 KiB |
@ -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 |
|||
@ -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) |
|||
File diff suppressed because it is too large
@ -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> |
|||
File diff suppressed because it is too large
@ -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); |
|||
@ -1,5 +1,3 @@ |
|||
export default { |
|||
devices: [], |
|||
|
|||
deviceLabel: 'Device' |
|||
devices: [] |
|||
}; |
|||
|
|||
@ -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
|
|||
}); |
|||
|
|||
@ -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 |
|||
} |
|||
}; |
|||
@ -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); |
|||
} |
|||
}; |
|||
}; |
|||
@ -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' |
|||
} |
|||
} |
|||
} |
|||
} |
|||
}; |
|||
@ -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' |
|||
} |
|||
} |
|||
} |
|||
} |
|||
}; |
|||
@ -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: {} |
|||
} |
|||
} |
|||
}; |
|||
@ -0,0 +1,5 @@ |
|||
export default { |
|||
en: { |
|||
hello: 'Hello' |
|||
} |
|||
}; |
|||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 434 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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`); |
|||
}); |
|||
@ -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(''); |
|||
}); |
|||
}); |
|||
|
|||
@ -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(); |
|||
}); |
|||
}); |
|||
|
|||
@ -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); |
|||
}); |
|||
}); |
|||
|
|||
@ -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); |
|||
}); |
|||
}); |
|||
|
|||
@ -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(''); |
|||
}); |
|||
}); |
|||
|
|||
@ -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…
Reference in new issue