mirror of https://github.com/artf/grapesjs.git
122 changed files with 15561 additions and 10830 deletions
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
|
After Width: | Height: | Size: 26 KiB |
@ -0,0 +1,306 @@ |
|||||
|
--- |
||||
|
title: Symbols |
||||
|
--- |
||||
|
# Symbols |
||||
|
|
||||
|
::: warning |
||||
|
This feature is released as a beta from GrapesJS v0.21.11 |
||||
|
|
||||
|
|
||||
|
To get a better understanding of the content in this guide we recommend reading [Components] first |
||||
|
::: |
||||
|
|
||||
|
Symbols are a special type of [Component] that allows you to easily reuse common elements across your project. They are particularly useful for components that appear multiple times in your project and need to remain consistent. By using Symbols, you can easily update these components in one place and have the changes reflected everywhere they are used. |
||||
|
|
||||
|
[[toc]] |
||||
|
|
||||
|
|
||||
|
## Concept |
||||
|
|
||||
|
A Symbol created from a component retains the same shape and uses the same [Components API], but it includes a reference to other related Symbols. When you create a new Symbol from a Component, it creates a Main Symbol, and the original component becomes an Instance Symbol. |
||||
|
|
||||
|
When you reuse the Symbol elsewhere, it creates new Instance Symbols. Any updates made to the Main Symbol are automatically replicated in all Instance Symbols, ensuring consistency throughout your project. |
||||
|
|
||||
|
Below is a simple representation of the connection between Main and Instance Symbols. |
||||
|
|
||||
|
<img :src="$withBase('/symbols-model.svg')"> |
||||
|
|
||||
|
::: warning Note |
||||
|
This feature operates at a low level, meaning there is no built-in UI for creating and managing symbols. Developers need to implement their own UI to interact with this feature. Below you'll find an example of implementation. |
||||
|
::: |
||||
|
|
||||
|
|
||||
|
## Programmatic usage |
||||
|
|
||||
|
Let's see how to work with and manage Symbols in your project. |
||||
|
|
||||
|
|
||||
|
|
||||
|
### Create symbol |
||||
|
|
||||
|
Create a new Symbol from any component in your project: |
||||
|
|
||||
|
```js |
||||
|
const anyComponent = editor.getSelected(); |
||||
|
const symbolMain = editor.Components.addSymbol(anyComponent); |
||||
|
``` |
||||
|
|
||||
|
This will transform `anyComponent` to an Instance and the returned `symbolMain` will be the Main Symbol. GrapesJS keeps track of Main Symbols separately in your project JSON, and they will be automatically reconnected when you reload the project. |
||||
|
|
||||
|
The `addSymbol` method also handles the creation of Instances. If you call it again by passing `symbolMain` or `anyComponent`, it will create a new Instance of `symbolMain`. |
||||
|
|
||||
|
```js |
||||
|
const secondInstance = editor.Components.addSymbol(symbolMain); |
||||
|
``` |
||||
|
|
||||
|
Now, `symbolMain` references two instances of its shape. |
||||
|
|
||||
|
To get all the available Symbols in your project, use `getSymbols`: |
||||
|
|
||||
|
```js |
||||
|
const symbols = editor.Components.getSymbols(); |
||||
|
const symbolMain = symbols[0]; |
||||
|
``` |
||||
|
|
||||
|
|
||||
|
|
||||
|
### Symbol details |
||||
|
|
||||
|
Once you have Symbols in your project, you might need to know when a Component is a Symbol and get details about it. Use the `getSymbolInfo` method for this: |
||||
|
|
||||
|
```js |
||||
|
// Details from the Main Symbol |
||||
|
const symbolMainInfo = editor.Components.getSymbolInfo(symbolMain); |
||||
|
|
||||
|
symbolMainInfo.isSymbol; // true; It's a Symbol |
||||
|
symbolMainInfo.isRoot; // true; It's the root of the Symbol |
||||
|
symbolMainInfo.isMain; // true; It's the Main Symbol |
||||
|
symbolMainInfo.isInstance; // false; It's not the Instance Symbol |
||||
|
symbolMainInfo.main; // symbolMainInfo; Reference to the Main Symbol |
||||
|
symbolMainInfo.instances; // [anyComponent, secondInstance]; Reference to Instance Symbols |
||||
|
symbolMainInfo.relatives; // [anyComponent, secondInstance]; Relative Symbols |
||||
|
|
||||
|
// Details from the Instance Symbol |
||||
|
const secondInstanceInfo = editor.Components.getSymbolInfo(secondInstance); |
||||
|
|
||||
|
symbolMainInfo.isSymbol; // true; It's a Symbol |
||||
|
symbolMainInfo.isRoot; // true; It's the root of the Symbol |
||||
|
symbolMainInfo.isMain; // false; It's not the Main Symbol |
||||
|
symbolMainInfo.isInstance; // true; It's the Instance Symbol |
||||
|
symbolMainInfo.main; // symbolMainInfo; Reference to the Main Symbol |
||||
|
symbolMainInfo.instances; // [anyComponent, secondInstance]; Reference to Instance Symbols |
||||
|
symbolMainInfo.relatives; // [anyComponent, symbolMain]; Relative Symbols |
||||
|
``` |
||||
|
|
||||
|
|
||||
|
|
||||
|
### Overrides |
||||
|
|
||||
|
When you update a Symbol's properties, changes are propagated to all related Symbols. To avoid propagating specific properties, you can specify at the component level which properties to skip: |
||||
|
|
||||
|
```js |
||||
|
anyComponent.set('my-property', true); |
||||
|
secondInstance.get('my-property'); // true; change propagated |
||||
|
|
||||
|
anyComponent.setSymbolOverride(['my-property']); |
||||
|
// Get current override value: anyComponent.getSymbolOverride(); |
||||
|
|
||||
|
anyComponent.set('my-property', false); |
||||
|
secondInstance.get('my-property'); // true; change didn't propagate |
||||
|
``` |
||||
|
|
||||
|
|
||||
|
|
||||
|
### Detach symbol |
||||
|
|
||||
|
Once you have Symbol instances you might need to disconnect one to create a new custom shape with other components inside, in that case you can use `detachSymbol`. |
||||
|
|
||||
|
```js |
||||
|
editor.Components.detachSymbol(anyComponent); |
||||
|
|
||||
|
const info = editor.Components.getSymbolInfo(anyComponent); |
||||
|
info.isSymbol; // false; Not a Symbol anymore |
||||
|
|
||||
|
const infoMain = editor.Components.getSymbolInfo(symbolMain); |
||||
|
infoMain.instances; // [secondInstance]; Removed the reference |
||||
|
``` |
||||
|
|
||||
|
|
||||
|
|
||||
|
### Remove symbol |
||||
|
|
||||
|
To remove a Main Symbol and detach all related instances: |
||||
|
|
||||
|
```js |
||||
|
const symbolMain = editor.Components.getSymbols()[0]; |
||||
|
symbolMain.remove(); |
||||
|
``` |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
## Events |
||||
|
|
||||
|
The editor triggers several symbol-related events that you can leverage for your integration: |
||||
|
|
||||
|
|
||||
|
* `symbol:main:add` Added new root main symbol. |
||||
|
```js |
||||
|
editor.on('symbol:main:add', ({ component }) => { ... }); |
||||
|
``` |
||||
|
|
||||
|
* `symbol:main:update` Root main symbol updated. |
||||
|
```js |
||||
|
editor.on('symbol:main:update', ({ component }) => { ... }); |
||||
|
``` |
||||
|
|
||||
|
* `symbol:main:remove` Root main symbol removed. |
||||
|
```js |
||||
|
editor.on('symbol:main:remove', ({ component }) => { ... }); |
||||
|
``` |
||||
|
|
||||
|
* `symbol:main` Catch-all event related to root main symbol updates. |
||||
|
```js |
||||
|
editor.on('symbol:main', ({ event, component }) => { ... }); |
||||
|
``` |
||||
|
|
||||
|
* `symbol:instance:add` Added new root instance symbol. |
||||
|
```js |
||||
|
editor.on('symbol:instance:add', ({ component }) => { ... }); |
||||
|
``` |
||||
|
|
||||
|
* `symbol:instance:remove` Root instance symbol removed. |
||||
|
```js |
||||
|
editor.on('symbol:instance:remove', ({ component }) => { ... }); |
||||
|
``` |
||||
|
|
||||
|
* `symbol:instance` Catch-all event related to root instance symbol updates. |
||||
|
```js |
||||
|
editor.on('symbol:instance', ({ event, component }) => { ... }); |
||||
|
``` |
||||
|
|
||||
|
* `symbol` Catch-all event for any symbol update (main or instance). |
||||
|
```js |
||||
|
editor.on('symbol', () => { ... }); |
||||
|
``` |
||||
|
|
||||
|
|
||||
|
|
||||
|
|
||||
|
## Example |
||||
|
|
||||
|
Below is a basic UI implementation leveraging the Symbols API: |
||||
|
|
||||
|
|
||||
|
<demo-viewer value="ta19s6go" height="500" darkcode show/> |
||||
|
|
||||
|
<!-- Demo template, here for reference |
||||
|
<style> |
||||
|
.app-wrapper { |
||||
|
height: 100vh; |
||||
|
display: flex; |
||||
|
flex-direction: column; |
||||
|
} |
||||
|
.vue-app { |
||||
|
padding: 10px; |
||||
|
display: flex; |
||||
|
gap: 10px; |
||||
|
} |
||||
|
.symbols-wrp { |
||||
|
display: flex; |
||||
|
gap: 10px; |
||||
|
width: 100%; |
||||
|
padding: 10px; |
||||
|
flex-direction: column; |
||||
|
border-radius: 3px; |
||||
|
} |
||||
|
.symbols { |
||||
|
display: flex; |
||||
|
gap: 10px; |
||||
|
width: 100%; |
||||
|
} |
||||
|
.symbol { |
||||
|
cursor: pointer; |
||||
|
flex-basis: 100px; |
||||
|
text-align: left; |
||||
|
margin: 0; |
||||
|
} |
||||
|
</style> |
||||
|
|
||||
|
<div class="app-wrapper"> |
||||
|
<div class="vue-app"> |
||||
|
<button @click="createSymbol">Create Symbol</button> |
||||
|
<div class="symbols-wrp gjs-one-bg gjs-two-color"> |
||||
|
<div v-if="symbols.length">Click on symbol to append</div> |
||||
|
<div class="symbols"> |
||||
|
<div |
||||
|
v-for="symbol in symbols" |
||||
|
class="gjs-block symbol" |
||||
|
@click="createInstance(symbol)" |
||||
|
:key="symbol.getId()" |
||||
|
> |
||||
|
Name: {{ symbol.getName() }} |
||||
|
Instances: {{ getInstancesLength(symbol) }} |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div id="gjs"></div> |
||||
|
</div> |
||||
|
|
||||
|
<script> |
||||
|
const editor = grapesjs.init({ |
||||
|
container: '#gjs', |
||||
|
height: '100%', |
||||
|
storageManager: false, |
||||
|
components: `<div style="display: flex"> |
||||
|
<article class="card" style="max-width: 300px; padding: 20px"> |
||||
|
<img src="https://placehold.co/600x400/000000/FFF" style="max-width: 100%"/> |
||||
|
<h1>Title</h1> |
||||
|
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua</p> |
||||
|
</article> |
||||
|
</div>`, |
||||
|
plugins: ['gjs-blocks-basic'], |
||||
|
selectorManager: { componentFirst: true }, |
||||
|
}); |
||||
|
|
||||
|
const { Components } = editor; |
||||
|
|
||||
|
const app = new Vue({ |
||||
|
el: '.vue-app', |
||||
|
data: { symbols: [] }, |
||||
|
mounted() { |
||||
|
editor.on('symbol', this.updateMainSymbolsList); |
||||
|
}, |
||||
|
destroyed() { |
||||
|
editor.off('symbol', this.updateMainSymbolsList); |
||||
|
}, |
||||
|
methods: { |
||||
|
updateMainSymbolsList() { |
||||
|
this.symbols = Components.getSymbols(); |
||||
|
}, |
||||
|
createSymbol() { |
||||
|
const selected = editor.getSelected(); |
||||
|
if (!selected) return alert('Select a component first!'); |
||||
|
|
||||
|
const info = Components.getSymbolInfo(selected); |
||||
|
if (info.isSymbol) return alert('Selected component is already a symbol!'); |
||||
|
|
||||
|
Components.addSymbol(selected); |
||||
|
}, |
||||
|
getInstancesLength(symbolMain) { |
||||
|
return Components.getSymbolInfo(symbolMain).instances.length; |
||||
|
}, |
||||
|
createInstance(symbolMain) { |
||||
|
const instance = Components.addSymbol(symbolMain); |
||||
|
editor.getWrapper().append(instance, { at: 0 }); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
</script> |
||||
|
--> |
||||
|
|
||||
|
|
||||
|
[Component]: </modules/Components.html> |
||||
|
[Components]: </modules/Components.html> |
||||
|
[Components API]: </api/component.html> |
||||
@ -0,0 +1,95 @@ |
|||||
|
import Asset from './model/Asset'; |
||||
|
|
||||
|
export type AssetEvent = `${AssetsEvents}`; |
||||
|
|
||||
|
export interface AssetOpenOptions { |
||||
|
select?: (asset: Asset, complete: boolean) => void; |
||||
|
types?: string[]; |
||||
|
accept?: string; |
||||
|
target?: any; |
||||
|
} |
||||
|
|
||||
|
/**{START_EVENTS}*/ |
||||
|
export enum AssetsEvents { |
||||
|
/** |
||||
|
* @event `asset:add` New asset added to the collection. The [Asset] is passed as an argument to the callback. |
||||
|
* @example |
||||
|
* editor.on('asset:add', (asset) => { ... }); |
||||
|
*/ |
||||
|
add = 'asset:add', |
||||
|
|
||||
|
/** |
||||
|
* @event `asset:remove` Asset removed from the collection. The [Asset] is passed as an argument to the callback. |
||||
|
* @example |
||||
|
* editor.on('asset:remove', (asset) => { ... }); |
||||
|
*/ |
||||
|
remove = 'asset:remove', |
||||
|
removeBefore = 'asset:remove:before', |
||||
|
|
||||
|
/** |
||||
|
* @event `asset:update` Asset updated. The [Asset] and the object containing changes are passed as arguments to the callback. |
||||
|
* @example |
||||
|
* editor.on('asset:update', (asset, updatedProps) => { ... }); |
||||
|
*/ |
||||
|
update = 'asset:update', |
||||
|
|
||||
|
/** |
||||
|
* @event `asset:open` Asset Manager opened. |
||||
|
* @example |
||||
|
* editor.on('asset:open', () => { ... }); |
||||
|
*/ |
||||
|
open = 'asset:open', |
||||
|
|
||||
|
/** |
||||
|
* @event `asset:close` Asset Manager closed. |
||||
|
* @example |
||||
|
* editor.on('asset:close', () => { ... }); |
||||
|
*/ |
||||
|
close = 'asset:close', |
||||
|
|
||||
|
/** |
||||
|
* @event `asset:upload:start` Asset upload start. |
||||
|
* @example |
||||
|
* editor.on('asset:upload:start', () => { ... }); |
||||
|
*/ |
||||
|
uploadStart = 'asset:upload:start', |
||||
|
|
||||
|
/** |
||||
|
* @event `asset:upload:end` Asset upload end. |
||||
|
* @example |
||||
|
* editor.on('asset:upload:end', (result) => { ... }); |
||||
|
*/ |
||||
|
uploadEnd = 'asset:upload:end', |
||||
|
|
||||
|
/** |
||||
|
* @event `asset:upload:error` Asset upload error. |
||||
|
* @example |
||||
|
* editor.on('asset:upload:error', (error) => { ... }); |
||||
|
*/ |
||||
|
uploadError = 'asset:upload:error', |
||||
|
|
||||
|
/** |
||||
|
* @event `asset:upload:response` Asset upload response. |
||||
|
* @example |
||||
|
* editor.on('asset:upload:response', (res) => { ... }); |
||||
|
*/ |
||||
|
uploadResponse = 'asset:upload:response', |
||||
|
|
||||
|
/** |
||||
|
* @event `asset:custom` Event to use in case of [custom Asset Manager UI](https://grapesjs.com/docs/modules/Assets.html#customization).
|
||||
|
* @example |
||||
|
* editor.on('asset:custom', ({ container, assets, ... }) => { ... }); |
||||
|
*/ |
||||
|
custom = 'asset:custom', |
||||
|
|
||||
|
/** |
||||
|
* @event `asset` Catch-all event for all the events mentioned above. An object containing all the available data about the triggered event is passed as an argument to the callback. |
||||
|
* @example |
||||
|
* editor.on('asset', ({ event, model, ... }) => { ... }); |
||||
|
*/ |
||||
|
all = 'asset', |
||||
|
} |
||||
|
/**{END_EVENTS}*/ |
||||
|
|
||||
|
// need this to avoid the TS documentation generator to break
|
||||
|
export default AssetsEvents; |
||||
@ -0,0 +1,71 @@ |
|||||
|
/**{START_EVENTS}*/ |
||||
|
export enum CommandsEvents { |
||||
|
/** |
||||
|
* @event `command:run` Triggered on run of any command. |
||||
|
* @example |
||||
|
* editor.on('command:run', ({ id, result, options }) => { |
||||
|
* console.log('Command id', id, 'command result', result); |
||||
|
* }); |
||||
|
*/ |
||||
|
run = 'command:run', |
||||
|
_run = 'run', |
||||
|
|
||||
|
/** |
||||
|
* @event `command:run:COMMAND_ID` Triggered on run of a specific command. |
||||
|
* @example |
||||
|
* editor.on('command:run:my-command', ({ result, options }) => { ... }); |
||||
|
*/ |
||||
|
runCommand = 'command:run:', |
||||
|
_runCommand = 'run:', |
||||
|
|
||||
|
/** |
||||
|
* @event `command:run:before:COMMAND_ID` Triggered before the command is called. |
||||
|
* @example |
||||
|
* editor.on('command:run:before:my-command', ({ options }) => { ... }); |
||||
|
*/ |
||||
|
runBeforeCommand = 'command:run:before:', |
||||
|
|
||||
|
/** |
||||
|
* @event `command:abort:COMMAND_ID` Triggered when the command execution is aborted. |
||||
|
* @example |
||||
|
* editor.on('command:abort:my-command', ({ options }) => { ... }); |
||||
|
* |
||||
|
* // The command could be aborted during the before event
|
||||
|
* editor.on('command:run:before:my-command', ({ options }) => { |
||||
|
* if (someCondition) { |
||||
|
* options.abort = true; |
||||
|
* } |
||||
|
* }); |
||||
|
*/ |
||||
|
abort = 'command:abort:', |
||||
|
_abort = 'abort:', |
||||
|
|
||||
|
/** |
||||
|
* @event `command:stop` Triggered on stop of any command. |
||||
|
* @example |
||||
|
* editor.on('command:stop', ({ id, result, options }) => { |
||||
|
* console.log('Command id', id, 'command result', result); |
||||
|
* }); |
||||
|
*/ |
||||
|
stop = 'command:stop', |
||||
|
_stop = 'stop', |
||||
|
|
||||
|
/** |
||||
|
* @event `command:stop:COMMAND_ID` Triggered on stop of a specific command. |
||||
|
* @example |
||||
|
* editor.on('command:run:my-command', ({ result, options }) => { ... }); |
||||
|
*/ |
||||
|
stopCommand = 'command:stop:', |
||||
|
_stopCommand = 'stop:', |
||||
|
|
||||
|
/** |
||||
|
* @event `command:stop:before:COMMAND_ID` Triggered before the command is called to stop. |
||||
|
* @example |
||||
|
* editor.on('command:stop:before:my-command', ({ options }) => { ... }); |
||||
|
*/ |
||||
|
stopBeforeCommand = 'command:stop:before:', |
||||
|
} |
||||
|
/**{END_EVENTS}*/ |
||||
|
|
||||
|
// need this to avoid the TS documentation generator to break
|
||||
|
export default CommandsEvents; |
||||
@ -0,0 +1,24 @@ |
|||||
|
import Component from './Component'; |
||||
|
import { toLowerCase } from '../../utils/mixins'; |
||||
|
import { DraggableDroppableFn } from './types'; |
||||
|
|
||||
|
export const type = 'head'; |
||||
|
const droppable = ['title', 'style', 'base', 'link', 'meta', 'script', 'noscript']; |
||||
|
|
||||
|
export default class ComponentHead extends Component { |
||||
|
get defaults() { |
||||
|
return { |
||||
|
// @ts-ignore
|
||||
|
...super.defaults, |
||||
|
type, |
||||
|
tagName: type, |
||||
|
draggable: false, |
||||
|
highlightable: false, |
||||
|
droppable: (({ tagName }) => !tagName || droppable.includes(toLowerCase(tagName))) as DraggableDroppableFn, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
static isComponent(el: HTMLElement) { |
||||
|
return toLowerCase(el.tagName) === type; |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,282 @@ |
|||||
|
import { isArray, isString, keys } from 'underscore'; |
||||
|
import Component, { keySymbol, keySymbolOvrd, keySymbols } from './Component'; |
||||
|
import { SymbolToUpOptions } from './types'; |
||||
|
import { isEmptyObj } from '../../utils/mixins'; |
||||
|
import Components from './Components'; |
||||
|
|
||||
|
export const isSymbolMain = (cmp: Component) => isArray(cmp.get(keySymbols)); |
||||
|
|
||||
|
export const isSymbolInstance = (cmp: Component) => !!cmp.get(keySymbol); |
||||
|
|
||||
|
export const isSymbol = (cmp: Component) => !!(isSymbolMain(cmp) || isSymbolInstance(cmp)); |
||||
|
|
||||
|
export const isSymbolRoot = (symbol: Component) => { |
||||
|
const parent = symbol.parent(); |
||||
|
return isSymbol(symbol) && (!parent || !isSymbol(parent)); |
||||
|
}; |
||||
|
|
||||
|
export const isSymbolNested = (symbol: Component) => { |
||||
|
if (!isSymbol(symbol)) return false; |
||||
|
const symbTopSelf = getSymbolTop(isSymbolMain(symbol) ? symbol : getSymbolMain(symbol)!); |
||||
|
const symbTop = getSymbolTop(symbol); |
||||
|
const symbTopMain = isSymbolMain(symbTop) ? symbTop : getSymbolMain(symbTop); |
||||
|
return symbTopMain !== symbTopSelf; |
||||
|
}; |
||||
|
|
||||
|
export const initSymbol = (symbol: Component) => { |
||||
|
if (symbol.__symbReady) return; |
||||
|
symbol.on('change', symbol.__upSymbProps); |
||||
|
symbol.__symbReady = true; |
||||
|
}; |
||||
|
|
||||
|
export const getSymbolMain = (symbol: Component): Component | undefined => { |
||||
|
let result = symbol.get(keySymbol); |
||||
|
|
||||
|
if (result && isString(result)) { |
||||
|
const ref = symbol.__getAllById()[result]; |
||||
|
if (ref) { |
||||
|
result = ref; |
||||
|
symbol.set(keySymbol, ref); |
||||
|
} else { |
||||
|
result = 0; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
return result || undefined; |
||||
|
}; |
||||
|
|
||||
|
export const getSymbolInstances = (symbol?: Component): Component[] | undefined => { |
||||
|
let symbs = symbol?.get(keySymbols); |
||||
|
|
||||
|
if (symbs && isArray(symbs)) { |
||||
|
symbs.forEach((symb, idx) => { |
||||
|
if (symb && isString(symb)) { |
||||
|
symbs[idx] = symbol!.__getAllById()[symb]; |
||||
|
} |
||||
|
}); |
||||
|
symbs = symbs.filter(symb => symb && !isString(symb)); |
||||
|
} |
||||
|
|
||||
|
return symbs || undefined; |
||||
|
}; |
||||
|
|
||||
|
export const isSymbolOverride = (symbol?: Component, prop = '') => { |
||||
|
const ovrd = symbol?.get(keySymbolOvrd); |
||||
|
const [prp] = prop.split(':'); |
||||
|
const props = prop !== prp ? [prop, prp] : [prop]; |
||||
|
return ovrd === true || (isArray(ovrd) && props.some(p => ovrd.indexOf(p) >= 0)); |
||||
|
}; |
||||
|
|
||||
|
export const getSymbolsToUpdate = (symb: Component, opts: SymbolToUpOptions = {}) => { |
||||
|
let result: Component[] = []; |
||||
|
const { changed } = opts; |
||||
|
|
||||
|
if ( |
||||
|
opts.fromInstance || |
||||
|
opts.noPropagate || |
||||
|
opts.fromUndo || |
||||
|
// Avoid updating others if the current component has override
|
||||
|
(changed && isSymbolOverride(symb, changed)) |
||||
|
) { |
||||
|
return result; |
||||
|
} |
||||
|
|
||||
|
const symbols = getSymbolInstances(symb) || []; |
||||
|
const symbol = getSymbolMain(symb); |
||||
|
const all = symbol ? [symbol, ...(getSymbolInstances(symbol) || [])] : symbols; |
||||
|
result = all |
||||
|
.filter(s => s !== symb) |
||||
|
// Avoid updating those with override
|
||||
|
.filter(s => !(changed && isSymbolOverride(s, changed))); |
||||
|
|
||||
|
return result; |
||||
|
}; |
||||
|
|
||||
|
export const getSymbolTop = (symbol: Component, opts?: any) => { |
||||
|
let result = symbol; |
||||
|
let parent = symbol.parent(opts); |
||||
|
|
||||
|
// while (parent && (isSymbolMain(parent) || getSymbol(parent))) {
|
||||
|
while (parent && isSymbol(parent)) { |
||||
|
result = parent; |
||||
|
parent = parent.parent(opts); |
||||
|
} |
||||
|
|
||||
|
return result; |
||||
|
}; |
||||
|
|
||||
|
export const detachSymbolInstance = (symbol: Component, opts: { skipRefs?: boolean } = {}) => { |
||||
|
const symbolMain = getSymbolMain(symbol); |
||||
|
const symbs = symbolMain && getSymbolInstances(symbolMain); |
||||
|
!opts.skipRefs && |
||||
|
symbs && |
||||
|
symbolMain.set( |
||||
|
keySymbols, |
||||
|
symbs.filter(s => s !== symbol) |
||||
|
); |
||||
|
symbol.set(keySymbol, 0); |
||||
|
symbol.components().forEach(s => detachSymbolInstance(s, opts)); |
||||
|
}; |
||||
|
|
||||
|
export const logSymbol = (symb: Component, type: string, toUp: Component[], opts: any = {}) => { |
||||
|
const symbol = getSymbolMain(symb); |
||||
|
const symbols = getSymbolInstances(symb); |
||||
|
|
||||
|
if (!symbol && !symbols) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
symb.em.log(type, { model: symb, toUp, context: 'symbols', opts }); |
||||
|
}; |
||||
|
|
||||
|
export const updateSymbolProps = (symbol: Component, opts: SymbolToUpOptions = {}) => { |
||||
|
const changed = symbol.changedAttributes() || {}; |
||||
|
const attrs = changed.attributes || {}; |
||||
|
delete changed.status; |
||||
|
delete changed.open; |
||||
|
delete changed[keySymbols]; |
||||
|
delete changed[keySymbol]; |
||||
|
delete changed[keySymbolOvrd]; |
||||
|
delete changed.attributes; |
||||
|
delete attrs.id; |
||||
|
|
||||
|
if (!isEmptyObj(attrs)) { |
||||
|
changed.attributes = attrs; |
||||
|
} |
||||
|
|
||||
|
if (!isEmptyObj(changed)) { |
||||
|
const toUp = getSymbolsToUpdate(symbol, opts); |
||||
|
// Avoid propagating overrides to other symbols
|
||||
|
keys(changed).map(prop => { |
||||
|
if (isSymbolOverride(symbol, prop)) delete changed[prop]; |
||||
|
}); |
||||
|
|
||||
|
logSymbol(symbol, 'props', toUp, { opts, changed }); |
||||
|
toUp.forEach(child => { |
||||
|
const propsChanged = { ...changed }; |
||||
|
// Avoid updating those with override
|
||||
|
keys(propsChanged).map(prop => { |
||||
|
if (isSymbolOverride(child, prop)) delete propsChanged[prop]; |
||||
|
}); |
||||
|
child.set(propsChanged, { fromInstance: symbol, ...opts }); |
||||
|
}); |
||||
|
} |
||||
|
}; |
||||
|
|
||||
|
export const updateSymbolCls = (symbol: Component, opts: any = {}) => { |
||||
|
const toUp = getSymbolsToUpdate(symbol, opts); |
||||
|
logSymbol(symbol, 'classes', toUp, { opts }); |
||||
|
toUp.forEach(child => { |
||||
|
// @ts-ignore This will propagate the change up to __upSymbProps
|
||||
|
child.set('classes', symbol.get('classes'), { fromInstance: symbol }); |
||||
|
}); |
||||
|
symbol.__changesUp(opts); |
||||
|
}; |
||||
|
|
||||
|
export const updateSymbolComps = (symbol: Component, m: Component, c: Components, o: any) => { |
||||
|
const optUp = o || c || {}; |
||||
|
const { fromInstance, fromUndo } = optUp; |
||||
|
const toUpOpts = { fromInstance, fromUndo }; |
||||
|
const isTemp = m.opt.temporary; |
||||
|
|
||||
|
// Reset
|
||||
|
if (!o) { |
||||
|
const coll = m as unknown as Components; |
||||
|
const toUp = getSymbolsToUpdate(symbol, { |
||||
|
...toUpOpts, |
||||
|
changed: 'components:reset', |
||||
|
}); |
||||
|
const cmps = coll.models; |
||||
|
const newSymbols = new Set<Component>(); |
||||
|
logSymbol(symbol, 'reset', toUp, { components: cmps }); |
||||
|
|
||||
|
toUp.forEach(rel => { |
||||
|
const relCmps = rel.components(); |
||||
|
const toReset = cmps.map((cmp, i) => { |
||||
|
// This particular case here is to handle reset from `resetFromString`
|
||||
|
// where we can receive an array of regulat components or already
|
||||
|
// existing symbols (updated already before reset)
|
||||
|
if (!isSymbol(cmp) || newSymbols.has(cmp)) { |
||||
|
newSymbols.add(cmp); |
||||
|
return cmp.clone({ symbol: true }); |
||||
|
} |
||||
|
return relCmps.at(i); |
||||
|
}); |
||||
|
relCmps.reset(toReset, { fromInstance: symbol, ...c } as any); |
||||
|
}); |
||||
|
// Add
|
||||
|
} else if (o.add) { |
||||
|
let addedInstances: Component[] = []; |
||||
|
const isMainSymb = !!getSymbolInstances(symbol); |
||||
|
const toUp = getSymbolsToUpdate(symbol, { |
||||
|
...toUpOpts, |
||||
|
changed: 'components:add', |
||||
|
}); |
||||
|
if (toUp.length) { |
||||
|
const addSymb = getSymbolMain(m); |
||||
|
addedInstances = (addSymb ? getSymbolInstances(addSymb) : getSymbolInstances(m)) || []; |
||||
|
addedInstances = [...addedInstances]; |
||||
|
addedInstances.push(addSymb ? addSymb : m); |
||||
|
} |
||||
|
!isTemp && |
||||
|
logSymbol(symbol, 'add', toUp, { |
||||
|
opts: o, |
||||
|
addedInstances: addedInstances.map(c => c.cid), |
||||
|
added: m.cid, |
||||
|
}); |
||||
|
// Here, before appending a new symbol, I have to ensure there are no previously
|
||||
|
// created symbols (eg. used mainly when drag components around)
|
||||
|
toUp.forEach(symb => { |
||||
|
const symbTop = getSymbolTop(symb); |
||||
|
const symbPrev = addedInstances.filter(addedInst => { |
||||
|
const addedTop = getSymbolTop(addedInst, { prev: 1 }); |
||||
|
return symbTop && addedTop && addedTop === symbTop; |
||||
|
})[0]; |
||||
|
const toAppend = symbPrev || m.clone({ symbol: true, symbolInv: isMainSymb }); |
||||
|
symb.append(toAppend, { fromInstance: symbol, ...o }); |
||||
|
}); |
||||
|
// Remove
|
||||
|
} else { |
||||
|
// Remove instance reference from the symbol
|
||||
|
const symb = getSymbolMain(m); |
||||
|
symb && |
||||
|
!o.temporary && |
||||
|
symb.set( |
||||
|
keySymbols, |
||||
|
getSymbolInstances(symb)!.filter(i => i !== m) |
||||
|
); |
||||
|
|
||||
|
// Propagate remove only if the component is an inner symbol
|
||||
|
if (!isSymbolRoot(m) && !o.skipRefsUp) { |
||||
|
const changed = 'components:remove'; |
||||
|
const { index } = o; |
||||
|
const parent = m.parent(); |
||||
|
const opts = { fromInstance: m, ...o }; |
||||
|
const isSymbNested = isSymbolRoot(m); |
||||
|
let toUpFn = (symb: Component) => { |
||||
|
const symbPrnt = symb.parent(); |
||||
|
symbPrnt && !isSymbolOverride(symbPrnt, changed) && symb.remove(opts); |
||||
|
}; |
||||
|
// Check if the parent allows the removing
|
||||
|
let toUp = !isSymbolOverride(parent, changed) ? getSymbolsToUpdate(m, toUpOpts) : []; |
||||
|
|
||||
|
if (isSymbNested) { |
||||
|
toUp = parent! && getSymbolsToUpdate(parent, { ...toUpOpts, changed })!; |
||||
|
toUpFn = symb => { |
||||
|
const toRemove = symb.components().at(index); |
||||
|
toRemove && toRemove.remove({ fromInstance: parent, ...opts }); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
!isTemp && |
||||
|
logSymbol(symbol, 'remove', toUp, { |
||||
|
opts: o, |
||||
|
removed: m.cid, |
||||
|
isSymbNested, |
||||
|
}); |
||||
|
toUp.forEach(toUpFn); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
symbol.__changesUp(optUp); |
||||
|
}; |
||||
@ -0,0 +1,56 @@ |
|||||
|
import { debounce } from 'underscore'; |
||||
|
import { Debounced, ObjectAny } from '../../common'; |
||||
|
import Component from './Component'; |
||||
|
import Components from './Components'; |
||||
|
import { detachSymbolInstance, getSymbolInstances } from './SymbolUtils'; |
||||
|
|
||||
|
interface PropsComponentUpdate { |
||||
|
component: Component; |
||||
|
changed: ObjectAny; |
||||
|
options: ObjectAny; |
||||
|
} |
||||
|
|
||||
|
export default class Symbols extends Components { |
||||
|
refreshDbn: Debounced; |
||||
|
|
||||
|
constructor(...args: ConstructorParameters<typeof Components>) { |
||||
|
super(...args); |
||||
|
this.refreshDbn = debounce(() => this.refresh(), 0); |
||||
|
const { events } = this; |
||||
|
this.on(events.update, this.onUpdate); |
||||
|
this.on(events.updateInside, this.onUpdateDeep); |
||||
|
} |
||||
|
|
||||
|
removeChildren(component: Component, coll?: Components, opts: any = {}) { |
||||
|
super.removeChildren(component, coll, opts); |
||||
|
getSymbolInstances(component)?.forEach(i => detachSymbolInstance(i, { skipRefs: true })); |
||||
|
this.__trgEvent(this.events.symbolMainRemove, { component }); |
||||
|
} |
||||
|
|
||||
|
onAdd(...args: Parameters<Components['onAdd']>) { |
||||
|
super.onAdd(...args); |
||||
|
const [component] = args; |
||||
|
this.__trgEvent(this.events.symbolMainAdd, { component }); |
||||
|
} |
||||
|
|
||||
|
onUpdate(props: PropsComponentUpdate) { |
||||
|
this.__trgEvent(this.events.symbolMainUpdate, props); |
||||
|
} |
||||
|
|
||||
|
onUpdateDeep(props: PropsComponentUpdate) { |
||||
|
this.__trgEvent(this.events.symbolMainUpdateDeep, props); |
||||
|
} |
||||
|
|
||||
|
refresh() { |
||||
|
const { em, events } = this; |
||||
|
em.trigger(events.symbol); |
||||
|
} |
||||
|
|
||||
|
__trgEvent(event: string, props: ObjectAny, isInstance = false) { |
||||
|
const { em, events } = this; |
||||
|
const eventType = isInstance ? events.symbolInstance : events.symbolMain; |
||||
|
em.trigger(event, props); |
||||
|
em.trigger(eventType, { ...props, event }); |
||||
|
this.refreshDbn(); |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,106 @@ |
|||||
|
import Component from './model/Component'; |
||||
|
|
||||
|
export enum ActionLabelComponents { |
||||
|
remove = 'component:remove', |
||||
|
add = 'component:add', |
||||
|
move = 'component:move', |
||||
|
} |
||||
|
|
||||
|
export interface SymbolInfo { |
||||
|
isSymbol: boolean; |
||||
|
isMain: boolean; |
||||
|
isInstance: boolean; |
||||
|
isRoot: boolean; |
||||
|
main?: Component; |
||||
|
instances: Component[]; |
||||
|
relatives: Component[]; |
||||
|
} |
||||
|
|
||||
|
export enum ComponentsEvents { |
||||
|
/** |
||||
|
* @event `component:add` New component added. |
||||
|
* @example |
||||
|
* editor.on('component:add', (component) => { ... }); |
||||
|
*/ |
||||
|
add = 'component:add', |
||||
|
|
||||
|
/** |
||||
|
* @event `component:remove` Component removed. |
||||
|
* @example |
||||
|
* editor.on('component:remove', (component) => { ... }); |
||||
|
*/ |
||||
|
remove = 'component:remove', |
||||
|
removeBefore = 'component:remove:before', |
||||
|
|
||||
|
/** |
||||
|
* @event `component:create` Component created. |
||||
|
* @example |
||||
|
* editor.on('component:create', (component) => { ... }); |
||||
|
*/ |
||||
|
create = 'component:create', |
||||
|
|
||||
|
/** |
||||
|
* @event `component:update` Component is updated, the component is passed as an argument to the callback. |
||||
|
* @example |
||||
|
* editor.on('component:update', (component) => { ... }); |
||||
|
*/ |
||||
|
update = 'component:update', |
||||
|
updateInside = 'component:update-inside', |
||||
|
|
||||
|
/** |
||||
|
* @event `symbol:main:add` Added new main symbol. |
||||
|
* @example |
||||
|
* editor.on('symbol:main:add', ({ component }) => { ... }); |
||||
|
*/ |
||||
|
symbolMainAdd = 'symbol:main:add', |
||||
|
|
||||
|
/** |
||||
|
* @event `symbol:main:update` Main symbol updated. |
||||
|
* @example |
||||
|
* editor.on('symbol:main:update', ({ component }) => { ... }); |
||||
|
*/ |
||||
|
symbolMainUpdate = 'symbol:main:update', |
||||
|
symbolMainUpdateDeep = 'symbol:main:update-deep', |
||||
|
|
||||
|
/** |
||||
|
* @event `symbol:main:remove` Main symbol removed. |
||||
|
* @example |
||||
|
* editor.on('symbol:main:remove', ({ component }) => { ... }); |
||||
|
*/ |
||||
|
symbolMainRemove = 'symbol:main:remove', |
||||
|
|
||||
|
/** |
||||
|
* @event `symbol:main` Catch-all event related to main symbol updates. |
||||
|
* @example |
||||
|
* editor.on('symbol:main', ({ event, component }) => { ... }); |
||||
|
*/ |
||||
|
symbolMain = 'symbol:main', |
||||
|
|
||||
|
/** |
||||
|
* @event `symbol:instance:add` Added new root instance symbol. |
||||
|
* @example |
||||
|
* editor.on('symbol:instance:add', ({ component }) => { ... }); |
||||
|
*/ |
||||
|
symbolInstanceAdd = 'symbol:instance:add', |
||||
|
|
||||
|
/** |
||||
|
* @event `symbol:instance:remove` Root instance symbol removed. |
||||
|
* @example |
||||
|
* editor.on('symbol:instance:remove', ({ component }) => { ... }); |
||||
|
*/ |
||||
|
symbolInstanceRemove = 'symbol:instance:remove', |
||||
|
|
||||
|
/** |
||||
|
* @event `symbol:instance` Catch-all event related to instance symbol updates. |
||||
|
* @example |
||||
|
* editor.on('symbol:instance', ({ event, component }) => { ... }); |
||||
|
*/ |
||||
|
symbolInstance = 'symbol:instance', |
||||
|
|
||||
|
/** |
||||
|
* @event `symbol` Catch-all event for any symbol update (main or instance). |
||||
|
* @example |
||||
|
* editor.on('symbol', () => { ... }); |
||||
|
*/ |
||||
|
symbol = 'symbol', |
||||
|
} |
||||
@ -1,4 +1,4 @@ |
|||||
import ComponentLabel from '../model/ComponentLabel'; |
import ComponentLabel from '../model/ComponentLabel'; |
||||
import ComponentView from './ComponentView'; |
import ComponentLinkView from './ComponentLinkView'; |
||||
|
|
||||
export default class ComponentLabelView extends ComponentView<ComponentLabel> {} |
export default class ComponentLabelView extends ComponentLinkView<ComponentLabel> {} |
||||
|
|||||
@ -0,0 +1,169 @@ |
|||||
|
const traitInputAttr = { placeholder: 'cth. Ketik disini' }; |
||||
|
|
||||
|
export default { |
||||
|
assetManager: { |
||||
|
addButton: 'Tambah gambar', |
||||
|
inputPlh: 'http://path/menuju/gambar.jpg', |
||||
|
modalTitle: 'Pilih Gambar', |
||||
|
uploadTitle: 'Tarik gambar kesini atau klik 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: { |
||||
|
'': 'Kotak', |
||||
|
wrapper: 'Badan', |
||||
|
text: 'Teks', |
||||
|
comment: 'Komentar', |
||||
|
image: 'Gambar', |
||||
|
video: 'Video', |
||||
|
label: 'Label', |
||||
|
link: 'Link', |
||||
|
map: 'Peta', |
||||
|
tfoot: 'Kaki tabel', |
||||
|
tbody: 'Badan tabel', |
||||
|
thead: 'Kepala tabel', |
||||
|
table: 'Tabel', |
||||
|
row: 'Baris tabel', |
||||
|
cell: 'Cell tabel', |
||||
|
}, |
||||
|
}, |
||||
|
deviceManager: { |
||||
|
device: 'Perangkat', |
||||
|
devices: { |
||||
|
desktop: 'Desktop', |
||||
|
tablet: 'Tablet', |
||||
|
mobileLandscape: 'Mobile Landscape', |
||||
|
mobilePortrait: 'Mobile Portrait', |
||||
|
}, |
||||
|
}, |
||||
|
panels: { |
||||
|
buttons: { |
||||
|
titles: { |
||||
|
preview: 'Pra-tayang', |
||||
|
fullscreen: 'Tampilan penuh', |
||||
|
'sw-visibility': 'Lihat komponen', |
||||
|
'export-template': 'Lihat kode', |
||||
|
'open-sm': 'Buka Manajemen Style', |
||||
|
'open-tm': 'Pengaturan', |
||||
|
'open-layers': 'Buka Layer Manager', |
||||
|
'open-blocks': 'Buka Blocks', |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
selectorManager: { |
||||
|
label: 'Class', |
||||
|
selected: 'Terpilih', |
||||
|
emptyState: '- State -', |
||||
|
states: { |
||||
|
hover: 'Hover', |
||||
|
active: 'Klik', |
||||
|
'nth-of-type(2n)': 'Rata/Ganjil', |
||||
|
}, |
||||
|
}, |
||||
|
styleManager: { |
||||
|
empty: 'Pilih elemen sebelum menggunakan Manajemen Style', |
||||
|
layer: 'Layer', |
||||
|
fileButton: 'Gambar', |
||||
|
sectors: { |
||||
|
general: 'Umum', |
||||
|
layout: 'Pemetaan', |
||||
|
typography: 'Tipografi', |
||||
|
decorations: 'Dekorasi', |
||||
|
extra: 'Ekstra', |
||||
|
flex: 'Flex', |
||||
|
dimension: 'Dimensi', |
||||
|
}, |
||||
|
// Default names for sub properties in Composite and Stack types.
|
||||
|
// Other labels are generated directly from their property names (eg. 'font-size' will be 'Font size').
|
||||
|
properties: { |
||||
|
'text-shadow-h': 'X', |
||||
|
'text-shadow-v': 'Y', |
||||
|
'text-shadow-blur': 'Blur', |
||||
|
'text-shadow-color': 'Warna', |
||||
|
'box-shadow-h': 'X', |
||||
|
'box-shadow-v': 'Y', |
||||
|
'box-shadow-blur': 'Blur', |
||||
|
'box-shadow-spread': 'Spread', |
||||
|
'box-shadow-color': 'Warna', |
||||
|
'box-shadow-type': 'Tipe', |
||||
|
'margin-top-sub': 'Atas', |
||||
|
'margin-right-sub': 'Kanan', |
||||
|
'margin-bottom-sub': 'Bawah', |
||||
|
'margin-left-sub': 'Kiri', |
||||
|
'padding-top-sub': 'Atas', |
||||
|
'padding-right-sub': 'Kanan', |
||||
|
'padding-bottom-sub': 'Bawah', |
||||
|
'padding-left-sub': 'Kiri', |
||||
|
'border-width-sub': 'Panjang', |
||||
|
'border-style-sub': 'Gaya', |
||||
|
'border-color-sub': 'Warna', |
||||
|
'border-top-left-radius-sub': 'Atas Kiri', |
||||
|
'border-top-right-radius-sub': 'Atas Kanan', |
||||
|
'border-bottom-right-radius-sub': 'Bawah Kanan', |
||||
|
'border-bottom-left-radius-sub': 'Bawah Kiri', |
||||
|
'transform-rotate-x': 'Putar X', |
||||
|
'transform-rotate-y': 'Putar Y', |
||||
|
'transform-rotate-z': 'Putar Z', |
||||
|
'transform-scale-x': 'Scale X', |
||||
|
'transform-scale-y': 'Scale Y', |
||||
|
'transform-scale-z': 'Scale Z', |
||||
|
'transition-property-sub': 'Properti', |
||||
|
'transition-duration-sub': 'Durasi', |
||||
|
'transition-timing-function-sub': 'Timing', |
||||
|
'background-image-sub': 'Gambar', |
||||
|
'background-repeat-sub': 'Berulang', |
||||
|
'background-position-sub': 'Posisi', |
||||
|
'background-attachment-sub': 'Lampiran', |
||||
|
'background-size-sub': 'Ukuran', |
||||
|
}, |
||||
|
// Translate options in style properties
|
||||
|
// options: {
|
||||
|
// float: { // Id of the property
|
||||
|
// ...
|
||||
|
// left: 'Left', // {option id}: {Option label}
|
||||
|
// }
|
||||
|
// }
|
||||
|
}, |
||||
|
traitManager: { |
||||
|
empty: 'Pilih elemen terlebih dulu sebelum menggunakan Manajemen Trait', |
||||
|
label: 'Pengaturan komponen', |
||||
|
categories: { |
||||
|
categoryId: 'Label kategori', |
||||
|
}, |
||||
|
traits: { |
||||
|
// The core library generates the name by their `name` property
|
||||
|
labels: { |
||||
|
title: 'Judul', |
||||
|
href: 'Url (href)', |
||||
|
// id: 'Id',
|
||||
|
// alt: 'Alt',
|
||||
|
}, |
||||
|
// In a simple trait, like text input, these are used on input attributes
|
||||
|
attributes: { |
||||
|
id: traitInputAttr, |
||||
|
alt: traitInputAttr, |
||||
|
title: traitInputAttr, |
||||
|
href: { placeholder: 'cth. https://google.com' }, |
||||
|
}, |
||||
|
// In a trait like select, these are used to translate option names
|
||||
|
options: { |
||||
|
target: { |
||||
|
false: 'Jendela ini', |
||||
|
_blank: 'Jendela baru', |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
}, |
||||
|
storageManager: { |
||||
|
recover: 'Apakah kamu ingin mengembalikan draft yang belum tersimpan?', |
||||
|
}, |
||||
|
}; |
||||
@ -0,0 +1,31 @@ |
|||||
|
import { I18nConfig } from './config'; |
||||
|
|
||||
|
export type Messages = Required<I18nConfig>['messages']; |
||||
|
|
||||
|
/**{START_EVENTS}*/ |
||||
|
export enum I18nEvents { |
||||
|
/** |
||||
|
* @event `i18n:add` New set of messages is added. |
||||
|
* @example |
||||
|
* editor.on('i18n:add', (messages) => { ... }); |
||||
|
*/ |
||||
|
add = 'i18n:add', |
||||
|
|
||||
|
/** |
||||
|
* @event `i18n:update` The set of messages is updated. |
||||
|
* @example |
||||
|
* editor.on('i18n:update', (messages) => { ... }); |
||||
|
*/ |
||||
|
update = 'i18n:update', |
||||
|
|
||||
|
/** |
||||
|
* @event `i18n:locale` Locale changed. |
||||
|
* @example |
||||
|
* editor.on('i18n:locale', ({ value, valuePrev }) => { ... }); |
||||
|
*/ |
||||
|
locale = 'i18n:locale', |
||||
|
} |
||||
|
/**{END_EVENTS}*/ |
||||
|
|
||||
|
// need this to avoid the TS documentation generator to break
|
||||
|
export default I18nEvents; |
||||
Some files were not shown because too many files changed in this diff
Loading…
Reference in new issue