@print-one/grapesjs
Version:
Free and Open Source Web Builder Framework
509 lines (441 loc) • 13.4 kB
text/typescript
import { bindAll, debounce, isString, isUndefined } from 'underscore';
import { ModuleView } from '../../abstract';
import { BoxRect } from '../../common';
import CssRulesView from '../../css_composer/view/CssRulesView';
import ComponentWrapperView from '../../dom_components/view/ComponentWrapperView';
import Droppable from '../../utils/Droppable';
import {
append,
appendVNodes,
createCustomEvent,
createEl,
getPointerEvent,
motionsEv,
off,
on,
} from '../../utils/dom';
import { hasDnd, setViewEl } from '../../utils/mixins';
import Canvas from '../model/Canvas';
import Frame from '../model/Frame';
import FrameWrapView from './FrameWrapView';
export default class FrameView extends ModuleView<Frame, HTMLIFrameElement> {
/** @ts-ignore */
get tagName() {
return 'iframe';
}
/** @ts-ignore */
get attributes() {
return { allowfullscreen: 'allowfullscreen' };
}
dragging = false;
loaded = false;
droppable?: Droppable;
rect?: DOMRect;
lastClientY?: number;
lastMaxHeight = 0;
private jsContainer?: HTMLElement;
private tools: { [key: string]: HTMLElement } = {};
private wrapper?: ComponentWrapperView;
private frameWrapView?: FrameWrapView;
constructor(model: Frame, view?: FrameWrapView) {
super({ model });
bindAll(this, 'updateClientY', 'stopAutoscroll', 'autoscroll', '_emitUpdate');
const { el } = this;
//@ts-ignore
this.module._config = {
...(this.config || {}),
//@ts-ignore
frameView: this,
//canvasView: view?.cv
};
this.frameWrapView = view;
this.showGlobalTools = debounce(this.showGlobalTools.bind(this), 50);
const cvModel = this.getCanvasModel();
this.listenTo(model, 'change:head', this.updateHead);
this.listenTo(cvModel, 'change:styles', this.renderStyles);
model.view = this;
setViewEl(el, this);
}
getBoxRect(): BoxRect {
const { el, module } = this;
const canvasView = module.getCanvasView();
const coords = module.getCoords();
const frameRect = el.getBoundingClientRect();
const canvasRect = canvasView.getCanvasOffset();
const vwDelta = canvasView.getViewportDelta();
const zoomM = module.getZoomMultiplier();
const x = (frameRect.x - canvasRect.left - vwDelta.x - coords.x) * zoomM;
const y = (frameRect.y - canvasRect.top - vwDelta.y - coords.y) * zoomM;
const width = frameRect.width * zoomM;
const height = frameRect.height * zoomM;
return {
x,
y,
width,
height,
};
}
/**
* Update `<head>` content of the frame
*/
updateHead() {
const { model } = this;
const headEl = this.getHead();
const toRemove: any[] = [];
const toAdd: any[] = [];
const current = model.head;
const prev = model.previous('head');
const attrStr = (attr: any = {}) =>
Object.keys(attr)
.sort()
.map(i => `[${i}="${attr[i]}"]`)
.join('');
const find = (items: any[], stack: any[], res: any[]) => {
items.forEach(item => {
const { tag, attributes } = item;
const has = stack.some(s => s.tag === tag && attrStr(s.attributes) === attrStr(attributes));
!has && res.push(item);
});
};
find(current, prev, toAdd);
find(prev, current, toRemove);
toRemove.forEach(stl => {
const el = headEl.querySelector(`${stl.tag}${attrStr(stl.attributes)}`);
el?.parentNode?.removeChild(el);
});
appendVNodes(headEl, toAdd);
}
getEl() {
return this.el;
}
getCanvasModel(): Canvas {
return this.em.Canvas.getModel();
}
getWindow() {
return this.getEl().contentWindow as Window;
}
getDoc() {
return this.getEl().contentDocument as Document;
}
getHead() {
return this.getDoc().querySelector('head') as HTMLHeadElement;
}
getBody() {
return this.getDoc().querySelector('body') as HTMLBodyElement;
}
getWrapper() {
return this.getBody().querySelector('[data-gjs-type=wrapper]') as HTMLElement;
}
getJsContainer() {
if (!this.jsContainer) {
this.jsContainer = createEl('div', { class: `${this.ppfx}js-cont` });
}
return this.jsContainer;
}
getToolsEl() {
return this.frameWrapView?.elTools as HTMLElement;
}
getGlobalToolsEl() {
return this.em.Canvas.getGlobalToolsEl()!;
}
getHighlighter() {
return this._getTool('[data-hl]');
}
getBadgeEl() {
return this._getTool('[data-badge]');
}
getOffsetViewerEl() {
return this._getTool('[data-offset]');
}
getRect() {
if (!this.rect) {
this.rect = this.el.getBoundingClientRect();
}
return this.rect;
}
/**
* Get rect data, not affected by the canvas zoom
*/
getOffsetRect() {
const { el } = this;
const { scrollTop, scrollLeft } = this.getBody();
const height = el.offsetHeight;
const width = el.offsetWidth;
return {
top: el.offsetTop,
left: el.offsetLeft,
height,
width,
scrollTop,
scrollLeft,
scrollBottom: scrollTop + height,
scrollRight: scrollLeft + width,
};
}
_getTool(name: string) {
const { tools } = this;
const toolsEl = this.getToolsEl();
if (!tools[name]) {
tools[name] = toolsEl.querySelector(name) as HTMLElement;
}
return tools[name];
}
remove(...args: any) {
this._toggleEffects(false);
this.tools = {};
this.wrapper?.remove();
ModuleView.prototype.remove.apply(this, args);
return this;
}
startAutoscroll() {
this.lastMaxHeight = this.getWrapper().offsetHeight - this.el.offsetHeight;
// By detaching those from the stack avoid browsers lags
// Noticeable with "fast" drag of blocks
setTimeout(() => {
this._toggleAutoscrollFx(true);
requestAnimationFrame(this.autoscroll);
}, 0);
}
autoscroll() {
if (this.dragging) {
const { lastClientY } = this;
const canvas = this.em.Canvas;
const win = this.getWindow();
const actualTop = win.pageYOffset;
const clientY = lastClientY || 0;
const limitTop = canvas.getConfig().autoscrollLimit!;
const limitBottom = this.getRect().height - limitTop;
let nextTop = actualTop;
if (clientY < limitTop) {
nextTop -= limitTop - clientY;
}
if (clientY > limitBottom) {
nextTop += clientY - limitBottom;
}
if (
!isUndefined(lastClientY) && // Fixes #3134
nextTop !== actualTop &&
nextTop > 0 &&
nextTop < this.lastMaxHeight
) {
const toolsEl = this.getGlobalToolsEl();
toolsEl.style.opacity = '0';
this.showGlobalTools();
win.scrollTo(0, nextTop);
canvas.spots.refreshDbn();
}
requestAnimationFrame(this.autoscroll);
}
}
updateClientY(ev: Event) {
ev.preventDefault();
this.lastClientY = getPointerEvent(ev).clientY * this.em.getZoomDecimal();
}
showGlobalTools() {
this.getGlobalToolsEl().style.opacity = '';
}
stopAutoscroll() {
this.dragging && this._toggleAutoscrollFx(false);
}
_toggleAutoscrollFx(enable: boolean) {
this.dragging = enable;
const win = this.getWindow();
const method = enable ? 'on' : 'off';
const mt = { on, off };
mt[method](win, 'mousemove dragover', this.updateClientY);
mt[method](win, 'mouseup', this.stopAutoscroll);
}
render() {
const { $el, ppfx, em } = this;
$el.attr({ class: `${ppfx}frame` });
this.renderScripts();
em.trigger('frame:render', this);
return this;
}
renderScripts() {
const { el, model, em } = this;
const evLoad = 'frame:load';
const evOpts = { el, model, view: this };
const canvas = this.getCanvasModel();
const appendScript = (scripts: any[]) => {
if (scripts.length > 0) {
const src = scripts.shift();
const scriptEl = createEl('script', {
type: 'text/javascript',
...(isString(src) ? { src } : src),
});
scriptEl.onerror = scriptEl.onload = appendScript.bind(null, scripts);
el.contentDocument?.head.appendChild(scriptEl);
} else {
this.renderBody();
em && em.trigger(evLoad, evOpts);
}
};
el.onload = () => {
const { frameContent } = this.config;
if (frameContent) {
const doc = this.getDoc();
doc.open();
doc.write(frameContent);
doc.close();
}
em && em.trigger(`${evLoad}:before`, evOpts);
appendScript([...canvas.get('scripts')]);
};
}
renderStyles(opts: any = {}) {
const head = this.getHead();
const canvas = this.getCanvasModel();
const normalize = (stls: any[]) =>
stls.map(href => ({
tag: 'link',
attributes: {
rel: 'stylesheet',
...(isString(href) ? { href } : href),
},
}));
const prevStyles = normalize(opts.prev || canvas.previous('styles'));
const styles = normalize(canvas.get('styles'));
const toRemove: any[] = [];
const toAdd: any[] = [];
const find = (items: any[], stack: any[], res: any[]) => {
items.forEach(item => {
const { href } = item.attributes;
const has = stack.some(s => s.attributes.href === href);
!has && res.push(item);
});
};
find(styles, prevStyles, toAdd);
find(prevStyles, styles, toRemove);
toRemove.forEach(stl => {
const el = head.querySelector(`link[href="${stl.attributes.href}"]`);
el?.parentNode?.removeChild(el);
});
appendVNodes(head, toAdd);
}
renderBody() {
const { config, em, model, ppfx } = this;
const doc = this.getDoc();
const body = this.getBody();
const win = this.getWindow();
const hasAutoHeight = model.hasAutoHeight();
const conf = em.config;
//@ts-ignore This could be used inside component-related scripts to check if the
// script is executed inside the editor.
win._isEditor = true;
this.renderStyles({ prev: [] });
const colorWarn = '#ffca6f';
append(
body,
`<style>
${conf.baseCss || config.frameStyle || ''}
${hasAutoHeight ? 'body { overflow: hidden }' : ''}
[data-gjs-type="wrapper"] {
${!hasAutoHeight ? 'min-height: 100vh;' : ''}
padding-top: 0.001em;
}
.${ppfx}dashed *[data-gjs-highlightable] {
outline: 1px dashed rgba(170,170,170,0.7);
outline-offset: -2px;
}
.${ppfx}selected {
outline: 2px solid #3b97e3 !important;
outline-offset: -2px;
}
.${ppfx}selected-parent {
outline: 2px solid ${colorWarn} !important
}
.${ppfx}no-select {
user-select: none;
-webkit-user-select:none;
-moz-user-select: none;
}
.${ppfx}freezed {
opacity: 0.5;
pointer-events: none;
}
.${ppfx}no-pointer {
pointer-events: none;
}
.${ppfx}plh-image {
background: #f5f5f5;
border: none;
height: 100px;
width: 100px;
display: block;
outline: 3px solid #ffca6f;
cursor: pointer;
outline-offset: -2px
}
.${ppfx}grabbing {
cursor: grabbing;
cursor: -webkit-grabbing;
}
.${ppfx}is__grabbing {
overflow-x: hidden;
}
.${ppfx}is__grabbing,
.${ppfx}is__grabbing * {
cursor: grabbing !important;
}
${conf.canvasCss || ''}
${conf.protectedCss || ''}
</style>`
);
const { root } = model;
const { view } = em.Components.getType('wrapper')!;
this.wrapper = new view({
model: root,
config: {
...root.config,
em,
frameView: this,
},
}).render();
append(body, this.wrapper?.el!);
append(
body,
new CssRulesView({
collection: model.getStyles(),
//@ts-ignore
config: {
...em.Css.getConfig(),
frameView: this,
},
}).render().el
);
append(body, this.getJsContainer());
// em.trigger('loaded'); // I need to manage only the first one maybe
//this.updateOffset(); // TOFIX (check if I need it)
// Avoid some default behaviours
//@ts-ignore
on(body, 'click', ev => ev && ev.target?.tagName == 'A' && ev.preventDefault());
on(body, 'submit', ev => ev && ev.preventDefault());
// When the iframe is focused the event dispatcher is not the same so
// I need to delegate all events to the parent document
[
{ event: 'keydown keyup keypress', class: 'KeyboardEvent' },
{ event: 'mousedown mousemove mouseup', class: 'MouseEvent' },
{ event: 'pointerdown pointermove pointerup', class: 'PointerEvent' },
{ event: 'wheel', class: 'WheelEvent', opts: { passive: !config.infiniteCanvas } },
].forEach(obj =>
obj.event.split(' ').forEach(event => {
doc.addEventListener(event, ev => this.el.dispatchEvent(createCustomEvent(ev, obj.class)), obj.opts);
})
);
this._toggleEffects(true);
if (hasDnd(em)) {
this.droppable = new Droppable(em, this.wrapper?.el);
}
this.loaded = true;
model.trigger('loaded');
}
_toggleEffects(enable: boolean) {
const method = enable ? on : off;
const win = this.getWindow();
win && method(win, `${motionsEv} resize`, this._emitUpdate);
}
_emitUpdate() {
this.model._emitUpdated();
}
}