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) { if (model) {
let toSelect;
if (model.get('selectable')) { if (model.get('selectable')) {
this.select(model, ev); toSelect = model;
} else { } else {
let parent = model.parent(); let parent = model.parent();
while (parent && !parent.get('selectable')) parent = parent.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; 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 * Reset id of the component and any of its style rule
* @param {Object} [opts={}] Options * @param {Object} [opts={}] Options

47
src/dom_components/model/ComponentText.js

@ -1,15 +1,38 @@
import Component from './Component'; import Component from './Component';
export default Component.extend({ export default class ComponentText extends Component {
defaults: { /**
...Component.prototype.defaults, * This method is called once the content of the text is reset.
type: 'text', */
droppable: false, onContentReset(components, opts) {
editable: true, const cmps = components || this.components();
__text: true,
},
toHTML() { cmps.forEach(model => {
return Component.prototype.toHTML.apply(this, arguments); 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 * @private
* */ * */
async onActive(e) { async onActive(e) {
const { rte, em } = this; const { rte, em, model } = this;
// We place this before stopPropagation in case of nested // We place this before stopPropagation in case of nested
// text components will not block the editing (#1394) // 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; return;
} }
@ -110,28 +121,8 @@ export default ComponentView.extend({
comps.length && comps.reset(null, opts); comps.length && comps.reset(null, opts);
model.set('content', content, contentOpt); model.set('content', content, contentOpt);
} else { } 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.resetFromString(content, opts);
comps.each(model => clean(model)); model.onContentReset(comps, opts);
comps.trigger('resetNavigator'); 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 node = doc.createElement('div');
const range = sel.getRangeAt(0); const range = sel.getRangeAt(0);
range.deleteContents(); range.deleteContents();
if (isString(value)) { if (isString(value)) {
node.innerHTML = value; node.innerHTML = value;
} else if (value) { } else if (value) {
node.appendChild(value); node.appendChild(value);
} }
Array.prototype.slice.call(node.childNodes).forEach(nd => { Array.prototype.slice.call(node.childNodes).forEach(nd => {
range.insertNode(nd); range.insertNode(nd);
}); });
@ -331,7 +333,7 @@ export default class RichTextEditor {
const toSel = cmp.find(`[${customElAttr}]`)[0]; const toSel = cmp.find(`[${customElAttr}]`)[0];
if (!toSel) return; if (!toSel) return;
toSel.removeAttributes(customElAttr); toSel.removeAttributes(customElAttr);
toSel.set('selectable', true); toSel.set({ _innertext: true });
editor.select(toSel); editor.select(toSel);
}); });
cmp.trigger('disable'); cmp.trigger('disable');

Loading…
Cancel
Save