UNPKG

grapesjs_codeapps

Version:

Free and Open Source Web Builder Framework/SC Modification

385 lines (345 loc) 11.1 kB
import Backbone from 'backbone'; import { on, off, getElement } from 'utils/mixins'; const FrameView = require('./FrameView'); const $ = Backbone.$; module.exports = Backbone.View.extend({ initialize(o) { _.bindAll(this, 'renderBody', 'onFrameScroll', 'clearOff'); on(window, 'scroll resize', this.clearOff); this.config = o.config || {}; this.em = this.config.em || {}; this.ppfx = this.config.pStylePrefix || ''; this.className = this.config.stylePrefix + 'canvas'; this.listenTo(this.em, 'change:canvasOffset', this.clearOff); this.frame = new FrameView({ model: this.model.get('frame'), config: this.config }); }, /** * Checks if the element is visible in the canvas's viewport * @param {HTMLElement} el * @return {Boolean} */ isElInViewport(el) { const rect = getElement(el).getBoundingClientRect(); const frameRect = this.getFrameOffset(1); const rTop = rect.top; const rLeft = rect.left; return ( rTop >= 0 && rLeft >= 0 && rTop <= frameRect.height && rLeft <= frameRect.width ); }, /** * Update tools position * @private */ onFrameScroll() { var u = 'px'; var body = this.frame.el.contentDocument.body; this.toolsEl.style.top = '-' + body.scrollTop + u; this.toolsEl.style.left = '-' + body.scrollLeft + u; this.em.trigger('canvasScroll'); }, /** * Insert scripts into head, it will call renderBody after all scripts loaded or failed * @private */ renderScripts() { var frame = this.frame; var that = this; frame.el.onload = () => { var scripts = that.config.scripts.slice(0), // clone counter = 0; function appendScript(scripts) { if (scripts.length > 0) { var script = document.createElement('script'); script.type = 'text/javascript'; script.src = scripts.shift(); script.onerror = script.onload = appendScript.bind(null, scripts); frame.el.contentDocument.head.appendChild(script); } else { that.renderBody(); } } appendScript(scripts); }; }, /** * Render inside frame's body * @private */ renderBody() { const { config, model } = this; const wrap = this.model.get('frame').get('wrapper'); const em = config.em; if (wrap) { const Canvas = em.get('Canvas'); const ppfx = this.ppfx; const body = $(Canvas.getBody()); const head = $(Canvas.getDocument().head); const cssc = em.get('CssComposer'); const conf = em.get('Config'); let externalStyles = ''; config.styles.forEach(style => { externalStyles += `<link rel="stylesheet" href="${style}"/>`; }); const colorWarn = '#ffca6f'; // I need all this styles to make the editor work properly // Remove `html { height: 100%;}` from the baseCss as it gives jumpings // effects (on ENTER) with RTE like CKEditor (maybe some bug there?!?) // With `body {height: auto;}` jumps in CKEditor are removed but in // Firefox is impossible to drag stuff in empty canvas, so bring back // `body {height: 100%;}`. // For the moment I give the priority to Firefox as it might be // CKEditor's issue var frameCss = ` ${em.config.baseCss || ''} .${ppfx}dashed *[data-highlightable] { outline: 1px dashed rgba(170,170,170,0.7); outline-offset: -2px; } .${ppfx}comp-selected { outline: 3px solid #3b97e3 !important; outline-offset: -3px; } .${ppfx}comp-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: 50px; width: 50px; display: block; outline: 3px solid #ffca6f; cursor: pointer; outline-offset: -2px } .${ppfx}grabbing { cursor: grabbing; cursor: -webkit-grabbing; } ${conf.canvasCss || ''} ${conf.protectedCss || ''} `; if (externalStyles) { head.append(externalStyles); } body.append('<style>' + frameCss + '</style>'); body.append(wrap.render()).append(cssc.render()); body.append(this.getJsContainer()); em.trigger('loaded'); this.frame.el.contentWindow.onscroll = this.onFrameScroll; this.frame.udpateOffset(); // When the iframe is focused the event dispatcher is not the same so // I need to delegate all events to the parent document const doc = document; const fdoc = this.frame.el.contentDocument; // Unfortunately just creating `KeyboardEvent(e.type, e)` is not enough, // the keyCode/which will be always `0`. Even if it's an old/deprecated // property keymaster (and many others) still use it... using `defineProperty` // hack seems the only way const createCustomEvent = (e, cls) => { let oEvent; try { oEvent = new window[cls](e.type, e); } catch (e) { oEvent = document.createEvent(cls); oEvent.initEvent(e.type, true, true); } oEvent.keyCodeVal = e.keyCode; ['keyCode', 'which'].forEach(prop => { Object.defineProperty(oEvent, prop, { get() { return this.keyCodeVal; } }); }); return oEvent; }; [ { event: 'keydown keyup', class: 'KeyboardEvent' } //{ event: 'mousedown mousemove mouseup', class: 'MouseEvent' }, ].forEach(obj => obj.event.split(' ').forEach(event => { fdoc.addEventListener(event, e => doc.dispatchEvent(createCustomEvent(e, obj.class)) ); }) ); } }, /** * Get the offset of the element * @param {HTMLElement} el * @return {Object} */ offset(el) { var rect = el.getBoundingClientRect(); var docBody = el.ownerDocument.body; return { top: rect.top + docBody.scrollTop, left: rect.left + docBody.scrollLeft, width: rect.width, height: rect.height }; }, /** * Cleare cached offsets * @private */ clearOff() { this.frmOff = null; this.cvsOff = null; }, /** * Return frame offset * @return {Object} * @private */ getFrameOffset(force = 0) { if (!this.frmOff || force) this.frmOff = this.offset(this.frame.el); return this.frmOff; }, /** * Return canvas offset * @return {Object} * @private */ getCanvasOffset() { if (!this.cvsOff) this.cvsOff = this.offset(this.el); return this.cvsOff; }, /** * Returns element's data info * @param {HTMLElement} el * @return {Object} * @private */ getElementPos(el, opts) { var opt = opts || {}; var frmOff = this.getFrameOffset(); var cvsOff = this.getCanvasOffset(); var eo = this.offset(el); var frmTop = opt.avoidFrameOffset ? 0 : frmOff.top; var frmLeft = opt.avoidFrameOffset ? 0 : frmOff.left; const top = eo.top + frmTop - cvsOff.top; const left = eo.left + frmLeft - cvsOff.left; // clientHeight/clientWidth are for SVGs const height = el.offsetHeight || el.clientHeight; const width = el.offsetWidth || el.clientWidth; return { top, left, height, width }; }, /** * Returns position data of the canvas element * @return {Object} obj Position object * @private */ getPosition() { const doc = this.frame.el.contentDocument; if (!doc) return; const bEl = doc.body; const fo = this.getFrameOffset(); const co = this.getCanvasOffset(); return { top: fo.top + bEl.scrollTop - co.top, left: fo.left + bEl.scrollLeft - co.left }; }, /** * Update javascript of a specific component passed by its View * @param {View} view Component's View * @private */ updateScript(view) { if (!view.scriptContainer) { view.scriptContainer = $('<div>'); this.getJsContainer().appendChild(view.scriptContainer.get(0)); } const model = view.model; const id = model.getId(); view.el.id = id; view.scriptContainer.html(''); // In editor, I make use of setTimeout as during the append process of elements // those will not be available immediately, therefore 'item' variable const script = document.createElement('script'); script.innerHTML = ` setTimeout(function() { var item = document.getElementById('${id}'); if (!item) return; (function(){ ${model.getScriptString()}; }.bind(item))() }, 1);`; // #873 // Adding setTimeout will make js components work on init of the editor setTimeout(() => view.scriptContainer.get(0).appendChild(script), 0); }, /** * Get javascript container * @private */ getJsContainer() { if (!this.jsContainer) { this.jsContainer = $(`<div class="${this.ppfx}js-cont">`).get(0); } return this.jsContainer; }, render() { this.wrapper = this.model.get('wrapper'); if (this.wrapper && typeof this.wrapper.render == 'function') { this.model.get('frame').set('wrapper', this.wrapper); this.$el.append(this.frame.render().el); var frame = this.frame; if (this.config.scripts.length === 0) { frame.el.onload = this.renderBody; } else { this.renderScripts(); // will call renderBody later } } var ppfx = this.ppfx; this.$el.append(` <div id="${ppfx}tools" style="pointer-events:none"> <div class="${ppfx}highlighter"></div> <div class="${ppfx}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"></div> <div class="${ppfx}offset-fixed-v"></div> </div> `); const el = this.el; const toolsEl = el.querySelector(`#${ppfx}tools`); this.hlEl = el.querySelector(`.${ppfx}highlighter`); this.badgeEl = el.querySelector(`.${ppfx}badge`); this.placerEl = el.querySelector(`.${ppfx}placeholder`); this.ghostEl = el.querySelector(`.${ppfx}ghost`); this.toolbarEl = el.querySelector(`.${ppfx}toolbar`); this.resizerEl = el.querySelector(`.${ppfx}resizer`); this.offsetEl = el.querySelector(`.${ppfx}offset-v`); this.fixedOffsetEl = el.querySelector(`.${ppfx}offset-fixed-v`); this.toolsEl = toolsEl; this.el.className = this.className; return this; } });