diff --git a/src/dom_components/view/ComponentTextView.js b/src/dom_components/view/ComponentTextView.js index 7be5fe659..ea1ca5567 100644 --- a/src/dom_components/view/ComponentTextView.js +++ b/src/dom_components/view/ComponentTextView.js @@ -88,19 +88,19 @@ module.exports = ComponentView.extend({ model.set('content', content, contentOpt); } else { const clean = model => { - const selectable = !['text', 'default', ''].some(type => - model.is(type) - ); + const textable = !!model.get('textable'); + const selectable = + !['text', 'default', ''].some(type => model.is(type)) || textable; model.set( { editable: selectable && model.get('editable'), selectable: selectable, hoverable: selectable, + removable: textable, + draggable: textable, highlightable: 0, - removable: 0, - draggable: 0, - copyable: 0, - toolbar: '' + copyable: textable, + ...(!textable && { toolbar: '' }) }, opts ); diff --git a/src/dom_components/view/ComponentView.js b/src/dom_components/view/ComponentView.js index 74f19e932..3670ed263 100644 --- a/src/dom_components/view/ComponentView.js +++ b/src/dom_components/view/ComponentView.js @@ -236,11 +236,14 @@ module.exports = Backbone.View.extend({ updateAttributes() { const attrs = []; const { model, $el, el } = this; - const defaultAttr = { 'data-gjs-type': model.get('type') || 'default' }; - - if (model.get('highlightable')) { - defaultAttr['data-highlightable'] = 1; - } + const defaultAttr = { + 'data-gjs-type': model.get('type') || 'default', + ...(model.get('highlightable') && { 'data-highlightable': 1 }), + ...(model.get('textable') && { + contenteditable: 'false', + 'data-gjs-textable': 'true' + }) + }; // Remove all current attributes each(el.attributes, attr => attrs.push(attr.nodeName)); diff --git a/src/utils/Sorter.js b/src/utils/Sorter.js index 12362a946..bfda067e9 100644 --- a/src/utils/Sorter.js +++ b/src/utils/Sorter.js @@ -1,6 +1,6 @@ import Backbone from 'backbone'; import { isString, isFunction, isArray, result } from 'underscore'; -import { on, off, matches, getElement } from 'utils/mixins'; +import { on, off, matches, getElement, getPointerEvent } from 'utils/mixins'; const $ = Backbone.$; module.exports = Backbone.View.extend({ @@ -51,6 +51,7 @@ module.exports = Backbone.View.extend({ this.canvasRelative = o.canvasRelative || 0; this.selectOnEnd = !o.avoidSelectOnEnd; this.scale = o.scale; + this.activeTextModel = null; if (this.em && this.em.on) { this.em.on('change:canvasOffset', this.udpateOffset); @@ -96,6 +97,34 @@ module.exports = Backbone.View.extend({ this.dropContent = content; }, + updateTextViewCursorPosition(e) { + const Canvas = this.em.get('Canvas'); + const targetDoc = Canvas.getDocument(); + let range = null; + + if (targetDoc.caretRangeFromPoint) { + // Chrome + const poiner = getPointerEvent(e); + range = targetDoc.caretRangeFromPoint(poiner.clientX, poiner.clientY); + } else if (e.rangeParent) { + // Firefox + range = targetDoc.createRange(); + range.setStart(e.rangeParent, e.rangeOffset); + } + + const sel = Canvas.getWindow().getSelection(); + Canvas.getFrameEl().focus(); + sel.removeAllRanges(); + range && sel.addRange(range); + }, + + setContentEditable(model, mode) { + if (model) { + const el = model.getEl(); + if (el.contentEditable != mode) el.contentEditable = mode; + } + }, + /** * Toggle cursor while sorting * @param {Boolean} active @@ -308,30 +337,40 @@ module.exports = Backbone.View.extend({ * Get the model of the current source element (element to drag) * @return {Model} */ - getSourceModel(source) { - var src = source || this.eV; - let dropContent = this.dropContent; - let dropModel = this.dropModel; - const em = this.em; + getSourceModel(source, { target, avoidChildren = 1 } = {}) { + const { em, eV } = this; + const src = source || eV; + let { dropModel, dropContent } = this; + const isTextable = src => + src && + target && + src.opt.avoidChildren && + this.isTextableActive(src, target); if (dropContent && em) { + if (isTextable(dropModel)) { + dropModel = null; + } + if (!dropModel) { - let comps = em.get('DomComponents').getComponents(); + const comps = em.get('DomComponents').getComponents(); const opts = { + avoidChildren, avoidStore: 1, - avoidChildren: 1, avoidUpdateStyle: 1 }; - let tempModel = comps.add(dropContent, { ...opts, temporary: 1 }); + const tempModel = comps.add(dropContent, { ...opts, temporary: 1 }); dropModel = comps.remove(tempModel, opts); this.dropModel = dropModel instanceof Array ? dropModel[0] : dropModel; + + if (isTextable(dropModel)) { + return this.getSourceModel(src, { target, avoidChildren: 0 }); + } } return dropModel; } - if (src) { - return $(src).data('model'); - } + return src && $(src).data('model'); }, /** @@ -359,11 +398,11 @@ module.exports = Backbone.View.extend({ * @param {Event} e * */ onMove(e) { - const em = this.em; + const ev = e; + const { em, onMoveClb, plh } = this; this.moved = 1; // Turn placeholder visibile - var plh = this.plh; var dsp = plh.style.display; if (!dsp || dsp === 'none') plh.style.display = 'block'; @@ -385,6 +424,7 @@ module.exports = Backbone.View.extend({ this.eventMove = e; //var targetNew = this.getTargetFromEl(e.target); + const sourceModel = this.getSourceModel(); const dims = this.dimsFromTarget(e.target, rX, rY); const target = this.target; const targetModel = this.getTargetModel(target); @@ -392,26 +432,39 @@ module.exports = Backbone.View.extend({ if (!targetModel) plh.style.display = 'none'; this.lastDims = dims; - var pos = this.findPosition(dims, rX, rY); - // If there is a significant changes with the pointer - if ( - !this.lastPos || - (this.lastPos.index != pos.index || this.lastPos.method != pos.method) - ) { - this.movePlaceholder(this.plh, dims, pos, this.prevTargetDim); - if (!this.$plh) this.$plh = $(this.plh); - - // With canvasRelative the offset is calculated automatically for - // each element - if (!this.canvasRelative) { - if (this.offTop) this.$plh.css('top', '+=' + this.offTop + 'px'); - if (this.offLeft) this.$plh.css('left', '+=' + this.offLeft + 'px'); - } + const pos = this.findPosition(dims, rX, rY); + if (this.isTextableActive(sourceModel, targetModel)) { + this.activeTextModel = targetModel; + this.setContentEditable(targetModel, true); + + plh.style.display = 'none'; this.lastPos = pos; + this.updateTextViewCursorPosition(ev); + } else { + this.disableTextable(); + this.activeTextModel = null; + + // If there is a significant changes with the pointer + if ( + !this.lastPos || + (this.lastPos.index != pos.index || this.lastPos.method != pos.method) + ) { + this.movePlaceholder(this.plh, dims, pos, this.prevTargetDim); + if (!this.$plh) this.$plh = $(this.plh); + + // With canvasRelative the offset is calculated automatically for + // each element + if (!this.canvasRelative) { + if (this.offTop) this.$plh.css('top', '+=' + this.offTop + 'px'); + if (this.offLeft) this.$plh.css('left', '+=' + this.offLeft + 'px'); + } + + this.lastPos = pos; + } } - if (typeof this.onMoveClb === 'function') this.onMoveClb(e); + isFunction(onMoveClb) && onMoveClb(e); em && em.trigger('sorter:drag', { @@ -424,6 +477,15 @@ module.exports = Backbone.View.extend({ }); }, + isTextableActive(src, trg) { + return src && src.get('textable') && trg && trg.is('text'); + }, + + disableTextable() { + const { activeTextModel } = this; + activeTextModel && activeTextModel.getView().disableEditing(); + }, + /** * Returns true if the elements is in flow, so is not in flow where * for example the component is with float:left @@ -495,9 +557,9 @@ module.exports = Backbone.View.extend({ * @return {Boolean} */ validTarget(trg, src) { - let srcModel = this.getSourceModel(src); + const trgModel = this.getTargetModel(trg); + const srcModel = this.getSourceModel(src, { target: trgModel }); src = srcModel && srcModel.view && srcModel.view.el; - let trgModel = this.getTargetModel(trg); trg = trgModel && trgModel.view && trgModel.view.el; let result = { valid: true, @@ -512,21 +574,23 @@ module.exports = Backbone.View.extend({ return result; } + // check if the source is draggable in target + let draggable = srcModel.get('draggable'); + draggable = draggable instanceof Array ? draggable.join(', ') : draggable; + result.dragInfo = draggable; + draggable = isString(draggable) ? this.matches(trg, draggable) : draggable; + result.draggable = draggable; + // Check if the target could accept the source let droppable = trgModel.get('droppable'); droppable = droppable instanceof Backbone.Collection ? 1 : droppable; droppable = droppable instanceof Array ? droppable.join(', ') : droppable; result.dropInfo = droppable; droppable = isString(droppable) ? this.matches(src, droppable) : droppable; + droppable = + draggable && this.isTextableActive(srcModel, trgModel) ? 1 : droppable; result.droppable = droppable; - // check if the source is draggable in target - let draggable = srcModel.get('draggable'); - draggable = draggable instanceof Array ? draggable.join(', ') : draggable; - result.dragInfo = draggable; - draggable = isString(draggable) ? this.matches(trg, draggable) : draggable; - result.draggable = draggable; - if (!droppable || !draggable) { result.valid = false; } @@ -914,7 +978,6 @@ module.exports = Backbone.View.extend({ * @return void * */ endMove(e) { - var created; const moved = [null]; const docs = this.getDocuments(); const container = this.getContainerEl(); @@ -923,10 +986,7 @@ module.exports = Backbone.View.extend({ off(container, 'mousemove dragover', this.onMove); off(docs, 'mouseup dragend touchend', this.endMove); off(docs, 'keydown', this.rollback); - //this.$document.off('mouseup', this.endMove); - //this.$document.off('keydown', this.rollback); this.plh.style.display = 'none'; - var clsReg = new RegExp('(?:^|\\s)' + this.freezeClass + '(?!\\S)', 'gi'); let src = this.eV; if (src && this.selectOnEnd) { @@ -953,6 +1013,7 @@ module.exports = Backbone.View.extend({ this.dragHelper = null; } + this.disableTextable(); this.selectTargetModel(); this.toggleSortCursor(); @@ -967,7 +1028,7 @@ module.exports = Backbone.View.extend({ * @param {Object} pos Object with position coordinates * */ move(dst, src, pos) { - var em = this.em; + const { em, activeTextModel } = this; const srcEl = getElement(src); em && em.trigger('component:dragEnd:before', dst, srcEl, pos); // @depricated var warns = []; @@ -981,8 +1042,9 @@ module.exports = Backbone.View.extend({ var dropInfo = validResult.dropInfo; var dragInfo = validResult.dragInfo; var dropContent = this.dropContent; - droppable = - validResult.trgModel instanceof Backbone.Collection ? 1 : droppable; + const { trgModel } = validResult; + droppable = trgModel instanceof Backbone.Collection ? 1 : droppable; + const isTextableActive = this.isTextableActive(model, trgModel); if (targetCollection && droppable && draggable) { index = pos.method === 'after' ? index + 1 : index; @@ -993,7 +1055,7 @@ module.exports = Backbone.View.extend({ opts.temporary = 1; modelTemp = targetCollection.add({}, { ...opts }); - if (model) { + if (model.collection) { modelToDrop = model.collection.remove(model, { temporary: 1 }); } } else { @@ -1002,7 +1064,18 @@ module.exports = Backbone.View.extend({ opts.avoidUpdateStyle = 1; } - created = targetCollection.add(modelToDrop, opts); + if (isTextableActive) { + const viewActive = activeTextModel.getView(); + activeTextModel.trigger('active'); + const { activeRte } = viewActive; + const modelEl = model.getEl(); + model.getView().render(); + modelEl.setAttribute('data-gjs-textable', 'true'); + const { outerHTML } = modelEl; + activeRte.insertHTML && activeRte.insertHTML(outerHTML); + } else { + created = targetCollection.add(modelToDrop, opts); + } if (!dropContent) { targetCollection.remove(modelTemp);