grapesjs-clot
Version:
Free and Open Source Web Builder Framework
692 lines (617 loc) • 19.4 kB
JavaScript
/**
* 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/canvas/config/config.js)
* ```js
* const editor = grapesjs.init({
* canvas: {
* // options
* }
* })
* ```
*
* Once the editor is instantiated you can use its API and listen to its events. Before using these methods, you should get the module from the instance.
*
* ```js
* // Listen to events
* editor.on('canvas:drop', () => { ... });
*
* // Use the API
* const canvas = editor.Canvas;
* canvas.setCoords(...);
* ```
* ## Available Events
* * `canvas:dragenter` - When something is dragged inside the canvas, `DataTransfer` instance passed as an argument
* * `canvas:dragover` - When something is dragging on canvas, `DataTransfer` instance passed as an argument
* * `canvas:drop` - Something is dropped in canvas, `DataTransfer` instance and the dropped model are passed as arguments
* * `canvas:dragend` - When a drag operation is ended, `DataTransfer` instance passed as an argument
* * `canvas:dragdata` - On any dataTransfer parse, `DataTransfer` instance and the `result` are passed as arguments.
* By changing `result.content` you're able to customize what is dropped
*
* ## Methods
* * [getConfig](#getconfig)
* * [getElement](#getelement)
* * [getFrameEl](#getframeel)
* * [getWindow](#getwindow)
* * [getDocument](#getdocument)
* * [getBody](#getbody)
* * [setCustomBadgeLabel](#setcustombadgelabel)
* * [hasFocus](#hasfocus)
* * [scrollTo](#scrollto)
* * [setZoom](#setzoom)
* * [getZoom](#getzoom)
* * [getCoords](#getcoords)
* * [setCoords](#setcoords)
*
* [Component]: component.html
* [Frame]: frame.html
*
* @module Canvas
*/
import { isUndefined } from 'underscore';
import { getElement, getViewEl } from 'utils/mixins';
import defaults from './config/config';
import Canvas from './model/Canvas';
import canvasView from './view/CanvasView';
export var myCanvas;
export default () => {
let c = {};
let canvas;
let CanvasView;
return {
/**
* Used inside RTE
* @private
*/
getCanvasView() {
return CanvasView;
},
name: 'Canvas',
/**
* Initialize module. Automatically called with a new instance of the editor
* @param {Object} config Configurations
* @private
*/
init(config = {}) {
c = {
...defaults,
...config,
module: this,
};
this.em = c.em;
const { scripts, styles } = c;
const ppfx = c.pStylePrefix;
if (ppfx) c.stylePrefix = ppfx + c.stylePrefix;
canvas = new Canvas({ scripts, styles }, config);
this.model = canvas;
this.startAutoscroll = this.startAutoscroll.bind(this);
this.stopAutoscroll = this.stopAutoscroll.bind(this);
myCanvas = canvas;
return this;
},
onLoad() {
this.model.init();
},
getModel() {
return canvas;
},
/**
* Get the configuration object
* @returns {Object} Configuration object
* @example
* console.log(canvas.getConfig())
*/
getConfig() {
return c;
},
/**
* Get the canvas element
* @returns {HTMLElement}
*/
getElement() {
return CanvasView.el;
},
getFrame(index) {
return this.getFrames()[index || 0];
},
/**
* Get the main frame element of the canvas
* @returns {HTMLIFrameElement}
*/
getFrameEl() {
const { frame } = CanvasView || {};
return frame && frame.el;
},
getFramesEl() {
return CanvasView.framesArea;
},
/**
* Get the main frame window instance
* @returns {Window}
*/
getWindow() {
return this.getFrameEl().contentWindow;
},
/**
* Get the main frame document element
* @returns {HTMLDocument}
*/
getDocument() {
const frame = this.getFrameEl();
return frame && frame.contentDocument;
},
/**
* Get the main frame body element
* @return {HTMLBodyElement}
*/
getBody() {
const doc = this.getDocument();
return doc && doc.body;
},
_getCompFrame(compView) {
return compView && compView._getFrame();
},
_getLocalEl(globalEl, compView, method) {
let result = globalEl;
const frameView = this._getCompFrame(compView);
result = frameView ? frameView[method]() : result;
return result;
},
/**
* Returns element containing all global canvas tools
* @returns {HTMLElement}
* @private
*/
getGlobalToolsEl() {
return CanvasView.toolsGlobEl;
},
/**
* Returns element containing all canvas tools
* @returns {HTMLElement}
* @private
*/
getToolsEl(compView) {
return this._getLocalEl(CanvasView.toolsEl, compView, 'getToolsEl');
},
/**
* Returns highlighter element
* @returns {HTMLElement}
* @private
*/
getHighlighter(compView) {
return this._getLocalEl(CanvasView.hlEl, compView, 'getHighlighter');
},
/**
* Returns badge element
* @returns {HTMLElement}
* @private
*/
getBadgeEl(compView) {
return this._getLocalEl(CanvasView.badgeEl, compView, 'getBadgeEl');
},
/**
* Returns placer element
* @returns {HTMLElement}
* @private
*/
getPlacerEl() {
return CanvasView.placerEl;
},
/**
* Returns ghost element
* @returns {HTMLElement}
* @private
*/
getGhostEl() {
return CanvasView.ghostEl;
},
/**
* Returns toolbar element
* @returns {HTMLElement}
* @private
*/
getToolbarEl() {
return CanvasView.toolbarEl;
},
/**
* Returns resizer element
* @returns {HTMLElement}
* @private
*/
getResizerEl() {
return CanvasView.resizerEl;
},
/**
* Returns offset viewer element
* @returns {HTMLElement}
* @private
*/
getOffsetViewerEl(compView) {
return this._getLocalEl(CanvasView.offsetEl, compView, 'getOffsetViewerEl');
},
/**
* Returns fixed offset viewer element
* @returns {HTMLElement}
* @private
*/
getFixedOffsetViewerEl() {
return CanvasView.fixedOffsetEl;
},
render() {
CanvasView && CanvasView.remove();
CanvasView = new canvasView({
model: canvas,
config: c,
});
return CanvasView.render().el;
},
/**
* Get frame position
* @returns {Object}
* @private
*/
getOffset() {
var frameOff = this.offset(this.getFrameEl());
var canvasOff = this.offset(this.getElement());
return {
top: frameOff.top - canvasOff.top,
left: frameOff.left - canvasOff.left,
};
},
/**
* Get the offset of the passed component element
* @param {HTMLElement} el
* @returns {Object}
* @private
*/
offset(el) {
return CanvasView.offset(el);
},
/**
* Set custom badge naming strategy
* @param {Function} f
* @example
* canvas.setCustomBadgeLabel(function(component){
* return component.getName();
* });
*/
setCustomBadgeLabel(f) {
c.customBadgeLabel = f;
},
/**
* Get element position relative to the canvas
* @param {HTMLElement} el
* @returns {Object}
* @private
*/
getElementPos(el, opts) {
return CanvasView.getElementPos(el, opts);
},
/**
* Returns element's offsets like margins and paddings
* @param {HTMLElement} el
* @returns {Object}
* @private
*/
getElementOffsets(el) {
return CanvasView.getElementOffsets(el);
},
/**
* Get canvas rectangular data
* @returns {Object}
*/
getRect() {
const { top, left } = CanvasView.getPosition();
return {
...CanvasView.getCanvasOffset(),
topScroll: top,
leftScroll: left,
};
},
/**
* This method comes handy when you need to attach something like toolbars
* to elements inside the canvas, dealing with all relative position,
* offsets, etc. and returning as result the object with positions which are
* viewable by the user (when the canvas is scrolled the top edge of the element
* is not viewable by the user anymore so the new top edge is the one of the canvas)
*
* The target should be visible before being passed here as invisible elements
* return empty string as width
* @param {HTMLElement} target The target in this case could be the toolbar
* @param {HTMLElement} element The element on which I'd attach the toolbar
* @param {Object} options Custom options
* @param {Boolean} options.toRight Set to true if you want the toolbar attached to the right
* @return {Object}
* @private
*/
getTargetToElementDim(target, element, options = {}) {
var opts = options || {};
var canvasPos = CanvasView.getPosition();
if (!canvasPos) return;
var pos = opts.elPos || CanvasView.getElementPos(element);
var toRight = options.toRight || 0;
var targetHeight = opts.targetHeight || target.offsetHeight;
var targetWidth = opts.targetWidth || target.offsetWidth;
var eventToTrigger = opts.event || null;
var elTop = pos.top - targetHeight;
var elLeft = pos.left;
elLeft += toRight ? pos.width : 0;
elLeft = toRight ? elLeft - targetWidth : elLeft;
var leftPos = elLeft < canvasPos.left ? canvasPos.left : elLeft;
var topPos = elTop < canvasPos.top ? canvasPos.top : elTop;
topPos = topPos > pos.top + pos.height ? pos.top + pos.height : topPos;
var result = {
top: topPos,
left: leftPos,
elementTop: pos.top,
elementLeft: pos.left,
elementWidth: pos.width,
elementHeight: pos.height,
targetWidth: target.offsetWidth,
targetHeight: target.offsetHeight,
canvasTop: canvasPos.top,
canvasLeft: canvasPos.left,
canvasWidth: canvasPos.width,
canvasHeight: canvasPos.height,
};
// In this way I can catch data and also change the position strategy
if (eventToTrigger && c.em) {
c.em.trigger(eventToTrigger, result);
}
return result;
},
canvasRectOffset(el, pos, opts = {}) {
const getFrameElFromDoc = doc => {
const { defaultView } = doc;
return defaultView && defaultView.frameElement;
};
const rectOff = (el, top = 1, pos) => {
const zoom = this.em.getZoomDecimal();
const side = top ? 'top' : 'left';
const doc = el.ownerDocument;
const { offsetTop = 0, offsetLeft = 0 } = opts.offset ? getFrameElFromDoc(doc) : {};
const { scrollTop = 0, scrollLeft = 0 } = doc.body || {};
const scroll = top ? scrollTop : scrollLeft;
const offset = top ? offsetTop : offsetLeft;
// if (!top) {
// console.log('LEFT', { posLeft: pos[side], scroll, offset }, el);
// }
return pos[side] - (scroll - offset) * zoom;
};
return {
top: rectOff(el, 1, pos),
left: rectOff(el, 0, pos),
};
},
getTargetToElementFixed(el, elToMove, opts = {}) {
const pos = opts.pos || this.getElementPos(el);
const cvOff = opts.canvasOff || this.canvasRectOffset(el, pos);
const toolbarH = elToMove.offsetHeight || 0;
const toolbarW = elToMove.offsetWidth || 0;
const elRight = pos.left + pos.width;
const cv = this.getCanvasView();
const frCvOff = cv.getPosition();
const frameOffset = cv.getFrameOffset(el);
const { event } = opts;
let top = -toolbarH;
let left = !isUndefined(opts.left) ? opts.left : pos.width - toolbarW;
left = pos.left < -left ? -pos.left : left;
left = elRight > frCvOff.width ? left - (elRight - frCvOff.width) : left;
// Scroll with the window if the top edge is reached and the
// element is bigger than the canvas
const fullHeight = pos.height + toolbarH;
const elIsShort = fullHeight < frameOffset.height;
if (cvOff.top < toolbarH) {
if (elIsShort) {
top = top + fullHeight;
} else {
top = -cvOff.top < pos.height ? -cvOff.top : pos.height;
}
}
const result = {
top,
left,
canvasOffsetTop: cvOff.top,
canvasOffsetLeft: cvOff.left,
};
// In this way I can catch data and also change the position strategy
event && this.em.trigger(event, result);
return result;
},
/**
* Instead of simply returning e.clientX and e.clientY this function
* calculates also the offset based on the canvas. This is helpful when you
* need to get X and Y position while moving between the editor area and
* canvas area, which is in the iframe
* @param {Event} e
* @return {Object}
* @private
*/
getMouseRelativePos(e, options) {
var opts = options || {};
var addTop = 0;
var addLeft = 0;
var subWinOffset = opts.subWinOffset;
var doc = e.target.ownerDocument;
var win = doc.defaultView || doc.parentWindow;
var frame = win.frameElement;
var yOffset = subWinOffset ? win.pageYOffset : 0;
var xOffset = subWinOffset ? win.pageXOffset : 0;
if (frame) {
var frameRect = frame.getBoundingClientRect();
addTop = frameRect.top || 0;
addLeft = frameRect.left || 0;
}
return {
y: e.clientY + addTop - yOffset,
x: e.clientX + addLeft - xOffset,
};
},
/**
* X and Y mouse position relative to the canvas
* @param {Event} ev
* @return {Object}
* @private
*/
getMouseRelativeCanvas(ev, opts) {
const zoom = this.getZoomDecimal();
const { top, left } = CanvasView.getPosition(opts);
return {
y: ev.clientY * zoom + top,
x: ev.clientX * zoom + left,
};
},
/**
* Check if the canvas is focused
* @returns {Boolean}
*/
hasFocus() {
return this.getDocument().hasFocus();
},
/**
* Detects if some input is focused (input elements, text components, etc.)
* @return {Boolean}
* @private
*/
isInputFocused() {
const doc = this.getDocument();
const frame = this.getFrameEl();
const toIgnore = ['body', ...this.getConfig().notTextable];
const docActive = frame && document.activeElement === frame;
const focused = docActive ? doc && doc.activeElement : document.activeElement;
return focused && !toIgnore.some(item => focused.matches(item));
},
/**
* Scroll canvas to the element if it's not visible. The scrolling is
* executed via `scrollIntoView` API and options of this method are
* passed to it. For instance, you can scroll smoothly by using
* `{ behavior: 'smooth' }`.
* @param {HTMLElement|[Component]} el
* @param {Object} [opts={}] Options, same as options for `scrollIntoView`
* @param {Boolean} [opts.force=false] Force the scroll, even if the element is already visible
* @example
* const selected = editor.getSelected();
* // Scroll smoothly (this behavior can be polyfilled)
* canvas.scrollTo(selected, { behavior: 'smooth' });
* // Force the scroll, even if the element is alredy visible
* canvas.scrollTo(selected, { force: true });
*/
scrollTo(el, opts = {}) {
const elem = getElement(el);
const view = elem && getViewEl(elem);
view && view.scrollIntoView(opts);
},
/**
* Start autoscroll
* @private
*/
startAutoscroll(frame) {
const fr = (frame && frame.view) || this.em.getCurrentFrame();
fr && fr.startAutoscroll();
},
/**
* Stop autoscroll
* @private
*/
stopAutoscroll(frame) {
const fr = (frame && frame.view) || this.em.getCurrentFrame();
fr && fr.stopAutoscroll();
},
/**
* Set canvas zoom value
* @param {Number} value The zoom value, from 0 to 100
* @returns {this}
* @example
* canvas.setZoom(50); // set zoom to 50%
*/
setZoom(value) {
canvas.set('zoom', parseFloat(value));
return this;
},
/**
* Get canvas zoom value
* @returns {Number}
* @example
* canvas.setZoom(50); // set zoom to 50%
* const zoom = canvas.getZoom(); // 50
*/
getZoom() {
return parseFloat(canvas.get('zoom'));
},
/**
* Set canvas position coordinates
* @param {Number} x Horizontal position
* @param {Number} y Vertical position
* @returns {this}
* @example
* canvas.setCoords(100, 100);
*/
setCoords(x, y) {
canvas.set({ x: parseFloat(x), y: parseFloat(y) });
return this;
},
/**
* Get canvas position coordinates
* @returns {Object} Object containing coordinates
* @example
* canvas.setCoords(100, 100);
* const coords = canvas.getCoords();
* // { x: 100, y: 100 }
*/
getCoords() {
const { x, y } = canvas.attributes;
return { x, y };
},
getZoomDecimal() {
return this.getZoom() / 100;
},
getZoomMultiplier() {
const zoom = this.getZoomDecimal();
return zoom ? 1 / zoom : 1;
},
toggleFramesEvents(on) {
const { style } = this.getFramesEl();
style.pointerEvents = on ? '' : 'none';
},
getFrames() {
return canvas.get('frames').map(item => item);
},
/**
* Add new frame to the canvas
* @param {Object} props Frame properties
* @returns {[Frame]}
* @example
* canvas.addFrame({
* name: 'Mobile home page',
* x: 100, // Position in canvas
* y: 100,
* width: 500, // Frame dimensions
* height: 600,
* // device: 'DEVICE-ID',
* components: [
* '<h1 class="testh">Title frame</h1>',
* '<p class="testp">Paragraph frame</p>',
* ],
* styles: `
* .testh { color: red; }
* .testp { color: blue; }
* `,
* });
*/
addFrame(props = {}, opts = {}) {
return canvas.get('frames').add(
{
...props,
},
{
...opts,
em: this.em,
}
);
},
destroy() {
canvas.stopListening();
CanvasView && CanvasView.remove();
[c, canvas, CanvasView].forEach(i => (i = {}));
['em', 'model', 'droppable'].forEach(i => (this[i] = {}));
},
};
};