Browse Source

Merge branch 'dev' into component-fix

pull/4439/head
Artur Arseniev 4 years ago
committed by GitHub
parent
commit
ab7872319f
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 4
      dist/grapes.min.js
  2. 2
      dist/grapes.min.js.map
  3. BIN
      docs/.vuepress/public/layer-manager.png
  4. 4
      docs/.vuepress/theme/layouts/CarbonAds.vue
  5. 2
      docs/api.js
  6. 47
      docs/api/layer_manager.md
  7. 321
      docs/modules/Layers.md
  8. 7
      package.json
  9. 37
      src/abstract/Collection.ts
  10. 21
      src/abstract/DomainViews.ts
  11. 32
      src/abstract/Module.ts
  12. 39
      src/abstract/View.ts
  13. 8
      src/canvas/model/Frame.ts
  14. 12
      src/canvas/model/Frames.ts
  15. 1
      src/dom_components/model/Component.js
  16. 283
      src/modal_dialog/index.js
  17. 264
      src/modal_dialog/index.ts
  18. 5
      src/modal_dialog/model/Modal.ts
  19. 28
      src/modal_dialog/view/ModalView.ts
  20. 4
      src/navigator/index.ts
  21. 5
      src/pages/model/Page.ts
  22. 245
      src/panels/index.js
  23. 232
      src/panels/index.ts
  24. 30
      src/panels/model/Button.js
  25. 62
      src/panels/model/Button.ts
  26. 19
      src/panels/model/Buttons.ts
  27. 20
      src/panels/model/Panel.js
  28. 31
      src/panels/model/Panel.ts
  29. 6
      src/panels/model/Panels.js
  30. 11
      src/panels/model/Panels.ts
  31. 70
      src/panels/view/ButtonView.ts
  32. 28
      src/panels/view/ButtonsView.ts
  33. 29
      src/panels/view/PanelView.ts
  34. 33
      src/panels/view/PanelsView.ts
  35. 2
      src/utils/Sorter.js
  36. 5
      test/specs/modal/index.js
  37. 6
      test/specs/modal/view/ModalView.js
  38. 12
      test/specs/panels/index.js
  39. 4
      test/specs/panels/model/PanelModels.js
  40. 11
      test/specs/panels/view/ButtonView.js
  41. 9
      test/specs/panels/view/ButtonsView.js
  42. 21
      test/specs/panels/view/PanelView.js
  43. 10
      test/specs/panels/view/PanelsView.js
  44. 13
      webpack.config.js
  45. 6581
      yarn.lock

4
dist/grapes.min.js

File diff suppressed because one or more lines are too long

2
dist/grapes.min.js.map

File diff suppressed because one or more lines are too long

BIN
docs/.vuepress/public/layer-manager.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

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

@ -9,7 +9,7 @@ export default {
if (!total) {
const src = document.createElement('script');
src.src = `//cdn.carbonads.com/carbon.js?serve=CEADC5QL&placement=grapesjscom`;
src.src = `//cdn.carbonads.com/carbon.js?serve=CEAIVK77&placement=grapesjscom`;
src.setAttribute('id', '_carbonads_js');
const adCont = document.getElementById('native-carbon');
adCont && adCont.appendChild(src);
@ -39,8 +39,6 @@ export default {
},
load () {
const s = document.createElement('script');
// s.id = '_carbonads_js';
// s.src = `//cdn.carbonads.com/carbon.js?serve=CEADC5QL&placement=grapesjscom`;
s.src = `//m.servedby-buysellads.com/monetization.js`;
s.onload = () => this.initCarbon();
this.$el.appendChild(s);

2
docs/api.js

@ -36,7 +36,7 @@ async function generateDocs () {
['selector_manager/model/State.ts', 'state.md'],
['css_composer/index.js', 'css_composer.md'],
['css_composer/model/CssRule.js', 'css_rule.md'],
['modal_dialog/index.js', 'modal_dialog.md'],
['modal_dialog/index.ts', 'modal_dialog.md'],
['rich_text_editor/index.js', 'rich_text_editor.md'],
['keymaps/index.js', 'keymaps.md'],
['undo_manager/index.js', 'undo_manager.md'],

47
docs/api/layer_manager.md

@ -36,7 +36,8 @@ const layers = editor.Layers;
* [setlocked][8]
* [isLocked][9]
* [setName][10]
* [getLayerData][11]
* [getName][11]
* [getLayerData][12]
[Page]: page.html
@ -48,7 +49,7 @@ Update the root layer with another component.
### Parameters
* `component` **([Component] | [String][12])** Component to be set as root
* `component` **([Component] | [String][13])** Component to be set as root
### Examples
@ -87,7 +88,7 @@ const components = layers.getComponents(component);
console.log(components);
```
Returns **[Array][13]<[Component]>**
Returns **[Array][14]<[Component]>**
## setOpen
@ -96,7 +97,7 @@ Update the layer open state of the component.
### Parameters
* `component` **[Component]** Component to update
* `value` **[Boolean][14]**
* `value` **[Boolean][15]**
## isOpen
@ -106,7 +107,7 @@ Check the layer open state of the component.
* `component` **[Component]**
Returns **[Boolean][14]**
Returns **[Boolean][15]**
## setVisible
@ -115,7 +116,7 @@ Update the layer visibility state of the component.
### Parameters
* `component` **[Component]** Component to update
* `value` **[Boolean][14]**
* `value` **[Boolean][15]**
## isVisible
@ -125,7 +126,7 @@ Check the layer visibility state of the component.
* `component` **[Component]**
Returns **[Boolean][14]**
Returns **[Boolean][15]**
## setLocked
@ -134,7 +135,7 @@ Update the layer locked state of the component.
### Parameters
* `component` **[Component]** Component to update
* `value` **[Boolean][14]**
* `value` **[Boolean][15]**
## isLocked
@ -144,16 +145,26 @@ Check the layer locked state of the component.
* `component` **[Component]**
Returns **[Boolean][14]**
Returns **[Boolean][15]**
## setName
Update the layer name state of the component.
Update the layer name of the component.
### Parameters
* `component` **[Component]** Component to update
* `value` **[String][12]** New name
* `value` **[String][13]** New name
## getName
Get the layer name of the component.
### Parameters
* `component` **[Component]**
Returns **[String][13]** Component layer name
## getLayerData
@ -171,7 +182,7 @@ const layerData = layers.getLayerData(component);
console.log(layerData);
```
Returns **[Object][15]** Object containing the layer data.
Returns **[Object][16]** Object containing the layer data.
[1]: #setroot
@ -193,12 +204,14 @@ Returns **[Object][15]** Object containing the layer data.
[10]: #setname
[11]: #getlayerdata
[11]: #getname
[12]: #getlayerdata
[12]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[13]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/String
[13]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[14]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array
[14]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[15]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Boolean
[15]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object
[16]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object

321
docs/modules/Layers.md

@ -4,12 +4,12 @@ title: Layer Manager
# Layer Manager
<!-- <p align="center"><img :src="$withBase('/selector-manager.jpg')" alt="GrapesJS - Selector Manager"/></p> -->
<p align="center"><img :src="$withBase('/layer-manager.png')" alt="GrapesJS - Layer Manager"/></p>
The Layer Manager module is responsible to manage and display your [Components] as a tree.
::: warning
This guide is referring to GrapesJS v0.19.4 or higher
This guide is referring to GrapesJS v0.19.5 or higher
:::
[[toc]]
@ -17,7 +17,7 @@ This guide is referring to GrapesJS v0.19.4 or higher
## Configuration
To change the default configurations you have to pass the `layerManager` property with the main configuration object.
To change the default configurations you have to pass the `layerManager` option with the main configuration object.
```js
const editor = grapesjs.init({
@ -28,6 +28,9 @@ const editor = grapesjs.init({
});
```
You can check here the full list of available configuration options: [Layer Manager Config](https://github.com/artf/grapesjs/blob/master/src/navigator/config/config.ts)
Layers are a direct representation of your components, therefore they will only be available once your components are loaded in the editor (eg. you might load your project data from a remote endpoint).
In your configuration, you're able to change the global behavior of layers (eg. make all the layers not sortable) and also decide which component layer should be used as a root.
@ -47,8 +50,6 @@ const editor = grapesjs.init({
The configurations are mainly targeting the default UI provided by GrapesJS core, in case you need more control over the tree of your layers, you can read more in the [Customization](#customization) section below.
You can check here the full list of available configuration options: [Layer Manager Config](https://github.com/artf/grapesjs/blob/master/src/navigator/config/config.ts)
## Programmatic usage
@ -61,32 +62,319 @@ If you need to manage layers programmatically you can use its [APIs][Layers API]
## Customization
The default UI can handle most of the common tasks but in case you need a more advanced logic/elements, that requires a replace of the default UI.
By using the [Layers API][Layers API] you're able to replace the default UI with your own implementation.
All you have to do is to indicate the editor your intent to use a custom UI and then subscribe to the `selector:custom` event that will trigger on any necessary update of the UI.
All you have to do is to indicate to the editor your intent to use a custom UI and then subscribe to a few events that allow you to properly update your UI.
```js
const editor = grapesjs.init({
// ...
selectorManager: {
layerManager: {
custom: true,
// ...
},
});
editor.on('selector:custom', props => {
// Use this event to append your UI in the default container provided by GrapesJS.
// You can skip this event if you don't rely on the core panels and decide to
// place the UI in some other place.
editor.on('layer:custom', (props) => {
// props.container (HTMLElement) - The default element where you can append your UI
// Here you would put the logic to render/update your UI.
});
```
In the example below we'll replicate most of the default functionality by using solely the Selector Manager API.
<demo-viewer value="v8cgkLfr" height="500" darkcode/>
// Triggered when the root layer is changed.
editor.on('layer:root', (root) => {
// Update the root of your UI
});
// Triggered when a component is updated, this allows you to update specific layers.
editor.on('layer:component', (component) => {
// Update the specific layer of your UI
});
```
In the example below we'll replicate most of the default functionality with our own implementation.
<demo-viewer value="L24hkgm5" height="500" darkcode/>
<!-- Demo template, here for reference
<style>
.layer-manager {
position: relative;
text-align: left;
}
.layer-item.hidden {
opacity: 0.5;
}
.layer-item-icon {
width: 15px;
cursor: pointer;
}
.layer-item-eye {
}
.layer-item-chevron {
transform: rotate(90deg);
}
.layer-item-chevron.open {
transform: rotate(180deg);
}
.layer-item-chevron.hidden {
opacity: 0;
pointer-events: none;
}
.layer-item-row {
display: flex;
align-items: center;
user-select: none;
gap: 8px;
padding: 5px 8px;
border-bottom: 1px solid rgba(0,0,0,0.35);
}
.layer-item-row.selected {
background-color: rgba(255,255,255,0.15);
}
.layer-item-row.hovered {
background-color: rgba(255,255,255,0.05);
}
.layer-item-name {
margin-left: 3px;
}
.layer-item-name.editing {
background-color: white;
color: #555;
padding: 0 3px;
}
.layer-item-name-cnt {
display: flex;
align-items: center;
flex-grow: 1;
}
.layer-drag-indicator {
position: absolute;
width: 100%;
height: 1px;
left: 0;
background-color: #3b97e3;
}
</style>
<div style="display: none">
<div
class="layer-manager"
@pointerdown="onDragStart"
@pointermove="onDragMove"
@pointerup="onDragEnd"
>
<layer-item v-if="root" :component="root" :level="0"></layer-item>
<div
v-if="dragIndicator.show"
class="layer-drag-indicator"
:style="{ top: `${dragIndicator.y}px`, marginLeft: `${dragIndicator.offset}px`, width: `calc(100% - ${dragIndicator.offset}px)` }"></div>
</div>
<div id="layer-item-template" style="display: none;">
<div :class="['layer-item', !visible && 'hidden']">
<div
:class="['layer-item-row', selected && 'selected', hovered && 'hovered']"
@click="setSelected"
@mouseenter="setHover(true)"
@mouseleave="setHover(false)"
ref="layerRef"
data-layer-item
>
<div class="layer-item-icon layer-item-eye" @click.stop="toggleVisibility()">
<svg v-if="visible" viewBox="0 0 24 24"><path fill="currentColor" d="M12,9A3,3 0 0,0 9,12A3,3 0 0,0 12,15A3,3 0 0,0 15,12A3,3 0 0,0 12,9M12,17A5,5 0 0,1 7,12A5,5 0 0,1 12,7A5,5 0 0,1 17,12A5,5 0 0,1 12,17M12,4.5C7,4.5 2.73,7.61 1,12C2.73,16.39 7,19.5 12,19.5C17,19.5 21.27,16.39 23,12C21.27,7.61 17,4.5 12,4.5Z" /></svg>
<svg v-else viewBox="0 0 24 24"><path fill="currentColor" d="M11.83,9L15,12.16C15,12.11 15,12.05 15,12A3,3 0 0,0 12,9C11.94,9 11.89,9 11.83,9M7.53,9.8L9.08,11.35C9.03,11.56 9,11.77 9,12A3,3 0 0,0 12,15C12.22,15 12.44,14.97 12.65,14.92L14.2,16.47C13.53,16.8 12.79,17 12,17A5,5 0 0,1 7,12C7,11.21 7.2,10.47 7.53,9.8M2,4.27L4.28,6.55L4.73,7C3.08,8.3 1.78,10 1,12C2.73,16.39 7,19.5 12,19.5C13.55,19.5 15.03,19.2 16.38,18.66L16.81,19.08L19.73,22L21,20.73L3.27,3M12,7A5,5 0 0,1 17,12C17,12.64 16.87,13.26 16.64,13.82L19.57,16.75C21.07,15.5 22.27,13.86 23,12C21.27,7.61 17,4.5 12,4.5C10.6,4.5 9.26,4.75 8,5.2L10.17,7.35C10.74,7.13 11.35,7 12,7Z" /></svg>
</div>
<div class="layer-item-name-cnt" :style="{ marginLeft: `${level*10}px` }">
<div :class="['layer-item-icon layer-item-chevron', open && 'open', !components.length && 'hidden']" @click.stop="toggleOpen()">
<svg viewBox="0 0 24 24"><path fill="currentColor" d="M7.41,15.41L12,10.83L16.59,15.41L18,14L12,8L6,14L7.41,15.41Z" /></svg>
</div>
<div ref="nameInput"
:class="['layer-item-name', editing && 'editing']"
:contenteditable="editing"
@dblclick.stop="setEditing(true)"
@blur.stop="setEditing(false)"
@keydown.enter="setEditing(false)"
>
{{ name }}
</div>
</div>
<div v-if="component.get('draggable')" class="layer-item-icon layer-item-move" data-layer-move>
<svg viewBox="0 0 24 24"><path fill="currentColor" d="M13,6V11H18V7.75L22.25,12L18,16.25V13H13V18H16.25L12,22.25L7.75,18H11V13H6V16.25L1.75,12L6,7.75V11H11V6H7.75L12,1.75L16.25,6H13Z"/></svg>
</div>
</div>
<div v-if="open" class="layer-items">
<layer-item v-for="cmp in components" :key="cmp.getId()" :component="cmp" :level="level + 1"/>
</div>
</div>
</div>
</div>
<script>
const { Components, Layers } = editor;
const cmpElMap = new WeakMap();
Vue.component('layer-item', {
template: '#layer-item-template',
props: { component: Object, level: Number },
data() {
return {
name: '',
components: [],
visible: true,
open: false,
selected: false,
hovered: false,
editing: false,
}
},
mounted() {
this.updateLayer(Layers.getLayerData(this.component));
cmpElMap.set(this.$refs.layerRef, this.component);
editor.on('layer:component', this.onLayerComponentUpdate);
},
destroyed() {
editor.off('layer:component', this.onLayerComponentUpdate);
},
methods: {
onLayerComponentUpdate(cmp) {
if (cmp === this.component) {
this.updateLayer(Layers.getLayerData(cmp));
}
},
updateLayer(data) {
this.name = data.name;
this.components = data.components;
this.visible = data.visible;
this.open = data.open;
this.selected = data.selected;
this.hovered = data.hovered;
},
toggleVisibility() {
const { component } = this;
Layers.setVisible(this.component, !this.visible);
},
toggleOpen() {
const { component } = this;
Layers.setOpen(this.component, !this.open);
},
setHover(hovered) {
Layers.setLayerData(this.component, { hovered })
},
setSelected(event) {
Layers.setLayerData(this.component, { selected: true }, { event })
},
setEditing(value) {
this.editing = value;
const el = this.$refs.nameInput;
if (!value) {
Layers.setName(this.component, el.innerText)
} else {
setTimeout(() => el.focus())
}
},
}
});
const app = new Vue({
el: '.layer-manager',
data: {
root: null,
isDragging: false,
draggingCmp: null,
draggingOverCmp: null,
dragIndicator: {},
canMoveRes: {},
},
mounted() {
editor.on('layer:custom', this.handleCustom);
editor.on('layer:root', this.handleRootChange);
},
destroyed() {
editor.off('layer:custom', this.handleCustom);
editor.off('layer:root', this.handleRootChange);
},
methods: {
handleCustom(props = {}) {
const { container, root } = props;
container && container.appendChild(this.$el);
this.handleRootChange(root);
},
handleRootChange(root) {
console.log('root update', root);
this.root = root;
},
getDragTarget(ev) {
const el = document.elementFromPoint(ev.clientX, ev.clientY);
const dragEl = el?.closest('[data-layer-move]');
const elLayer = el?.closest('[data-layer-item]');
return {
dragEl,
elLayer,
cmp: cmpElMap.get(elLayer),
}
},
onDragStart(ev) {
if (this.getDragTarget(ev).dragEl) {
this.isDragging = true;
}
},
onDragMove(ev) {
if (!this.isDragging) return;
const { cmp, elLayer } = this.getDragTarget(ev);
if (!cmp || !elLayer) return;
const { draggingCmp } = this;
const layerRect = elLayer.getBoundingClientRect();
const layerH = elLayer.offsetHeight;
const layerY = elLayer.offsetTop;
const pointerY = ev.clientY;
const isBefore = pointerY < (layerRect.y + layerH / 2);
const cmpSource = !draggingCmp ? cmp : draggingCmp;
const cmpTarget = cmp.parent();
const cmpIndex = cmp.index() + (isBefore ? 0 : 1);
this.draggingCmp = !draggingCmp ? cmp : draggingCmp;
this.draggingOverCmp = cmp;
const canMove = Components.canMove(cmpTarget, cmpSource, cmpIndex);
const canMoveInside = Components.canMove(cmp, cmpSource);
const canMoveRes = {
...canMove,
canMoveInside,
index: cmpIndex,
};
// if (
// canMoveInside.result &&
// (
// pointerY > (layerRect.y + LAYER_PAD)
// && pointerY < (layerRect.y + layerH - LAYER_PAD))
// ) {
// pointerInside = true;
// canMoveRes.target = cmp;
// delete canMoveRes.index;
// }
// setDragParent(pointerInside ? cmp : undefined);
this.canMoveRes = canMoveRes;
const dragLevel = (cmp ? cmp.parents() : []).length;
this.dragIndicator = {
y: layerY + (isBefore ? 0 : layerH),
h: layerH,
offset: dragLevel * 10 + 20,
show: !!(this.draggingCmp && canMoveRes?.result),
};
},
onDragEnd(ev) {
const { canMoveRes } = this;
canMoveRes.result && canMoveRes.source.move(canMoveRes.target, { at: canMoveRes.index });
this.isDragging = false;
this.draggingCmp = null;
this.draggingOverCmp = null;
this.dragIndicator = {};
this.canMoveRes = {};
},
}
});
</script>
-->
## Events
@ -94,8 +382,5 @@ In the example below we'll replicate most of the default functionality by using
For a complete list of available events, you can check it [here](/api/layer_manager.html#available-events).
[Selector]: </api/selector.html>
[Style Manager]: <Style-manager.html>
[Components]: <Components.html>
[Getting Started]: </getting-started.html>
[Layers API]: </api/layer_manager.html>

7
package.json

@ -1,7 +1,7 @@
{
"name": "grapesjs",
"description": "Free and Open Source Web Builder Framework",
"version": "0.19.4",
"version": "0.19.5",
"author": "Artur Arseniev",
"license": "BSD-3-Clause",
"homepage": "http://grapesjs.com",
@ -16,7 +16,7 @@
"url": "https://github.com/artf/grapesjs.git"
},
"dependencies": {
"backbone": "1.3.3",
"backbone": "1.4.1",
"backbone-undo": "^0.2.5",
"codemirror": "^5.63.0",
"codemirror-formatting": "^1.0.0",
@ -32,7 +32,7 @@
"@vuepress/plugin-google-analytics": "^1.8.2",
"documentation": "^13.2.5",
"eslint": "^8.12.0",
"grapesjs-cli": "^3.0.0",
"grapesjs-cli": "^3.0.1",
"html-entities": "^1.4.0",
"husky": "^2.7.0",
"jest": "^24.9.0",
@ -41,7 +41,6 @@
"prettier": "^2.4.1",
"sass": "^1.42.1",
"sinon": "^7.5.0",
"terser-webpack-plugin": "^5.3.1",
"vuepress": "^1.8.2",
"whatwg-fetch": "^3.6.2"
},

37
src/abstract/Collection.ts

@ -1,6 +1,35 @@
import Backbone from 'backbone';
import Backbone, { AddOptions } from 'backbone';
import { isArray, isObject, isUndefined } from 'underscore';
import Model from './Model';
export default class Collection<
TModel extends Model = Model
> extends Backbone.Collection<TModel> {}
type Module<TModel extends Model> = TModel extends Model<infer M> ? M : unknown;
type ModelConstructor<TModel extends Model> = { new (mod: Module<TModel>, attr: any): TModel };
export default class Collection<TModel extends Model = Model> extends Backbone.Collection<TModel> {
module!: Module<TModel>;
private newModel!: ModelConstructor<TModel>;
add(model: Array<Record<string, any>> | TModel, options?: AddOptions): TModel;
add(models: Array<Array<Record<string, any>> | TModel>, options?: AddOptions): TModel[];
add(model?: unknown, options?: AddOptions): any {
//Note: the undefined case needed because backbonejs not handle the reset() correctly
var models = isArray(model) ? model : !isUndefined(model) ? [model] : undefined;
models = models?.map(m => (m instanceof this.newModel ? m : new this.newModel(this.module, m))) ?? [undefined];
return super.add(isArray(model) ? models : models[0], options);
}
constructor(
module: Module<TModel>,
models: TModel[] | Array<Record<string, any>>,
modelConstructor: ModelConstructor<TModel>
) {
super(models, { module, modelConstructor });
}
preinitialize(models?: TModel[] | Array<Record<string, any>>, options?: any) {
this.newModel = options.modelConstructor;
this.module = options.module;
}
}

21
src/abstract/DomainViews.ts

@ -1,13 +1,13 @@
import { includes } from 'underscore';
import Backbone from 'backbone';
import View from './View';
import Collection from './Collection';
import Model from './Model';
/*interface DomainView<TView, TModel>{
constructor(model: TModel): TView
}*/
type TModel<TCollection> = TCollection extends Backbone.Collection<infer TModel>? TModel: Model;
export default abstract class DomainViews<TCollection extends Backbone.Collection<Model>, TItemView extends View> extends View<TModel<TCollection>> {
export default abstract class DomainViews<
TCollection extends Collection,
TItemView extends View
> extends View<TCollection> {
// Defines the View per type
itemsView = '';
@ -26,7 +26,7 @@ export default abstract class DomainViews<TCollection extends Backbone.Collectio
* @param {Model} model
* @private
* */
private addTo(model: TModel<TCollection>) {
private addTo(model: Model) {
this.add(model);
}
@ -35,7 +35,7 @@ export default abstract class DomainViews<TCollection extends Backbone.Collectio
const warn = `${ns ? `[${ns}]: ` : ''}'${type}' type not found`;
em?.logWarning(warn);*/
}
protected abstract renderView(model: TModel<TCollection>, itemType: string): TItemView;
protected abstract renderView(model: Model, itemType: string): TItemView;
/**
* Render new model inside the view
@ -43,7 +43,7 @@ export default abstract class DomainViews<TCollection extends Backbone.Collectio
* @param {Object} fragment Fragment collection
* @private
* */
private add(model: TModel<TCollection>, fragment?: DocumentFragment) {
private add(model: Model, fragment?: DocumentFragment) {
const { reuseView, viewCollection, itemsView = {} } = this;
var frag = fragment || null;
var typeField = model.get(this.itemType);
@ -69,10 +69,7 @@ export default abstract class DomainViews<TCollection extends Backbone.Collectio
this.clearItems();
this.$el.empty();
if (this.collection.length)
this.collection.each((model) => {
this.add(model, frag);
}, this);
if (this.collection.length) this.collection.each(model => this.add(model, frag));
this.$el.append(frag);
this.onRender();

32
src/abstract/Module.ts

@ -3,8 +3,7 @@ import { Collection, View } from '../common';
import EditorModel from '../editor/model/Editor';
import { createId, isDef, deepMerge } from '../utils/mixins';
export interface IModule<TConfig extends any = any>
extends IBaseModule<TConfig> {
export interface IModule<TConfig extends any = any> extends IBaseModule<TConfig> {
init(cfg: any): void;
destroy(): void;
postLoad(key: any): any;
@ -32,9 +31,7 @@ export interface IStorableModule extends IModule {
postLoad(key: any): any;
}
export default abstract class Module<T extends ModuleConfig = ModuleConfig>
implements IModule<T>
{
export default abstract class Module<T extends ModuleConfig = ModuleConfig> implements IModule<T> {
private _em: EditorModel;
private _config: T;
private _name: string;
@ -70,7 +67,7 @@ export default abstract class Module<T extends ModuleConfig = ModuleConfig>
onLoad?(): void;
init(cfg: T) {}
abstract destroy(): void;
abstract render(): HTMLElement;
abstract render(): HTMLElement | JQuery<HTMLElement> | undefined;
postLoad(key: any): void {}
get name(): string {
@ -92,7 +89,7 @@ export default abstract class Module<T extends ModuleConfig = ModuleConfig>
* Move the main DOM element of the module.
* To execute only post editor render (in postRender)
*/
__appendTo() {
__appendTo() {
const elTo = this.getConfig().appendTo;
if (elTo) {
@ -135,10 +132,7 @@ export abstract class ItemManagerModule<
return obj;
}
loadProjectData(
data: any = {},
param: { all?: TCollection; onResult?: Function; reset?: boolean } = {}
) {
loadProjectData(data: any = {}, param: { all?: TCollection; onResult?: Function; reset?: boolean } = {}) {
const { all, onResult, reset } = param;
const key = this.storageKey;
const opts: any = { action: 'load' };
@ -189,12 +183,8 @@ export abstract class ItemManagerModule<
em &&
all
.on('add', (m: any, c: any, o: any) => em.trigger(events.add, m, o))
.on('remove', (m: any, c: any, o: any) =>
em.trigger(events.remove, m, o)
)
.on('change', (p: any, c: any) =>
em.trigger(events.update, p, p.changedAttributes(), c)
)
.on('remove', (m: any, c: any, o: any) => em.trigger(events.remove, m, o))
.on('change', (p: any, c: any) => em.trigger(events.update, p, p.changedAttributes(), c))
.on('all', this.__catchAllEvent, this);
// Register collections
this.cls = [all].concat(opts.collections || []);
@ -203,7 +193,7 @@ export abstract class ItemManagerModule<
entity.on('all', (ev: any, model: any, coll: any, opts: any) => {
const options = opts || coll;
const opt = { event: ev, ...options };
[em, all].map((md) => md.trigger(event, model, opt));
[em, all].map(md => md.trigger(event, model, opt));
});
});
}
@ -263,13 +253,11 @@ export abstract class ItemManagerModule<
}
__listenUpdate(model: TCollection, event: string) {
model.on('change', (p, c) =>
this.em.trigger(event, p, p.changedAttributes(), c)
);
model.on('change', (p, c) => this.em.trigger(event, p, p.changedAttributes(), c));
}
__destroy() {
this.cls.forEach((coll) => {
this.cls.forEach(coll => {
coll.stopListening();
coll.reset();
});

39
src/abstract/View.ts

@ -1,29 +1,46 @@
import Backbone from 'backbone';
import Collection from './Collection';
import Model from './Model';
import Module, { IBaseModule } from './Module';
import { IBaseModule } from './Module';
export default class View<TModel extends Model = Model, TElement extends Element = HTMLElement> extends Backbone.View<
TModel,
TElement
> {
type ModuleFromModel<TModel extends Model> = TModel extends Model<infer M> ? M : unknown;
type Module<TItem extends Model | Collection> = TItem extends Collection<infer M>
? ModuleFromModel<M>
: TItem extends Model<infer M>
? M
: unknown;
type TCollection<TItem extends Model | Collection> = TItem extends Collection ? TItem : unknown;
export default class View<
TModel extends Model | Collection = Model,
TElement extends Element = HTMLElement
> extends Backbone.View<TModel extends Model ? TModel : undefined, TElement> {
protected get pfx() {
return this.ppfx + this.config.stylePrefix || '';
return this.ppfx + (this.config as any).stylePrefix || '';
}
protected get ppfx() {
return (this.em.config as any).stylePrefix || '';
return this.em.config.stylePrefix || '';
}
protected get module(): TModel extends Model<infer M> ? M : unknown {
//console.log((this.collection.first as any).module)
return this.model?.module ?? (this.collection as any).module;
collection!: TModel extends Model ? Collection<Model> : TModel;
protected get module(): Module<TModel> {
return (this.model as any)?.module ?? this.collection.module;
}
protected get em() {
return this.module.em;
}
protected get config(): TModel extends Model<infer M> ? (M extends IBaseModule<infer C> ? C : unknown) : unknown {
protected get config(): Module<TModel> extends IBaseModule<infer C> ? C : unknown {
return this.module.config as any;
}
public className!: string;
preinitialize(options?: any) {
this.className = '';
}
}

8
src/canvas/model/Frame.ts

@ -38,8 +38,8 @@ export default class Frame extends Model<CanvasModule> {
/**
* @hideconstructor
*/
constructor(module: CanvasModule, props: any) {
super(module, props);
constructor(module: CanvasModule, attr: any) {
super(module, attr);
const { em } = this;
const { styles, component } = this.attributes;
const domc = em.get('DomComponents');
@ -82,8 +82,8 @@ export default class Frame extends Model<CanvasModule> {
this.set('styles', allRules);
}
!props.width && this.set(keyAutoW, 1);
!props.height && this.set(keyAutoH, 1);
!attr.width && this.set(keyAutoW, 1);
!attr.height && this.set(keyAutoH, 1);
}
get head(): { tag: string; attributes: any }[] {

12
src/canvas/model/Frames.ts

@ -1,6 +1,6 @@
import { bindAll } from 'underscore';
import CanvasModule from '..';
import { Collection } from '../../common';
import { Collection } from '../../abstract';
import Page from '../../pages/model/Page';
import Frame from './Frame';
@ -8,11 +8,9 @@ export default class Frames extends Collection<Frame> {
loadedItems = 0;
itemsToLoad = 0;
page?: Page;
module: CanvasModule
constructor(module: CanvasModule, models: Frame[] = []) {
super(models);
this.module = module;
constructor(module: CanvasModule, models: Frame[] | Array<Record<string, any>> = []) {
super(module, models, Frame);
bindAll(this, 'itemLoaded');
this.on('reset', this.onReset);
this.on('remove', this.onRemove);
@ -20,7 +18,7 @@ export default class Frames extends Collection<Frame> {
onReset(m: Frame, opts?: { previousModels?: Frame[] }) {
const prev = opts?.previousModels || [];
prev.map((p) => this.onRemove(p));
prev.map(p => this.onRemove(p));
}
onRemove(removed?: Frame) {
@ -43,6 +41,6 @@ export default class Frames extends Collection<Frame> {
}
listenToLoadItems(on: boolean) {
this.forEach((item) => item[on ? 'on' : 'off']('loaded', this.itemLoaded));
this.forEach(item => item[on ? 'on' : 'off']('loaded', this.itemLoaded));
}
}

1
src/dom_components/model/Component.js

@ -1767,6 +1767,7 @@ export default class Component extends StyleableModel {
}
this.remove({ temporary: 1 });
component.append(this, opts);
this.emitUpdate();
}
}
return this;

283
src/modal_dialog/index.js

@ -1,283 +0,0 @@
/**
* 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/modal_dialog/config/config.js)
* ```js
* const editor = grapesjs.init({
* modal: {
* // options
* }
* })
* ```
*
* Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance
*
* ```js
* const modal = editor.Modal;
* ```
*
* ## Available Events
* * `modal:open` - Modal is opened
* * `modal:close` - Modal is closed
* * `modal` - Event triggered on any change related to the modal. An object containing all the available data about the triggered event is passed as an argument to the callback.
*
* ## Methods
* * [open](#open)
* * [close](#close)
* * [isOpen](#isopen)
* * [setTitle](#settitle)
* * [getTitle](#gettitle)
* * [setContent](#setcontent)
* * [getContent](#getcontent)
* * [onceClose](#onceclose)
* * [onceOpen](#onceopen)
*
* @module Modal
*/
import { debounce, isFunction, isString } from 'underscore';
import { createText } from '../utils/dom';
import defaults from './config/config';
import ModalM from './model/Modal';
import ModalView from './view/ModalView';
export default () => {
var c = {};
var model, modal;
const triggerEvent = (enable, em) => {
em && em.trigger(`modal:${enable ? 'open' : 'close'}`);
};
return {
/**
* Name of the module
* @type {String}
* @private
*/
name: 'Modal',
getConfig() {
return c;
},
/**
* Initialize module. Automatically called with a new instance of the editor
* @param {Object} config Configurations
* @private
*/
init(config = {}) {
c = {
...defaults,
...config,
};
const em = c.em;
this.em = em;
var ppfx = c.pStylePrefix;
if (ppfx) c.stylePrefix = ppfx + c.stylePrefix;
model = new ModalM(c);
model.on('change:open', (m, enb) => triggerEvent(enb, em));
model.on(
'change',
debounce(() => {
const data = this._evData();
const { custom } = this.getConfig();
isFunction(custom) && custom(data);
em.trigger('modal', data);
})
);
return this;
},
_evData() {
const titl = this.getTitle();
const cnt = this.getContent();
const { open, attributes } = model.attributes;
return {
open,
attributes,
title: isString(titl) ? createText(titl) : titl,
content: isString(cnt) ? createText(cnt) : cnt.get ? cnt.get(0) : cnt,
close: () => this.close(),
};
},
postRender(view) {
const el = view.model.getConfig().el || view.el;
const res = this.render();
res && res.appendTo(el);
},
/**
* Open the modal window
* @param {Object} [opts={}] Options
* @param {String|HTMLElement} [opts.title] Title to set for the modal
* @param {String|HTMLElement} [opts.content] Content to set for the modal
* @param {Object} [opts.attributes] Updates the modal wrapper with custom attributes
* @returns {this}
* @example
* modal.open({
* title: 'My title',
* content: 'My content',
* attributes: { class: 'my-class' },
* });
*/
open(opts = {}) {
const attr = opts.attributes || {};
opts.title && this.setTitle(opts.title);
opts.content && this.setContent(opts.content);
model.set('attributes', attr);
model.open();
modal && modal.updateAttr(attr);
return this;
},
/**
* Close the modal window
* @returns {this}
* @example
* modal.close();
*/
close() {
model.close();
return this;
},
/**
* Execute callback when the modal will be closed.
* The callback will be called one only time
* @param {Function} clb Callback to call
* @returns {this}
* @example
* modal.onceClose(() => {
* console.log('The modal is closed');
* });
*/
onceClose(clb) {
this.em.once('modal:close', clb);
return this;
},
/**
* Execute callback when the modal will be opened.
* The callback will be called one only time
* @param {Function} clb Callback to call
* @returns {this}
* @example
* modal.onceOpen(() => {
* console.log('The modal is opened');
* });
*/
onceOpen(clb) {
this.em.once('modal:open', clb);
return this;
},
/**
* Checks if the modal window is open
* @returns {Boolean}
* @example
* modal.isOpen(); // true | false
*/
isOpen() {
return !!model.get('open');
},
/**
* Set the title to the modal window
* @param {string | HTMLElement} title Title
* @returns {this}
* @example
* // pass a string
* modal.setTitle('Some title');
* // or an HTMLElement
* const el = document.createElement('div');
* el.innerText = 'New title';
* modal.setTitle(el);
*/
setTitle(title) {
model.set('title', title);
return this;
},
/**
* Returns the title of the modal window
* @returns {string | HTMLElement}
* @example
* modal.getTitle();
*/
getTitle() {
return model.get('title');
},
/**
* Set the content of the modal window
* @param {string | HTMLElement} content Content
* @returns {this}
* @example
* // pass a string
* modal.setContent('Some content');
* // or an HTMLElement
* const el = document.createElement('div');
* el.innerText = 'New content';
* modal.setContent(el);
*/
setContent(content) {
model.set('content', ' ');
model.set('content', content);
return this;
},
/**
* Get the content of the modal window
* @returns {string | HTMLElement}
* @example
* modal.getContent();
*/
getContent() {
return model.get('content');
},
/**
* Returns content element
* @return {HTMLElement}
* @private
*/
getContentEl() {
return modal.getContent().get(0);
},
/**
* Returns modal model
* @return {Model}
* @private
*/
getModel() {
return model;
},
/**
* Render the modal window
* @return {HTMLElement}
* @private
*/
render() {
if (this.getConfig().custom) return;
const View = ModalView.extend(c.extend);
const el = modal && modal.el;
modal = new View({
el,
model,
config: c,
});
return modal.render().$el;
},
destroy() {
modal && modal.remove();
[c, model, modal].forEach(i => (i = {}));
this.em = {};
},
};
};

264
src/modal_dialog/index.ts

@ -0,0 +1,264 @@
/**
* 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/modal_dialog/config/config.js)
* ```js
* const editor = grapesjs.init({
* modal: {
* // options
* }
* })
* ```
*
* Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance
*
* ```js
* const modal = editor.Modal;
* ```
*
* ## Available Events
* * `modal:open` - Modal is opened
* * `modal:close` - Modal is closed
* * `modal` - Event triggered on any change related to the modal. An object containing all the available data about the triggered event is passed as an argument to the callback.
*
* ## Methods
* * [open](#open)
* * [close](#close)
* * [isOpen](#isopen)
* * [setTitle](#settitle)
* * [getTitle](#gettitle)
* * [setContent](#setcontent)
* * [getContent](#getcontent)
* * [onceClose](#onceclose)
* * [onceOpen](#onceopen)
*
* @module Modal
*/
import { EventHandler } from 'backbone';
import { debounce, isFunction, isString } from 'underscore';
import { Module } from '../abstract';
import EditorView from '../editor/view/EditorView';
import EditorModel from '../editor/model/Editor';
import { createText } from '../utils/dom';
import defaults from './config/config';
import ModalM from './model/Modal';
import ModalView from './view/ModalView';
export default class ModalManager extends Module<typeof defaults> {
modal?: ModalView;
/**
* Initialize module. Automatically called with a new instance of the editor
* @param {Object} config Configurations
* @private
*/
constructor(em: EditorModel) {
super(em, 'Modal', defaults);
this.model = new ModalM(this);
this.model.on('change:open', (m: ModalM, enable: boolean) => {
em.trigger(`modal:${enable ? 'open' : 'close'}`);
});
this.model.on(
'change',
debounce(() => {
const data = this._evData();
const { custom } = this.config;
//@ts-ignore
isFunction(custom) && custom(data);
em.trigger('modal', data);
}, 0)
);
return this;
}
_evData() {
const titl = this.getTitle();
const cnt = this.getContent();
const { open, attributes } = this.model.attributes;
return {
open,
attributes,
title: isString(titl) ? createText(titl) : titl,
//@ts-ignore
content: isString(cnt) ? createText(cnt) : cnt.get ? cnt.get(0) : cnt,
close: () => this.close(),
};
}
postRender(view: EditorView) {
const el = view.model.config.el || view.el;
const res = this.render();
res && el?.appendChild(res);
}
/**
* Open the modal window
* @param {Object} [opts={}] Options
* @param {String|HTMLElement} [opts.title] Title to set for the modal
* @param {String|HTMLElement} [opts.content] Content to set for the modal
* @param {Object} [opts.attributes] Updates the modal wrapper with custom attributes
* @returns {this}
* @example
* modal.open({
* title: 'My title',
* content: 'My content',
* attributes: { class: 'my-class' },
* });
*/
open(opts: any = {}) {
const attr = opts.attributes || {};
opts.title && this.setTitle(opts.title);
opts.content && this.setContent(opts.content);
this.model.set('attributes', attr);
this.model.open();
this.modal && this.modal.updateAttr(attr);
return this;
}
/**
* Close the modal window
* @returns {this}
* @example
* modal.close();
*/
close() {
this.model.close();
return this;
}
/**
* Execute callback when the modal will be closed.
* The callback will be called one only time
* @param {Function} clb Callback to call
* @returns {this}
* @example
* modal.onceClose(() => {
* console.log('The modal is closed');
* });
*/
onceClose(clb: EventHandler) {
this.em.once('modal:close', clb);
return this;
}
/**
* Execute callback when the modal will be opened.
* The callback will be called one only time
* @param {Function} clb Callback to call
* @returns {this}
* @example
* modal.onceOpen(() => {
* console.log('The modal is opened');
* });
*/
onceOpen(clb: EventHandler) {
this.em.once('modal:open', clb);
return this;
}
/**
* Checks if the modal window is open
* @returns {Boolean}
* @example
* modal.isOpen(); // true | false
*/
isOpen() {
return !!this.model.get('open');
}
/**
* Set the title to the modal window
* @param {string | HTMLElement} title Title
* @returns {this}
* @example
* // pass a string
* modal.setTitle('Some title');
* // or an HTMLElement
* const el = document.createElement('div');
* el.innerText = 'New title';
* modal.setTitle(el);
*/
setTitle(title: string) {
this.model.set('title', title);
return this;
}
/**
* Returns the title of the modal window
* @returns {string | HTMLElement}
* @example
* modal.getTitle();
*/
getTitle() {
return this.model.get('title');
}
/**
* Set the content of the modal window
* @param {string | HTMLElement} content Content
* @returns {this}
* @example
* // pass a string
* modal.setContent('Some content');
* // or an HTMLElement
* const el = document.createElement('div');
* el.innerText = 'New content';
* modal.setContent(el);
*/
setContent(content: string | HTMLElement) {
this.model.set('content', ' ');
this.model.set('content', content);
return this;
}
/**
* Get the content of the modal window
* @returns {string | HTMLElement}
* @example
* modal.getContent();
*/
getContent(): string | HTMLElement {
return this.model.get('content');
}
/**
* Returns content element
* @return {HTMLElement}
* @private
*/
getContentEl() {
//@ts-ignore
return this.modal?.getContent().get(0);
}
/**
* Returns modal model
* @return {Model}
* @private
*/
getModel() {
return this.model;
}
/**
* Render the modal window
* @return {HTMLElement}
* @private
*/
render(): HTMLElement | undefined {
if (this.config.custom) return;
const View = ModalView.extend(this.config.extend);
const el = this.modal && this.modal.el;
this.modal = new View({
el,
model: this.model,
config: this.config,
});
return this.modal?.render().el;
}
destroy() {
this.modal?.remove();
}
}

5
src/modal_dialog/model/Modal.js → src/modal_dialog/model/Modal.ts

@ -1,6 +1,7 @@
import { Model } from '../../common';
import ModalManager from '..';
import { Model } from '../../abstract';
export default class Modal extends Model {
export default class Modal extends Model<ModalManager> {
defaults() {
return {
title: '',

28
src/modal_dialog/view/ModalView.js → src/modal_dialog/view/ModalView.ts

@ -1,7 +1,8 @@
import { View } from '../../common';
import { View } from '../../abstract';
import Modal from '../model/Modal';
export default class ModalView extends View {
template({ pfx, ppfx, content, title }) {
export default class ModalView extends View<Modal> {
template({ pfx, ppfx, content, title }: any) {
return `<div class="${pfx}dialog ${ppfx}one-bg ${ppfx}two-color">
<div class="${pfx}header">
<div class="${pfx}title">${title}</div>
@ -22,19 +23,19 @@ export default class ModalView extends View {
};
}
initialize(o) {
$title?: JQuery<HTMLElement>;
$content?: JQuery<HTMLElement>;
$collector?: JQuery<HTMLElement>;
constructor(o: any) {
super(o);
const model = this.model;
const config = o.config || {};
const pfx = config.stylePrefix || '';
this.config = config;
this.pfx = pfx;
this.ppfx = config.pStylePrefix || '';
this.listenTo(model, 'change:open', this.updateOpen);
this.listenTo(model, 'change:title', this.updateTitle);
this.listenTo(model, 'change:content', this.updateContent);
}
onClick(e) {
onClick(e: Event) {
const bkd = this.config.backdrop;
bkd && e.target === this.el && this.hide();
}
@ -52,7 +53,6 @@ export default class ModalView extends View {
/**
* Returns content element
* @return {HTMLElement}
* @private
*/
getContent() {
const pfx = this.pfx;
@ -69,7 +69,7 @@ export default class ModalView extends View {
* @return {HTMLElement}
* @private
*/
getTitle(opts = {}) {
getTitle(opts: any = {}) {
if (!this.$title) this.$title = this.$el.find('.' + this.pfx + 'title');
return opts.$ ? this.$title : this.$title.get(0);
}
@ -93,6 +93,7 @@ export default class ModalView extends View {
* */
updateTitle() {
const title = this.getTitle({ $: true });
//@ts-ignore
title && title.empty().append(this.model.get('title'));
}
@ -120,8 +121,9 @@ export default class ModalView extends View {
this.model.open();
}
updateAttr(attr) {
updateAttr(attr?: any) {
const { pfx, $el, el } = this;
//@ts-ignore
const currAttr = [].slice.call(el.attributes).map(i => i.name);
$el.removeAttr(currAttr.join(' '));
$el.attr({

4
src/navigator/index.ts

@ -267,7 +267,7 @@ export default class LayerManager extends Module<typeof defaults> {
};
}
setLayerData(component: any, data: Partial<Omit<LayerData, 'components'>>, opts = {}) {
setLayerData(component: Component, data: Partial<Omit<LayerData, 'components'>>, opts = {}) {
const { em, config } = this;
const { open, selected, hovered, visible, locked, name } = data;
const cmpOpts = { fromLayers: true, ...opts };
@ -279,7 +279,7 @@ export default class LayerManager extends Module<typeof defaults> {
if (selected) {
em.setSelected(component, cmpOpts);
const scroll = config.scrollCanvas;
scroll && component.views.forEach((view: any) => view.scrollIntoView(scroll));
scroll && component.views?.forEach((view: any) => view.scrollIntoView(scroll));
} else {
em.removeSelected(component, cmpOpts);
}

5
src/pages/model/Page.ts

@ -24,10 +24,7 @@ export default class Page extends Model {
['component', 'styles'].map(i => this.unset(i));
}
const frms: any[] = props.frames || [defFrame];
const frames = new Frames(
em.get('Canvas'),
frms?.map(model => new Frame(em.get('Canvas'), model))
);
const frames = new Frames(em.get('Canvas'), frms);
frames.page = this;
this.set('frames', frames);
!this.getId() && this.set('id', em?.get('PageManager')._createId());

245
src/panels/index.js

@ -1,245 +0,0 @@
/**
* 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/panels/config/config.js)
* ```js
* const editor = grapesjs.init({
* panels: {
* // options
* }
* })
* ```
*
* Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance
*
* ```js
* const panelManager = editor.Panels;
* ```
*
* * [addPanel](#addpanel)
* * [addButton](#addbutton)
* * [getButton](#getbutton)
* * [getPanel](#getpanel)
* * [getPanels](#getpanels)
* * [getPanelsEl](#getpanelsel)
* * [removePanel](#removepanel)
* * [removeButton](#removebutton)
*
* @module Panels
*/
import defaults from './config/config';
import Panel from './model/Panel';
import Panels from './model/Panels';
import PanelsView from './view/PanelsView';
export default () => {
var c = {};
var panels, PanelsViewObj;
return {
/**
* Name of the module
* @type {String}
* @private
*/
name: 'Panels',
/**
* Initialize module. Automatically called with a new instance of the editor
* @param {Object} config Configurations
* @private
*/
init(config) {
c = config || {};
for (var name in defaults) {
if (!(name in c)) c[name] = defaults[name];
}
var ppfx = c.pStylePrefix;
if (ppfx) c.stylePrefix = ppfx + c.stylePrefix;
panels = new Panels(c.defaults);
return this;
},
/**
* Returns the collection of panels
* @return {Collection} Collection of panel
*/
getPanels() {
return panels;
},
/**
* Returns panels element
* @return {HTMLElement}
*/
getPanelsEl() {
return PanelsViewObj && PanelsViewObj.el;
},
/**
* Add new panel to the collection
* @param {Object|Panel} panel Object with right properties or an instance of Panel
* @return {Panel} Added panel. Useful in case passed argument was an Object
* @example
* var newPanel = panelManager.addPanel({
* id: 'myNewPanel',
* visible : true,
* buttons : [...],
* });
*/
addPanel(panel) {
return panels.add(panel);
},
/**
* Remove a panel from the collection
* @param {Object|Panel|String} panel Object with right properties or an instance of Panel or Painel id
* @return {Panel} Removed panel. Useful in case passed argument was an Object
* @example
* const newPanel = panelManager.removePanel({
* id: 'myNewPanel',
* visible : true,
* buttons : [...],
* });
*
* const newPanel = panelManager.removePanel('myNewPanel');
*
*/
removePanel(panel) {
return panels.remove(panel);
},
/**
* Get panel by ID
* @param {string} id Id string
* @return {Panel|null}
* @example
* var myPanel = panelManager.getPanel('myNewPanel');
*/
getPanel(id) {
var res = panels.where({ id });
return res.length ? res[0] : null;
},
/**
* Add button to the panel
* @param {string} panelId Panel's ID
* @param {Object|Button} button Button object or instance of Button
* @return {Button|null} Added button. Useful in case passed button was an Object
* @example
* var newButton = panelManager.addButton('myNewPanel',{
* id: 'myNewButton',
* className: 'someClass',
* command: 'someCommand',
* attributes: { title: 'Some title'},
* active: false,
* });
* // It's also possible to pass the command as an object
* // with .run and .stop methods
* ...
* command: {
* run: function(editor) {
* ...
* },
* stop: function(editor) {
* ...
* }
* },
* // Or simply like a function which will be evaluated as a single .run command
* ...
* command: function(editor) {
* ...
* }
*/
addButton(panelId, button) {
var pn = this.getPanel(panelId);
return pn ? pn.get('buttons').add(button) : null;
},
/**
* Remove button from the panel
* @param {String} panelId Panel's ID
* @param {String} buttonId Button's ID
* @return {Button|null} Removed button.
* @example
* const removedButton = panelManager.addButton('myNewPanel',{
* id: 'myNewButton',
* className: 'someClass',
* command: 'someCommand',
* attributes: { title: 'Some title'},
* active: false,
* });
*
* const removedButton = panelManager.removeButton('myNewPanel', 'myNewButton');
*
*/
removeButton(panelId, button) {
var pn = this.getPanel(panelId);
return pn && pn.get('buttons').remove(button);
},
/**
* Get button from the panel
* @param {string} panelId Panel's ID
* @param {string} id Button's ID
* @return {Button|null}
* @example
* var button = panelManager.getButton('myPanel','myButton');
*/
getButton(panelId, id) {
var pn = this.getPanel(panelId);
if (pn) {
var res = pn.get('buttons').where({ id });
return res.length ? res[0] : null;
}
return null;
},
/**
* Render panels and buttons
* @return {HTMLElement}
* @private
*/
render() {
PanelsViewObj && PanelsViewObj.remove();
PanelsViewObj = new PanelsView({
collection: panels,
config: c,
});
return PanelsViewObj.render().el;
},
/**
* Active activable buttons
* @private
*/
active() {
this.getPanels().each(p => {
p.get('buttons').each(btn => {
btn.get('active') && btn.trigger('updateActive');
});
});
},
/**
* Disable buttons flagged as disabled
* @private
*/
disableButtons() {
this.getPanels().each(p => {
p.get('buttons').each(btn => {
if (btn.get('disable')) btn.trigger('change:disable');
});
});
},
destroy() {
panels.reset();
panels.stopListening();
PanelsViewObj && PanelsViewObj.remove();
[c, panels, PanelsViewObj].forEach(i => (i = {}));
},
Panel,
};
};

232
src/panels/index.ts

@ -0,0 +1,232 @@
/**
* 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/panels/config/config.js)
* ```js
* const editor = grapesjs.init({
* panels: {
* // options
* }
* })
* ```
*
* Once the editor is instantiated you can use its API. Before using these methods you should get the module from the instance
*
* ```js
* const panelManager = editor.Panels;
* ```
*
* * [addPanel](#addpanel)
* * [addButton](#addbutton)
* * [getButton](#getbutton)
* * [getPanel](#getpanel)
* * [getPanels](#getpanels)
* * [getPanelsEl](#getpanelsel)
* * [removePanel](#removepanel)
* * [removeButton](#removebutton)
*
* @module Panels
*/
import { Module } from '../abstract';
import EditorModel from '../editor/model/Editor';
import defaults from './config/config';
import Button from './model/Button';
import Panel from './model/Panel';
import Panels from './model/Panels';
import PanelsView from './view/PanelsView';
export default class PanelManager extends Module<typeof defaults> {
//config = {};
panels: Panels;
PanelsViewObj?: PanelsView;
/**
* Initialize module. Automatically called with a new instance of the editor
* @param {Object} config Configurations
* @private
*/
constructor(em: EditorModel) {
super(em, 'Panels', defaults);
this.panels = new Panels(this, this.config.defaults);
for (var name in defaults) {
//@ts-ignore
if (!(name in this.config)) this.config[name] = defaults[name];
}
return this;
}
/**
* Returns the collection of panels
* @return {Collection} Collection of panel
*/
getPanels() {
return this.panels;
}
/**
* Returns panels element
* @return {HTMLElement}
*/
getPanelsEl() {
return this.PanelsViewObj && this.PanelsViewObj.el;
}
/**
* Add new panel to the collection
* @param {Object|Panel} panel Object with right properties or an instance of Panel
* @return {Panel} Added panel. Useful in case passed argument was an Object
* @example
* var newPanel = panelManager.addPanel({
* id: 'myNewPanel',
* visible : true,
* buttons : [...],
* });
*/
addPanel(panel: Panel | Array<Record<string, any>>) {
return this.panels.add(panel);
}
/**
* Remove a panel from the collection
* @param {Object|Panel|String} panel Object with right properties or an instance of Panel or Painel id
* @return {Panel} Removed panel. Useful in case passed argument was an Object
* @example
* const newPanel = panelManager.removePanel({
* id: 'myNewPanel',
* visible : true,
* buttons : [...],
* });
*
* const newPanel = panelManager.removePanel('myNewPanel');
*
*/
removePanel(panel: Panel) {
return this.panels.remove(panel);
}
/**
* Get panel by ID
* @param {string} id Id string
* @return {Panel|null}
* @example
* var myPanel = panelManager.getPanel('myNewPanel');
*/
getPanel(id: string) {
var res = this.panels.where({ id });
return res.length ? res[0] : null;
}
/**
* Add button to the panel
* @param {string} panelId Panel's ID
* @param {Object|Button} button Button object or instance of Button
* @return {Button|null} Added button. Useful in case passed button was an Object
* @example
* var newButton = panelManager.addButton('myNewPanel',{
* id: 'myNewButton',
* className: 'someClass',
* command: 'someCommand',
* attributes: { title: 'Some title'},
* active: false,
* });
* // It's also possible to pass the command as an object
* // with .run and .stop methods
* ...
* command: {
* run: function(editor) {
* ...
* },
* stop: function(editor) {
* ...
* }
* },
* // Or simply like a function which will be evaluated as a single .run command
* ...
* command: function(editor) {
* ...
* }
*/
addButton(panelId: string, button: any) {
var pn = this.getPanel(panelId);
return pn ? pn.get('buttons').add(button) : null;
}
/**
* Remove button from the panel
* @param {String} panelId Panel's ID
* @param {String} buttonId Button's ID
* @return {Button|null} Removed button.
* @example
* const removedButton = panelManager.addButton('myNewPanel',{
* id: 'myNewButton',
* className: 'someClass',
* command: 'someCommand',
* attributes: { title: 'Some title'},
* active: false,
* });
*
* const removedButton = panelManager.removeButton('myNewPanel', 'myNewButton');
*
*/
removeButton(panelId: string, button: any) {
var pn = this.getPanel(panelId);
return pn && pn.get('buttons').remove(button);
}
/**
* Get button from the panel
* @param {string} panelId Panel's ID
* @param {string} id Button's ID
* @return {Button|null}
* @example
* var button = panelManager.getButton('myPanel','myButton');
*/
getButton(panelId: string, id: string) {
var pn = this.getPanel(panelId);
if (pn) {
var res = pn.get('buttons').where({ id });
return res.length ? res[0] : null;
}
return null;
}
/**
* Render panels and buttons
* @return {HTMLElement}
*/
render() {
this.PanelsViewObj && this.PanelsViewObj.remove();
this.PanelsViewObj = new PanelsView(this.panels);
return this.PanelsViewObj.render().el;
}
/**
* Active activable buttons
* @private
*/
active() {
this.getPanels().each(p => {
//@ts-ignore
p.get('buttons').each(btn => {
btn.get('active') && btn.trigger('updateActive');
});
});
}
/**
* Disable buttons flagged as disabled
* @private
*/
disableButtons() {
this.getPanels().each(p => {
//@ts-ignore
p.get('buttons').each(btn => {
if (btn.get('disable')) btn.trigger('change:disable');
});
});
}
destroy() {
this.panels.reset();
this.panels.stopListening();
this.PanelsViewObj && this.PanelsViewObj.remove();
}
}

30
src/panels/model/Button.js

@ -1,30 +0,0 @@
import { Model } from '../../common';
export default class Button extends Model {
defaults() {
return {
id: '',
label: '',
tagName: 'span',
className: '',
command: '',
context: '',
buttons: [],
attributes: {},
options: {},
active: false,
dragDrop: false,
togglable: true,
runDefaultCommand: true,
stopDefaultCommand: false,
disable: false,
};
}
initialize(options) {
if (this.get('buttons').length) {
var Buttons = require('./Buttons').default;
this.set('buttons', new Buttons(this.get('buttons')));
}
}
}

62
src/panels/model/Button.ts

@ -0,0 +1,62 @@
import PanelManager from '..';
import { Model } from '../../abstract';
import EditorModel from '../../editor/model/Editor';
import Buttons from './Buttons';
export default class Button extends Model<PanelManager> {
defaults() {
return {
id: '',
label: '',
tagName: 'span',
className: '',
command: '',
context: '',
buttons: [],
attributes: {},
options: {},
active: false,
dragDrop: false,
togglable: true,
runDefaultCommand: true,
stopDefaultCommand: false,
disable: false,
};
}
get className(): string {
return this.get('className');
}
get command(): string {
return this.get('command');
}
get active(): boolean {
return this.get('active');
}
set active(isActive: boolean) {
this.set('active', isActive);
}
get togglable(): boolean {
return this.get('togglable');
}
get runDefaultCommand(): boolean {
return this.get('runDefaultCommand');
}
get stopDefaultCommand(): boolean {
return this.get('stopDefaultCommand');
}
get disable(): boolean {
return this.get('disable');
}
constructor(module: PanelManager, options: any) {
super(module, options);
if (this.get('buttons').length) {
this.set('buttons', new Buttons(this.module, this.get('buttons')));
}
}
}

19
src/panels/model/Buttons.js → src/panels/model/Buttons.ts

@ -1,7 +1,11 @@
import { Collection } from '../../common';
import PanelManager from '..';
import { Collection } from '../../abstract';
import Button from './Button';
export default class Buttons extends Collection {
export default class Buttons extends Collection<Button> {
constructor(module: PanelManager, models: Button[]) {
super(module, models, Button);
}
/**
* Deactivate all buttons, except one passed
* @param {Object} except Model to ignore
@ -9,7 +13,7 @@ export default class Buttons extends Collection {
*
* @return void
* */
deactivateAllExceptOne(except, r) {
deactivateAllExceptOne(except: Button, r: boolean) {
this.forEach((model, index) => {
if (model !== except) {
model.set('active', false);
@ -24,11 +28,12 @@ export default class Buttons extends Collection {
*
* @return void
* */
deactivateAll(ctx, sender) {
deactivateAll(ctx?: string, sender?: any) {
const context = ctx || '';
this.forEach(model => {
if (model.get('context') == context && model !== sender) {
model.set('active', false, { fromCollection: 1 });
//@ts-ignore
model.set('active', false, { fromCollection: true });
}
});
}
@ -39,7 +44,7 @@ export default class Buttons extends Collection {
*
* @return void
* */
disableAllButtons(ctx) {
disableAllButtons(ctx?: string) {
var context = ctx || '';
this.forEach((model, index) => {
if (model.get('context') == context) {
@ -55,7 +60,7 @@ export default class Buttons extends Collection {
*
* @return void
* */
disableAllButtonsExceptOne(except, r) {
disableAllButtonsExceptOne(except: Button, r: boolean) {
this.forEach((model, index) => {
if (model !== except) {
model.set('disable', true);

20
src/panels/model/Panel.js

@ -1,20 +0,0 @@
import { Model } from '../../common';
import Buttons from './Buttons';
export default class Panel extends Model {
defaults() {
return {
id: '',
content: '',
visible: true,
buttons: [],
attributes: {},
};
}
initialize(options) {
this.btn = this.get('buttons') || [];
this.buttons = new Buttons(this.btn);
this.set('buttons', this.buttons);
}
}

31
src/panels/model/Panel.ts

@ -0,0 +1,31 @@
import PanelManager from '..';
import { Model } from '../../abstract';
import Buttons from './Buttons';
export default class Panel extends Model<PanelManager> {
defaults() {
return {
id: '',
content: '',
visible: true,
buttons: [],
attributes: {},
};
}
get buttons() {
return this.get('buttons');
}
private set buttons(buttons: Buttons) {
this.set('buttons', buttons);
}
view?: any;
constructor(module: PanelManager, options: any) {
super(module, options);
var btn = this.get('buttons') || [];
this.buttons = new Buttons(module, btn);
}
}

6
src/panels/model/Panels.js

@ -1,6 +0,0 @@
import { Collection } from '../../common';
import Panel from './Panel';
export default class Panels extends Collection {}
Panels.prototype.model = Panel;

11
src/panels/model/Panels.ts

@ -0,0 +1,11 @@
import PanelManager from '..';
import { Collection } from '../../abstract';
import Panel from './Panel';
export default class Panels extends Collection<Panel> {
constructor(module: PanelManager, models: Panel[] | Array<Record<string, any>>) {
super(module, models, Panel);
}
}
Panels.prototype.model = Panel;

70
src/panels/view/ButtonView.js → src/panels/view/ButtonView.ts

@ -1,7 +1,10 @@
import { isString, isObject, isFunction } from 'underscore';
import { View } from '../../common';
import { View } from '../../abstract';
import Button from '../model/Button';
import Buttons from '../model/Buttons';
export default class ButtonView extends View {
export default class ButtonView extends View<Button> {
//@ts-ignore
tagName() {
return this.model.get('tagName');
}
@ -12,18 +15,20 @@ export default class ButtonView extends View {
};
}
initialize(o) {
const { model } = this;
const cls = model.get('className');
commands: any;
activeCls: string;
disableCls: string;
btnsVisCls: string;
//Note: I don't think this is working
$buttons?: any;
constructor(o: any) {
super(o);
const { model, em, pfx, ppfx } = this;
const cls = model.className;
const { command, listen } = model.attributes;
const config = o.config || {};
const { em } = config;
this.config = config;
this.em = em;
const pfx = this.config.stylePrefix || '';
const ppfx = this.config.pStylePrefix || '';
this.pfx = pfx;
this.ppfx = this.config.pStylePrefix || '';
this.id = pfx + model.get('id');
this.activeCls = `${pfx}active ${ppfx}four-color`;
this.disableCls = `${ppfx}disabled`;
@ -38,7 +43,7 @@ export default class ButtonView extends View {
this.listenTo(model, 'change:disable', this.updateDisable);
if (em && isString(command) && listen) {
const chnOpt = { fromListen: 1 };
const chnOpt: any = { fromListen: true };
this.listenTo(em, `run:${command}`, () => model.set('active', true, chnOpt));
this.listenTo(em, `stop:${command}`, () => model.set('active', false, chnOpt));
}
@ -51,9 +56,9 @@ export default class ButtonView extends View {
*
* @return void
* */
updateClassName() {
private updateClassName() {
const { model, pfx } = this;
const cls = model.get('className');
const cls = model.className;
const attrCls = model.get('attributes').class;
const classStr = `${attrCls ? attrCls : ''} ${pfx}btn ${cls ? cls : ''}`;
this.$el.attr('class', classStr.trim());
@ -64,7 +69,7 @@ export default class ButtonView extends View {
*
* @return void
* */
updateAttributes() {
private updateAttributes() {
const { em, model, $el } = this;
const attr = model.get('attributes') || {};
const title = em && em.t && em.t(`panels.buttons.titles.${model.id}`);
@ -79,7 +84,7 @@ export default class ButtonView extends View {
*
* @return void
* */
updateBtnsVis() {
private updateBtnsVis() {
if (!this.$buttons) return;
if (this.model.get('bntsVis')) this.$buttons.addClass(this.btnsVisCls);
@ -91,12 +96,12 @@ export default class ButtonView extends View {
*
* @return void
* */
updateActive(m, v, opts = {}) {
private updateActive(m: any, v: any, opts: any = {}) {
const { model, commands, $el, activeCls } = this;
const { fromCollection, fromListen } = opts;
const context = model.get('context');
const options = model.get('options');
const commandName = model.get('command');
const commandName = model.command;
let command = {};
if (!commandName) return;
@ -109,12 +114,13 @@ export default class ButtonView extends View {
command = commands.create(commandName);
}
if (model.get('active')) {
!fromCollection && model.collection.deactivateAll(context, model);
if (model.active) {
!fromCollection && (model.collection as Buttons)?.deactivateAll(context, model);
model.set('active', true, { silent: true }).trigger('checkActive');
!fromListen && commands.runCommand(command, { ...options, sender: model });
// Disable button if the command has no stop method
//@ts-ignore
command.noStop && model.set('active', false);
} else {
$el.removeClass(activeCls);
@ -124,7 +130,7 @@ export default class ButtonView extends View {
updateDisable() {
const { disableCls, model } = this;
const disable = model.get('disable');
const disable = model.disable;
this.$el[disable ? 'addClass' : 'removeClass'](disableCls);
}
@ -135,7 +141,7 @@ export default class ButtonView extends View {
* */
checkActive() {
const { model, $el, activeCls } = this;
model.get('active') ? $el.addClass(activeCls) : $el.removeClass(activeCls);
model.active ? $el.addClass(activeCls) : $el.removeClass(activeCls);
}
/**
@ -144,31 +150,31 @@ export default class ButtonView extends View {
*
* @return void
* */
clicked(e) {
clicked(e: Event) {
const { model } = this;
if (model.get('bntsVis') || model.get('disable') || !model.get('command')) return;
if (model.get('bntsVis') || model.disable || !model.command) return;
this.toggleActive();
}
toggleActive() {
private toggleActive() {
const { model, em } = this;
const { active, togglable } = model.attributes;
const { active, togglable } = model;
if (active && !togglable) return;
model.set('active', !active);
model.active = !active;
// If the stop is requested
if (active) {
if (model.get('runDefaultCommand')) em.runDefault();
if (model.runDefaultCommand) em.runDefault();
} else {
if (model.get('stopDefaultCommand')) em.stopDefault();
if (model.stopDefaultCommand) em.stopDefault();
}
}
render() {
public render() {
const { model } = this;
const label = model.get('label');
const { $el } = this;

28
src/panels/view/ButtonsView.js → src/panels/view/ButtonsView.ts

@ -1,13 +1,12 @@
import { result } from 'underscore';
import { View } from '../../common';
import { View } from '../../abstract';
import Button from '../model/Button';
import Buttons from '../model/Buttons';
import ButtonView from './ButtonView';
export default class ButtonsView extends View {
initialize(o) {
this.opt = o || {};
this.config = this.opt.config || {};
this.pfx = this.config.stylePrefix || '';
this.parentM = this.opt.parentM || null;
export default class ButtonsView extends View<Buttons> {
constructor(collection: Buttons) {
super({ collection });
this.listenTo(this.collection, 'add', this.addTo);
this.listenTo(this.collection, 'reset remove', this.render);
this.className = this.pfx + 'buttons';
@ -19,7 +18,7 @@ export default class ButtonsView extends View {
*
* @return Object
* */
addTo(model) {
private addTo(model: Button) {
this.addToCollection(model);
}
@ -30,15 +29,12 @@ export default class ButtonsView extends View {
*
* @return Object Object created
* */
addToCollection(model, fragmentEl) {
private addToCollection(model: Button, fragmentEl?: DocumentFragment) {
const fragment = fragmentEl || null;
const viewObject = ButtonView;
const el = model.get('el');
const view = new viewObject({
const view = new ButtonView({
el,
model,
config: this.config,
parentM: this.parentM,
});
const rendered = view.render().el;
@ -51,13 +47,11 @@ export default class ButtonsView extends View {
return rendered;
}
render() {
public render() {
var fragment = document.createDocumentFragment();
this.$el.empty();
this.collection.each(function (model) {
this.addToCollection(model, fragment);
}, this);
this.collection.each(model => this.addToCollection(model, fragment));
this.$el.append(fragment);
this.$el.attr('class', result(this, 'className'));

29
src/panels/view/PanelView.js → src/panels/view/PanelView.ts

@ -1,14 +1,10 @@
import { View } from '../../common';
import { View } from '../../abstract';
import Panel from '../model/Panel';
import ButtonsView from './ButtonsView';
export default class PanelView extends View {
initialize(o) {
const config = o.config || {};
const model = this.model;
this.config = config;
this.pfx = config.stylePrefix || '';
this.ppfx = config.pStylePrefix || '';
this.buttons = model.get('buttons');
export default class PanelView extends View<Panel> {
constructor(model: Panel) {
super({ model, el: model.get('el') });
this.className = this.pfx + 'panel';
this.id = this.pfx + model.get('id');
this.listenTo(model, 'change:appendContent', this.appendContent);
@ -39,12 +35,13 @@ export default class PanelView extends View {
this.$el.removeClass(`${this.ppfx}hidden`);
}
//@ts-ignore
attributes() {
return this.model.get('attributes');
}
initResize() {
const em = this.config.em;
const em = this.em;
const editor = em ? em.get('Editor') : '';
const resizable = this.model.get('resizable');
@ -85,7 +82,7 @@ export default class PanelView extends View {
onEnd() {
em && em.trigger('change:canvasOffset');
},
posFetcher: (el, { target }) => {
posFetcher: (el: HTMLElement, { target }: any) => {
const style = el.style;
const config = resizer.getConfig();
const keyWidth = config.keyWidth;
@ -111,6 +108,7 @@ export default class PanelView extends View {
}
render() {
const { buttons } = this.model;
const $el = this.$el;
const ppfx = this.ppfx;
const cls = `${this.className} ${this.id} ${ppfx}one-bg ${ppfx}two-color`;
@ -118,12 +116,9 @@ export default class PanelView extends View {
this.toggleVisible();
if (this.buttons.length) {
var buttons = new ButtonsView({
collection: this.buttons,
config: this.config,
});
$el.append(buttons.render().el);
if (buttons.length) {
var buttonsView = new ButtonsView(buttons);
$el.append(buttonsView.render().el);
}
$el.append(this.model.get('content'));

33
src/panels/view/PanelsView.js → src/panels/view/PanelsView.ts

@ -1,19 +1,18 @@
import { View } from '../../common';
import { View } from '../../abstract';
import Panel from '../model/Panel';
import Panels from '../model/Panels';
import PanelView from './PanelView';
export default class PanelsView extends View {
initialize(o) {
this.opt = o || {};
this.config = this.opt.config || {};
this.pfx = this.config.stylePrefix || '';
const items = this.collection;
this.listenTo(items, 'add', this.addTo);
this.listenTo(items, 'reset', this.render);
this.listenTo(items, 'remove', this.onRemove);
export default class PanelsView extends View<Panels> {
constructor(target: Panels) {
super({ collection: target });
this.listenTo(target, 'add', this.addTo);
this.listenTo(target, 'reset', this.render);
this.listenTo(target, 'remove', this.onRemove);
this.className = this.pfx + 'panels';
}
onRemove(model) {
private onRemove(model: Panel) {
const view = model.view;
view && view.remove();
}
@ -25,7 +24,7 @@ export default class PanelsView extends View {
* @return Object
* @private
* */
addTo(model) {
private addTo(model: Panel) {
this.addToCollection(model);
}
@ -38,15 +37,11 @@ export default class PanelsView extends View {
* @return Object Object created
* @private
* */
addToCollection(model, fragmentEl) {
private addToCollection(model: Panel, fragmentEl?: DocumentFragment) {
const fragment = fragmentEl || null;
const config = this.config;
const el = model.get('el');
const view = new PanelView({
el,
model,
config,
});
const view = new PanelView(model);
const rendered = view.render().el;
const appendTo = model.get('appendTo');
@ -67,7 +62,7 @@ export default class PanelsView extends View {
return rendered;
}
render() {
public render() {
const $el = this.$el;
const frag = document.createDocumentFragment();
$el.empty();

2
src/utils/Sorter.js

@ -478,7 +478,7 @@ export default Backbone.View.extend({
},
isTextableActive(src, trg) {
return src && src.get && src.get('textable') && trg && trg.is('text');
return src?.get?.('textable') && trg?.isInstanceOf('text');
},
disableTextable() {

5
test/specs/modal/index.js

@ -1,11 +1,14 @@
import Modal from 'modal_dialog';
import Editor from 'editor';
describe('Modal dialog', () => {
describe('Main', () => {
var em;
var obj;
beforeEach(() => {
obj = new Modal().init();
em = new Editor({});
obj = new Modal(em);
});
afterEach(() => {

6
test/specs/modal/view/ModalView.js

@ -1,13 +1,15 @@
import ModalView from 'modal_dialog/view/ModalView';
import Modal from 'modal_dialog/model/Modal';
import Editor from 'editor';
describe('ModalView', () => {
var model;
var view;
var editorModel;
var em;
beforeEach(() => {
model = new Modal();
em = new Editor({});
model = new Modal(em);
view = new ModalView({
model,
});

12
test/specs/panels/index.js

@ -1,11 +1,15 @@
import Panels from 'panels';
import Panel from 'panels/model/Panel';
import Editor from 'editor/model/Editor';
describe('Panels', () => {
describe('Main', () => {
var em;
var obj;
beforeEach(() => {
obj = new Panels().init();
em = new Editor({});
obj = new Panels(em);
});
afterEach(() => {
@ -31,7 +35,7 @@ describe('Panels', () => {
});
test('Adds new panel correctly via Panel instance', () => {
var oPanel = new obj.Panel({ id: 'test' });
var oPanel = new Panel(obj, { id: 'test' });
var panel = obj.addPanel(oPanel);
expect(panel).toEqual(oPanel);
expect(panel.get('id')).toEqual('test');
@ -127,7 +131,7 @@ describe('Panels', () => {
});
test('Removes panel correctly via Panel instance', () => {
var oPanel = new obj.Panel({ id: 'test' });
var oPanel = new Panel(obj, { id: 'test' });
var panel = obj.addPanel(oPanel);
expect(panel).toEqual(oPanel);
expect(panel.get('id')).toEqual('test');
@ -136,7 +140,7 @@ describe('Panels', () => {
});
test('Removes panel correctly via id', () => {
var oPanel = new obj.Panel({ id: 'test' });
var oPanel = new Panel(obj, { id: 'test' });
var panel = obj.addPanel(oPanel);
expect(panel).toEqual(oPanel);
expect(panel.get('id')).toEqual('test');

4
test/specs/panels/model/PanelModels.js

@ -22,7 +22,7 @@ describe('Button', () => {
});
test('Init with other buttons inside correctly', () => {
obj = new Button({
obj = new Button(null, {
buttons: [{}],
});
expect(obj.get('buttons') instanceof Buttons).toEqual(true);
@ -109,7 +109,7 @@ describe('Panel', () => {
});
test('Init with buttons inside correctly', () => {
obj = new Panel({
obj = new Panel(null, {
buttons: [{}],
});
expect(obj.get('buttons') instanceof Buttons).toEqual(true);

11
test/specs/panels/view/ButtonView.js

@ -1,14 +1,17 @@
import ButtonView from 'panels/view/ButtonView';
import Button from 'panels/model/Button';
import Editor from 'editor';
describe('ButtonView', () => {
var fixtures;
var em;
var model;
var view;
var btnClass = 'btn';
var btnClass = 'gjs-pn-btn';
beforeEach(() => {
model = new Button({ command: 'fake-command' });
em = new Editor({});
model = new Button(em.Panels, { command: 'fake-command' });
view = new ButtonView({
model: model,
});
@ -40,7 +43,7 @@ describe('ButtonView', () => {
test('Check enable active', () => {
model.set('active', true, { silent: true });
view.checkActive();
expect(view.el.getAttribute('class')).toContain(btnClass + ' active');
expect(view.el.getAttribute('class')).toContain(btnClass + ' gjs-pn-active');
});
test('Check disable active', () => {
@ -54,7 +57,7 @@ describe('ButtonView', () => {
test('Disable the button', () => {
model.set('disable', true, { silent: true });
view.updateDisable();
expect(view.el.getAttribute('class')).toEqual(btnClass + ' disabled');
expect(view.el.getAttribute('class')).toEqual(btnClass + ' gjs-disabled');
});
test('Enable the disabled button', () => {

9
test/specs/panels/view/ButtonsView.js

@ -1,16 +1,17 @@
import ButtonsView from 'panels/view/ButtonsView';
import Buttons from 'panels/model/Buttons';
import Editor from 'editor';
describe('ButtonsView', () => {
var fixtures;
var em;
var model;
var view;
beforeEach(() => {
model = new Buttons([]);
view = new ButtonsView({
collection: model,
});
em = new Editor({});
model = new Buttons(em.Panels, []);
view = new ButtonsView(model);
document.body.innerHTML = '<div id="fixtures"></div>';
fixtures = document.body.querySelector('#fixtures');
fixtures.appendChild(view.render().el);

21
test/specs/panels/view/PanelView.js

@ -1,16 +1,17 @@
import PanelView from 'panels/view/PanelView';
import Panel from 'panels/model/Panel';
import Editor from 'editor';
describe('PanelView', () => {
var fixtures;
var em;
var model;
var view;
beforeEach(() => {
model = new Panel();
view = new PanelView({
model,
});
em = new Editor();
model = new Panel(em.Panels);
view = new PanelView(model);
document.body.innerHTML = '<div id="fixtures"></div>';
fixtures = document.body.querySelector('#fixtures');
fixtures.appendChild(view.render().el);
@ -38,16 +39,16 @@ describe('PanelView', () => {
});
test('Hide panel', () => {
expect(view.$el.hasClass('hidden')).toBeFalsy();
expect(view.$el.hasClass('gjs-hidden')).toBeFalsy();
model.set('visible', false);
expect(view.$el.hasClass('hidden')).toBeTruthy();
expect(view.$el.hasClass('gjs-hidden')).toBeTruthy();
});
test('Show panel', () => {
model.set('visible', false);
expect(view.$el.hasClass('hidden')).toBeTruthy();
expect(view.$el.hasClass('gjs-hidden')).toBeTruthy();
model.set('visible', true);
expect(view.$el.hasClass('hidden')).toBeFalsy();
expect(view.$el.hasClass('gjs-hidden')).toBeFalsy();
});
describe('Init with options', () => {
@ -55,9 +56,7 @@ describe('PanelView', () => {
model = new Panel({
buttons: [{}],
});
view = new PanelView({
model,
});
view = new PanelView(model);
document.body.innerHTML = '<div id="fixtures"></div>';
fixtures = document.body.querySelector('#fixtures');
fixtures.appendChild(view.render().el);

10
test/specs/panels/view/PanelsView.js

@ -1,17 +1,17 @@
import PanelsView from 'panels/view/PanelsView';
import Panels from 'panels/model/Panels';
import Editor from 'editor';
describe('PanelsView', () => {
var fixtures;
var $fixture;
var em;
var model;
var view;
beforeEach(() => {
model = new Panels([]);
view = new PanelsView({
collection: model,
});
em = new Editor({});
model = new Panels(em.Panels);
view = new PanelsView(model);
document.body.innerHTML = '<div id="fixtures"></div>';
fixtures = document.body.querySelector('#fixtures');
fixtures.appendChild(view.render().el);

13
webpack.config.js

@ -1,7 +1,6 @@
import path from 'path';
import webpack from 'webpack';
import pkg from './package.json';
import TerserPlugin from 'terser-webpack-plugin';
const rootDir = path.resolve(__dirname);
@ -12,18 +11,6 @@ export default ({ config }) => ({
filename: 'grapes.min.js',
libraryExport: 'default',
},
optimization: {
minimizer: [new TerserPlugin({
extractComments: false,
terserOptions: {
output: {
comments: false,
quote_style: 3, // Preserve original quotes
preamble: `/*! grapesjs - ${pkg.version} */`,
}
}
})],
},
devServer: {
...config.devServer,
static: [rootDir],

6581
yarn.lock

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