@print-one/grapesjs
Version:
Free and Open Source Web Builder Framework
267 lines (246 loc) • 8.29 kB
text/typescript
import { bindAll, isNumber, isNull, debounce } from 'underscore';
import { ModuleView } from '../../abstract';
import FrameView from './FrameView';
import { createEl, removeEl } from '../../utils/dom';
import Dragger from '../../utils/Dragger';
import CanvasView from './CanvasView';
import Frame from '../model/Frame';
export default class FrameWrapView extends ModuleView<Frame> {
events() {
return {
'click [data-action-remove]': 'remove',
'mousedown [data-action-move]': 'startDrag',
};
}
elTools?: HTMLElement;
frame: FrameView;
dragger?: Dragger;
cv: CanvasView;
classAnim: string;
sizeObserver?: ResizeObserver;
constructor(model: Frame, canvasView: CanvasView) {
super({ model });
bindAll(this, 'onScroll', 'frameLoaded', 'updateOffset', 'remove', 'startDrag');
const config = {
...model.config,
frameWrapView: this,
};
this.cv = canvasView;
this.frame = new FrameView(model, this);
this.classAnim = `${this.ppfx}frame-wrapper--anim`;
this.updateOffset = debounce(this.updateOffset.bind(this), 0);
this.updateSize = debounce(this.updateSize.bind(this), 0);
this.listenTo(model, 'loaded', this.frameLoaded);
this.listenTo(model, 'change:x change:y', this.updatePos);
this.listenTo(model, 'change:width change:height', this.updateSize);
this.listenTo(model, 'destroy remove', this.remove);
this.updatePos();
this.setupDragger();
}
setupDragger() {
const { module, model } = this;
let dragX: number, dragY: number, zoom: number;
const toggleEffects = (on: boolean) => {
module.toggleFramesEvents(on);
};
this.dragger = new Dragger({
onStart: () => {
const { x, y } = model.attributes;
zoom = this.em.getZoomMultiplier();
dragX = x;
dragY = y;
toggleEffects(false);
},
onEnd: () => toggleEffects(true),
setPosition: (posOpts: any) => {
model.set({
x: dragX + posOpts.x * zoom,
y: dragY + posOpts.y * zoom,
});
},
});
}
startDrag(ev?: Event) {
ev && this.dragger?.start(ev);
}
__clear(opts?: any) {
const { frame } = this;
frame && frame.remove(opts);
removeEl(this.elTools);
}
remove(opts?: any) {
this.__clear(opts);
ModuleView.prototype.remove.apply(this, opts);
//@ts-ignore
['frame', 'dragger', 'cv', 'elTools'].forEach(i => (this[i] = 0));
return this;
}
updateOffset() {
const { em, $el, frame } = this;
if (!em || em.destroyed) return;
em.runDefault({ preserveSelected: 1 });
$el.removeClass(this.classAnim);
frame?.model?._emitUpdated();
}
updatePos(md?: boolean) {
const { model, el } = this;
const { x, y } = model.attributes;
const { style } = el;
this.frame.rect = undefined;
style.left = isNaN(x) ? x : `${x}px`;
style.top = isNaN(y) ? y : `${y}px`;
md && this.updateOffset();
}
updateSize() {
this.updateDim();
}
/**
* Update dimensions of the frame
* @private
*/
updateDim() {
const { em, el, $el, model, classAnim, frame } = this;
if (!frame) return;
frame.rect = undefined;
$el.addClass(classAnim);
const { noChanges, width, height } = this.__handleSize();
// Set width and height from DOM (should be done only once)
if (isNull(width) || isNull(height)) {
model.set(
{
...(!width ? { width: el.offsetWidth } : {}),
...(!height ? { height: el.offsetHeight } : {}),
},
{ silent: 1 }
);
}
// Prevent fixed highlighting box which appears when on
// component hover during the animation
em.stopDefault({ preserveSelected: 1 });
noChanges ? this.updateOffset() : setTimeout(this.updateOffset, 350);
}
onScroll() {
const { frame, em } = this;
em.trigger('frame:scroll', {
frame,
body: frame.getBody(),
target: frame.getWindow(),
});
}
frameLoaded() {
const { frame, config } = this;
frame.getWindow().onscroll = this.onScroll;
this.updateDim();
}
__handleSize() {
const un = 'px';
const { model, el } = this;
const { style } = el;
const { width, height } = model.attributes;
const currW = style.width || '';
const currH = style.height || '';
const newW = width || '';
const newH = height || '';
const noChanges = currW == newW && currH == newH;
const newWidth = isNumber(newW) ? `${newW}${un}` : newW;
const newHeight = isNumber(newH) ? `${newH}${un}` : newH;
style.width = newWidth;
if (model.hasAutoHeight()) {
const iframe = this.frame.el;
if (
iframe.contentDocument
// this doesn't work always
// && !this.sizeObserver
) {
const { contentDocument } = iframe;
const observer = new ResizeObserver(() => {
style.height = `${contentDocument.body.scrollHeight}px`;
});
observer.observe(contentDocument.body);
this.sizeObserver?.disconnect();
this.sizeObserver = observer;
}
} else {
style.height = newHeight;
this.sizeObserver?.disconnect();
delete this.sizeObserver;
}
return { noChanges, width, height, newW, newH };
}
render() {
const { frame, $el, ppfx, cv, model, el } = this;
const { onRender } = model.attributes;
this.__clear();
this.__handleSize();
frame.render();
$el
.empty()
.attr({ class: `${ppfx}frame-wrapper` })
.append(
`
<div class="${ppfx}frame-wrapper__top gjs-two-color" data-frame-top>
<div class="${ppfx}frame-wrapper__name" data-action-move>
${model.get('name') || ''}
</div>
<div class="${ppfx}frame-wrapper__top-r">
<div class="${ppfx}frame-wrapper__icon" data-action-remove style="display: none">
<svg viewBox="0 0 24 24"><path d="M19 4h-3.5l-1-1h-5l-1 1H5v2h14M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12z"></path></svg>
</div>
</div>
</div>
<div class="${ppfx}frame-wrapper__right" data-frame-right></div>
<div class="${ppfx}frame-wrapper__left" data-frame-left></div>
<div class="${ppfx}frame-wrapper__bottom" data-frame-bottom></div>
`
)
.append(frame.el);
const elTools = createEl(
'div',
{
class: `${ppfx}tools`,
style: 'pointer-events:none; display: none',
},
`
<div class="${ppfx}highlighter" data-hl></div>
<div class="${ppfx}badge" data-badge></div>
<div class="${ppfx}placeholder">
<div class="${ppfx}placeholder-int"></div>
</div>
<div class="${ppfx}ghost"></div>
<div class="${ppfx}toolbar" style="pointer-events:all"></div>
<div class="${ppfx}resizer"></div>
<div class="${ppfx}offset-v" data-offset>
<div class="gjs-marginName" data-offset-m>
<div class="gjs-margin-v-el gjs-margin-v-top" data-offset-m-t></div>
<div class="gjs-margin-v-el gjs-margin-v-bottom" data-offset-m-b></div>
<div class="gjs-margin-v-el gjs-margin-v-left" data-offset-m-l></div>
<div class="gjs-margin-v-el gjs-margin-v-right" data-offset-m-r></div>
</div>
<div class="gjs-paddingName" data-offset-m>
<div class="gjs-padding-v-el gjs-padding-v-top" data-offset-p-t></div>
<div class="gjs-padding-v-el gjs-padding-v-bottom" data-offset-p-b></div>
<div class="gjs-padding-v-el gjs-padding-v-left" data-offset-p-l></div>
<div class="gjs-padding-v-el gjs-padding-v-right" data-offset-p-r></div>
</div>
</div>
<div class="${ppfx}offset-fixed-v"></div>
`
);
this.elTools = elTools;
const twrp = cv?.toolsWrapper;
twrp && twrp.appendChild(elTools); // TODO remove on frame remove
onRender &&
onRender({
el,
elTop: el.querySelector('[data-frame-top]'),
elRight: el.querySelector('[data-frame-right]'),
elBottom: el.querySelector('[data-frame-bottom]'),
elLeft: el.querySelector('[data-frame-left]'),
frame: model,
frameWrapperView: this,
remove: this.remove,
startDrag: this.startDrag,
});
return this;
}
}