Browse Source

Refactor text component and its editing

pull/4141/head
Artur Arseniev 4 years ago
parent
commit
7eac39a76c
  1. 15
      src/commands/view/SelectComponent.js
  2. 55
      src/dom_components/model/Component.js
  3. 47
      src/dom_components/model/ComponentText.js
  4. 37
      src/dom_components/view/ComponentTextView.js
  5. 4
      src/rich_text_editor/model/RichTextEditor.js

15
src/commands/view/SelectComponent.js

@ -293,12 +293,23 @@ export default {
}
if (model) {
let toSelect;
if (model.get('selectable')) {
this.select(model, ev);
toSelect = model;
} else {
let parent = model.parent();
while (parent && !parent.get('selectable')) parent = parent.parent();
this.select(parent, ev);
toSelect = parent;
}
if (toSelect) {
// Avoid selection of inner text components during editing
if (em.isEditing() && !model.get('textable') && model.isChildOf('text')) {
return;
}
this.select(toSelect, ev);
}
}
},

55
src/dom_components/model/Component.js

@ -1711,6 +1711,61 @@ export default class Component extends Model.extend(Styleable) {
return this;
}
/**
* Check if the component is an instance of some component type.
* @param {String} type Component type
* @returns {Boolean}
* @example
* // Add a new component type by extending an existing one
* editor.Components.addType('text-ext', { extend: 'text' });
* // Append a new component somewhere
* const newTextExt = editor.getSelected().append({ type: 'text-ext' })[0];
* newTextExt.isInstanceOf('text-ext'); // true
* newTextExt.isInstanceOf('text'); // true
*/
isInstanceOf(type) {
const cmp = this.em?.get('DomComponents').getType(type)?.model;
if (!cmp) return false;
return this instanceof cmp;
}
/**
* Check if the component is a child of some other component (or component type)
* @param {[Component]|String} component Component parent to check. In case a string is passed,
* the check will be performed on the component type.
* @returns {Boolean}
* @example
* const newTextComponent = editor.getSelected().append({
* type: 'text',
* components: 'My text <b>here</b>',
* })[0];
* const innerComponent = newTextComponent.find('b')[0];
* innerComponent.isChildOf(newTextComponent); // true
* innerComponent.isChildOf('text'); // true
*/
isChildOf(component) {
const byType = isString(component);
let parent = this.parent();
while (parent) {
if (byType) {
if (parent.isInstanceOf(component)) {
return true;
}
} else {
if (parent === component) {
return true;
}
}
parent = parent.parent();
}
return false;
}
/**
* Reset id of the component and any of its style rule
* @param {Object} [opts={}] Options

47
src/dom_components/model/ComponentText.js

@ -1,15 +1,38 @@
import Component from './Component';
export default Component.extend({
defaults: {
...Component.prototype.defaults,
type: 'text',
droppable: false,
editable: true,
__text: true,
},
export default class ComponentText extends Component {
/**
* This method is called once the content of the text is reset.
*/
onContentReset(components, opts) {
const cmps = components || this.components();
toHTML() {
return Component.prototype.toHTML.apply(this, arguments);
},
});
cmps.forEach(model => {
const textable = !!model.get('textable');
const isBaseType = ['text', 'default', ''].some(type => model.is(type));
const selectable = !isBaseType || textable;
model.set(
{
_innertext: model.get('_innertext') ? true : !selectable,
// editable: selectable && model.get('editable'),
// selectable: selectable,
// hoverable: selectable,
// draggable: textable,
// highlightable: 0,
// copyable: textable,
...(!textable && { toolbar: '' }),
},
opts
);
this.onContentReset(model.components(), opts);
});
}
}
ComponentText.prototype.defaults = {
...Component.getDefaults(),
type: 'text',
droppable: false,
editable: true,
__text: true,
};

37
src/dom_components/view/ComponentTextView.js

@ -30,11 +30,22 @@ export default ComponentView.extend({
* @private
* */
async onActive(e) {
const { rte, em } = this;
const { rte, em, model } = this;
// We place this before stopPropagation in case of nested
// text components will not block the editing (#1394)
if (this.rteEnabled || !this.model.get('editable') || (em && em.isEditing())) {
if (this.rteEnabled || !model.get('editable') || model.get('_innertext') || em?.isEditing()) {
// If the current is inner text, select the closest text
if (model.get('_innertext')) {
let parent = model.parent();
while (parent && !parent.get('__text')) {
parent = parent.parent();
}
if (parent) {
em.setSelected(parent);
parent.trigger('active');
}
}
return;
}
@ -110,28 +121,8 @@ export default ComponentView.extend({
comps.length && comps.reset(null, opts);
model.set('content', content, contentOpt);
} else {
const clean = model => {
const textable = !!model.get('textable');
const selectable = !['text', 'default', ''].some(type => model.is(type)) || textable;
model.set(
{
_innertext: !selectable,
editable: selectable && model.get('editable'),
selectable: selectable,
hoverable: selectable,
removable: textable,
draggable: textable,
highlightable: 0,
copyable: textable,
...(!textable && { toolbar: '' }),
},
opts
);
model.get('components').each(model => clean(model));
};
comps.resetFromString(content, opts);
comps.each(model => clean(model));
model.onContentReset(comps, opts);
comps.trigger('resetNavigator');
}
},

4
src/rich_text_editor/model/RichTextEditor.js

@ -312,11 +312,13 @@ export default class RichTextEditor {
const node = doc.createElement('div');
const range = sel.getRangeAt(0);
range.deleteContents();
if (isString(value)) {
node.innerHTML = value;
} else if (value) {
node.appendChild(value);
}
Array.prototype.slice.call(node.childNodes).forEach(nd => {
range.insertNode(nd);
});
@ -331,7 +333,7 @@ export default class RichTextEditor {
const toSel = cmp.find(`[${customElAttr}]`)[0];
if (!toSel) return;
toSel.removeAttributes(customElAttr);
toSel.set('selectable', true);
toSel.set({ _innertext: true });
editor.select(toSel);
});
cmp.trigger('disable');

Loading…
Cancel
Save