Browse Source

Merge branch 'xQwexx-component-fix' into dev

pull/4487/head
Artur Arseniev 4 years ago
parent
commit
1dfd8a54f9
  1. 15
      src/css_composer/config/config.js
  2. 12
      src/dom_components/index.ts
  3. 98
      src/dom_components/model/Component.js
  4. 39
      src/dom_components/model/ComponentComment.js
  5. 31
      src/dom_components/model/ComponentFrame.js
  6. 244
      src/dom_components/model/ComponentImage.js
  7. 19
      src/dom_components/model/ComponentLabel.js
  8. 18
      src/dom_components/model/ComponentLink.js
  9. 126
      src/dom_components/model/ComponentMap.js
  10. 35
      src/dom_components/model/ComponentScript.js
  11. 29
      src/dom_components/model/ComponentSvg.js
  12. 19
      src/dom_components/model/ComponentSvgIn.js
  13. 27
      src/dom_components/model/ComponentTable.js
  14. 72
      src/dom_components/model/ComponentTableBody.js
  15. 17
      src/dom_components/model/ComponentTableCell.js
  16. 17
      src/dom_components/model/ComponentTableFoot.js
  17. 17
      src/dom_components/model/ComponentTableHead.js
  18. 17
      src/dom_components/model/ComponentTableRow.js
  19. 18
      src/dom_components/model/ComponentText.js
  20. 45
      src/dom_components/model/ComponentTextNode.js
  21. 622
      src/dom_components/model/ComponentVideo.js
  22. 2
      src/dom_components/model/ComponentWrapper.js
  23. 16
      src/domain_abstract/model/StyleableModel.js
  24. 4
      test/specs/dom_components/model/ComponentImage.js
  25. 3
      test/specs/dom_components/model/ComponentTypes.js

15
src/css_composer/config/config.js

@ -4,19 +4,4 @@ export default {
// Default CSS style
rules: [],
/**
* Adjust style object before creation/update.
* @example
* onBeforeStyle(style) {
* const padValue = style.padding;
* if (padValue === '10px') {
* delete style.padding;
* style['padding-top'] = padValue;
* // ...
* }
* return style;
* }
*/
onBeforeStyle: null,
};

12
src/dom_components/index.ts

@ -442,19 +442,23 @@ export default class ComponentManager extends ItemManagerModule {
// If the model/view is a simple object I need to extend it
if (typeof model === 'object') {
const defaults = result(model, 'defaults');
delete model.defaults;
methods.model = modelToExt.extend(
{
...model,
...getExtendedObj(extendFn, model, modelToExt),
defaults: {
...(result(modelToExt.prototype, 'defaults') || {}),
...(result(model, 'defaults') || {}),
},
},
{
isComponent: compType && !extendType && !isComponent ? modelToExt.isComponent : isComponent || (() => 0),
}
);
Object.defineProperty(methods.model.prototype, 'defaults', {
value: {
...(result(modelToExt.prototype, 'defaults') || {}),
...(defaults || {}),
},
});
}
if (typeof view === 'object') {

98
src/dom_components/model/Component.js

@ -94,6 +94,51 @@ export const keyUpdateInside = `${keyUpdate}-inside`;
* @module docsjs.Component
*/
export default class Component extends StyleableModel {
get defaults() {
return {
tagName: 'div',
type: '',
name: '',
removable: true,
draggable: true,
droppable: true,
badgable: true,
stylable: true,
'stylable-require': '',
'style-signature': '',
unstylable: '',
highlightable: true,
copyable: true,
resizable: false,
editable: false,
layerable: true,
selectable: true,
hoverable: true,
locked: false,
void: false,
state: '', // Indicates if the component is in some CSS state like ':hover', ':active', etc.
status: '', // State, eg. 'selected'
content: '',
icon: '',
style: '',
styles: '', // Component related styles
classes: '', // Array of classes
script: '',
'script-props': '',
'script-export': '',
attributes: '',
traits: ['id', 'title'],
propagate: '',
dmode: '',
toolbar: null,
[keySymbol]: 0,
[keySymbols]: 0,
[keySymbolOvrd]: 0,
_undo: true,
_undoexc: ['status', 'open'],
};
}
/**
* Hook method, called once the model is created
*/
@ -1253,7 +1298,6 @@ export default class Component extends StyleableModel {
/**
* Override original clone method
* @private
*/
clone(opt = {}) {
const em = this.em;
@ -1485,7 +1529,6 @@ export default class Component extends StyleableModel {
* Return a shallow copy of the model's attributes for JSON
* stringification.
* @return {Object}
* @private
*/
toJSON(opts = {}) {
const obj = Model.prototype.toJSON.call(this, opts);
@ -1838,6 +1881,10 @@ export default class Component extends StyleableModel {
}
}
Component.getDefaults = function () {
return result(this.prototype, 'defaults');
};
/**
* Detect if the passed element is a valid component.
* In case the element is valid an object abstracted
@ -1965,50 +2012,3 @@ Component.checkId = (components, styles = [], list = {}, opts = {}) => {
components && Component.checkId(components, styles, list, opts);
});
};
Component.getDefaults = function () {
return result(this.prototype, 'defaults');
};
Component.prototype.defaults = {
tagName: 'div',
type: '',
name: '',
removable: true,
draggable: true,
droppable: true,
badgable: true,
stylable: true,
'stylable-require': '',
'style-signature': '',
unstylable: '',
highlightable: true,
copyable: true,
resizable: false,
editable: false,
layerable: true,
selectable: true,
hoverable: true,
locked: false,
void: false,
state: '', // Indicates if the component is in some CSS state like ':hover', ':active', etc.
status: '', // State, eg. 'selected'
content: '',
icon: '',
style: '',
styles: '', // Component related styles
classes: '', // Array of classes
script: '',
'script-props': '',
'script-export': '',
attributes: '',
traits: ['id', 'title'],
propagate: '',
dmode: '',
toolbar: null,
[keySymbol]: 0,
[keySymbols]: 0,
[keySymbolOvrd]: 0,
_undo: true,
_undoexc: ['status', 'open'],
};

39
src/dom_components/model/ComponentComment.js

@ -1,24 +1,21 @@
import Component from './ComponentTextNode';
import ComponentTextNode from './ComponentTextNode';
export default Component.extend(
{
defaults: {
...Component.prototype.defaults,
},
export default class ComponentComment extends ComponentTextNode {
get defaults() {
return { ...super.defaults };
}
toHTML() {
return `<!--${this.get('content')}-->`;
}
}
toHTML() {
return `<!--${this.get('content')}-->`;
},
},
{
isComponent(el) {
if (el.nodeType == 8) {
return {
tagName: 'NULL',
type: 'comment',
content: el.textContent,
};
}
},
ComponentComment.isComponent = el => {
if (el.nodeType == 8) {
return {
tagName: 'NULL',
type: 'comment',
content: el.textContent,
};
}
);
};

31
src/dom_components/model/ComponentFrame.js

@ -3,21 +3,18 @@ import { toLowerCase } from 'utils/mixins';
const type = 'iframe';
export default Component.extend(
{
defaults() {
return {
...Component.prototype.defaults,
type,
tagName: type,
droppable: false,
resizable: true,
traits: ['id', 'title', 'src'],
attributes: { frameborder: '0' },
};
},
},
{
isComponent: el => toLowerCase(el.tagName) === type,
export default class ComponentFrame extends Component {
get defaults() {
return {
...super.defaults,
type,
tagName: type,
droppable: false,
resizable: true,
traits: ['id', 'title', 'src'],
attributes: { frameborder: '0' },
};
}
);
}
ComponentFrame.isComponent = el => toLowerCase(el.tagName) === type;

244
src/dom_components/model/ComponentImage.js

@ -5,10 +5,10 @@ import { toLowerCase, buildBase64UrlFromSvg, hasWin } from '../../utils/mixins';
const svgAttrs =
'xmlns="http://www.w3.org/2000/svg" width="100" viewBox="0 0 24 24" style="fill: rgba(0,0,0,0.15); transform: scale(0.75)"';
export default Component.extend(
{
defaults: {
...Component.prototype.defaults,
export default class ComponentImage extends Component {
get defaults() {
return {
...super.defaults,
type: 'image',
tagName: 'img',
void: true,
@ -30,134 +30,132 @@ export default Component.extend(
// File to load asynchronously once the model is rendered
file: '',
},
};
}
initialize(o, opt) {
Component.prototype.initialize.apply(this, arguments);
const { src } = this.get('attributes');
if (src && buildBase64UrlFromSvg(result(this, 'defaults').src) !== src) {
this.set('src', src, { silent: 1 });
}
},
initToolbar(...args) {
Component.prototype.initToolbar.apply(this, args);
const em = this.em;
if (em) {
var cmd = em.get('Commands');
var cmdName = 'image-editor';
// Add Image Editor button only if the default command exists
if (cmd.has(cmdName)) {
let hasButtonBool = false;
var tb = this.get('toolbar');
for (let i = 0; i < tb.length; i++) {
if (tb[i].command === 'image-editor') {
hasButtonBool = true;
break;
}
}
initialize(o, opt) {
Component.prototype.initialize.apply(this, arguments);
const { src } = this.get('attributes');
if (src && buildBase64UrlFromSvg(result(this, 'defaults').src) !== src) {
this.set('src', src, { silent: 1 });
}
}
initToolbar(...args) {
Component.prototype.initToolbar.apply(this, args);
const em = this.em;
if (em) {
var cmd = em.get('Commands');
var cmdName = 'image-editor';
if (!hasButtonBool) {
tb.push({
attributes: { class: 'fa fa-pencil' },
command: cmdName,
});
this.set('toolbar', tb);
// Add Image Editor button only if the default command exists
if (cmd.has(cmdName)) {
let hasButtonBool = false;
var tb = this.get('toolbar');
for (let i = 0; i < tb.length; i++) {
if (tb[i].command === 'image-editor') {
hasButtonBool = true;
break;
}
}
}
},
/**
* Returns object of attributes for HTML
* @return {Object}
* @private
*/
getAttrToHTML(...args) {
const attr = Component.prototype.getAttrToHTML.apply(this, args);
const src = this.getSrcResult();
if (src) attr.src = src;
return attr;
},
getSrcResult(opt = {}) {
const src = this.get(opt.fallback ? 'fallback' : 'src') || '';
let result = src;
if (src && src.substr(0, 4) === '<svg') {
result = buildBase64UrlFromSvg(src);
}
return result;
},
isDefaultSrc() {
const src = this.get('src');
const srcDef = result(this, 'defaults').src;
return src === srcDef || src === buildBase64UrlFromSvg(srcDef);
},
/**
* Return a shallow copy of the model's attributes for JSON
* stringification.
* @return {Object}
* @private
*/
toJSON(...args) {
const obj = Component.prototype.toJSON.apply(this, args);
if (obj.attributes && obj.src === obj.attributes.src) {
delete obj.src;
if (!hasButtonBool) {
tb.push({
attributes: { class: 'fa fa-pencil' },
command: cmdName,
});
this.set('toolbar', tb);
}
}
}
}
return obj;
},
/**
* Parse uri
* @param {string} uri
* @return {object}
* @private
*/
parseUri(uri) {
let result = {};
const getQueryObject = (search = '') => {
const query = {};
const qrs = search.substring(1).split('&');
for (let i = 0; i < qrs.length; i++) {
const pair = qrs[i].split('=');
const name = decodeURIComponent(pair[0]);
if (name) query[name] = decodeURIComponent(pair[1] || '');
}
/**
* Returns object of attributes for HTML
* @return {Object}
* @private
*/
getAttrToHTML(...args) {
const attr = Component.prototype.getAttrToHTML.apply(this, args);
const src = this.getSrcResult();
if (src) attr.src = src;
return attr;
}
getSrcResult(opt = {}) {
const src = this.get(opt.fallback ? 'fallback' : 'src') || '';
let result = src;
if (src && src.substr(0, 4) === '<svg') {
result = buildBase64UrlFromSvg(src);
}
return result;
}
return query;
};
isDefaultSrc() {
const src = this.get('src');
const srcDef = result(this, 'defaults').src;
return src === srcDef || src === buildBase64UrlFromSvg(srcDef);
}
if (hasWin()) {
result = document.createElement('a');
result.href = uri;
} else if (typeof URL !== 'undefined') {
try {
result = new URL(uri);
} catch (e) {}
/**
* Return a shallow copy of the model's attributes for JSON
* stringification.
* @return {Object}
* @private
*/
toJSON(...args) {
const obj = Component.prototype.toJSON.apply(this, args);
if (obj.attributes && obj.src === obj.attributes.src) {
delete obj.src;
}
return obj;
}
/**
* Parse uri
* @param {string} uri
* @return {object}
* @private
*/
parseUri(uri) {
let result = {};
const getQueryObject = (search = '') => {
const query = {};
const qrs = search.substring(1).split('&');
for (let i = 0; i < qrs.length; i++) {
const pair = qrs[i].split('=');
const name = decodeURIComponent(pair[0]);
if (name) query[name] = decodeURIComponent(pair[1] || '');
}
return {
hostname: result.hostname || '',
pathname: result.pathname || '',
protocol: result.protocol || '',
search: result.search || '',
hash: result.hash || '',
port: result.port || '',
query: getQueryObject(result.search),
};
},
},
{
isComponent: el => toLowerCase(el.tagName) === 'img',
return query;
};
if (hasWin()) {
result = document.createElement('a');
result.href = uri;
} else if (typeof URL !== 'undefined') {
try {
result = new URL(uri);
} catch (e) {}
}
return {
hostname: result.hostname || '',
pathname: result.pathname || '',
protocol: result.protocol || '',
search: result.search || '',
hash: result.hash || '',
port: result.port || '',
query: getQueryObject(result.search),
};
}
);
}
ComponentImage.isComponent = el => toLowerCase(el.tagName) === 'img';

19
src/dom_components/model/ComponentLabel.js

@ -1,18 +1,17 @@
import Component from './ComponentText';
import ComponentText from './ComponentText';
import { toLowerCase } from 'utils/mixins';
const type = 'label';
export default Component.extend(
{
defaults: {
...Component.prototype.defaults,
export default class ComponentLabel extends ComponentText {
get defaults() {
return {
...super.defaults,
type,
tagName: type,
traits: ['id', 'title', 'for'],
},
},
{
isComponent: el => toLowerCase(el.tagName) === type,
};
}
);
}
ComponentLabel.isComponent = el => toLowerCase(el.tagName) === type;

18
src/dom_components/model/ComponentLink.js

@ -4,14 +4,16 @@ import ComponentText from './ComponentText';
const type = 'link';
export default class ComponentLink extends ComponentText {}
ComponentLink.prototype.defaults = {
...ComponentText.getDefaults(),
type,
tagName: 'a',
traits: ['title', 'href', 'target'],
};
export default class ComponentLink extends ComponentText {
get defaults() {
return {
...super.defaults,
type,
tagName: 'a',
traits: ['title', 'href', 'target'],
};
}
}
ComponentLink.isComponent = (el, opts = {}) => {
let result;

126
src/dom_components/model/ComponentMap.js

@ -1,11 +1,10 @@
import Component from './ComponentImage';
import OComponent from './Component';
import ComponentImage from './ComponentImage';
import { toLowerCase } from 'utils/mixins';
export default Component.extend(
{
defaults: {
...Component.prototype.defaults,
export default class ComponentMap extends ComponentImage {
get defaults() {
return {
...super.defaults,
type: 'map',
src: '',
void: 0,
@ -15,7 +14,7 @@ export default Component.extend(
address: '',
zoom: '1',
attributes: { frameborder: 0 },
toolbar: OComponent.prototype.defaults.toolbar,
toolbar: super.defaults.toolbar,
traits: [
{
label: 'Address',
@ -42,65 +41,64 @@ export default Component.extend(
changeProp: 1,
},
],
},
};
}
initialize(o, opt) {
if (this.get('src')) this.parseFromSrc();
else this.updateSrc();
Component.prototype.initialize.apply(this, arguments);
this.listenTo(this, 'change:address change:zoom change:mapType', this.updateSrc);
}
initialize(o, opt) {
if (this.get('src')) this.parseFromSrc();
else this.updateSrc();
Component.prototype.initialize.apply(this, arguments);
this.listenTo(this, 'change:address change:zoom change:mapType', this.updateSrc);
},
updateSrc() {
this.set('src', this.getMapUrl());
}
updateSrc() {
this.set('src', this.getMapUrl());
},
/**
* Returns url of the map
* @return {string}
* @private
*/
getMapUrl() {
var md = this;
var addr = md.get('address');
var zoom = md.get('zoom');
var type = md.get('mapType');
var size = '';
addr = addr ? '&q=' + addr : '';
zoom = zoom ? '&z=' + zoom : '';
type = type ? '&t=' + type : '';
var result = md.get('mapUrl') + '?' + addr + zoom + type;
result += '&output=embed';
return result;
}
/**
* Returns url of the map
* @return {string}
* @private
*/
getMapUrl() {
var md = this;
var addr = md.get('address');
var zoom = md.get('zoom');
var type = md.get('mapType');
var size = '';
addr = addr ? '&q=' + addr : '';
zoom = zoom ? '&z=' + zoom : '';
type = type ? '&t=' + type : '';
var result = md.get('mapUrl') + '?' + addr + zoom + type;
result += '&output=embed';
return result;
},
/**
* Set attributes by src string
* @private
*/
parseFromSrc() {
var uri = this.parseUri(this.get('src'));
var qr = uri.query;
if (qr.q) this.set('address', qr.q);
if (qr.z) this.set('zoom', qr.z);
if (qr.t) this.set('mapType', qr.t);
}
}
/**
* Set attributes by src string
* @private
*/
parseFromSrc() {
var uri = this.parseUri(this.get('src'));
var qr = uri.query;
if (qr.q) this.set('address', qr.q);
if (qr.z) this.set('zoom', qr.z);
if (qr.t) this.set('mapType', qr.t);
},
},
{
/**
* Detect if the passed element is a valid component.
* In case the element is valid an object abstracted
* from the element will be returned
* @param {HTMLElement}
* @return {Object}
* @private
*/
isComponent(el) {
var result = '';
if (toLowerCase(el.tagName) == 'iframe' && /maps\.google\.com/.test(el.src)) {
result = { type: 'map', src: el.src };
}
return result;
},
/**
* Detect if the passed element is a valid component.
* In case the element is valid an object abstracted
* from the element will be returned
* @param {HTMLElement}
* @return {Object}
* @private
*/
ComponentMap.isComponent = el => {
var result = '';
if (toLowerCase(el.tagName) == 'iframe' && /maps\.google\.com/.test(el.src)) {
result = { type: 'map', src: el.src };
}
);
return result;
};

35
src/dom_components/model/ComponentScript.js

@ -3,29 +3,28 @@ import { toLowerCase } from 'utils/mixins';
const type = 'script';
export default Component.extend(
{
defaults: {
...Component.prototype.defaults,
export default class ComponentScript extends Component {
get defaults() {
return {
...super.defaults,
type,
tagName: type,
droppable: false,
draggable: false,
layerable: false,
},
},
{
isComponent(el) {
if (toLowerCase(el.tagName) == type) {
const result = { type };
};
}
}
ComponentScript.isComponent = el => {
if (toLowerCase(el.tagName) == type) {
const result = { type };
if (el.src) {
result.src = el.src;
result.onload = el.onload;
}
if (el.src) {
result.src = el.src;
result.onload = el.onload;
}
return result;
}
},
return result;
}
);
};

29
src/dom_components/model/ComponentSvg.js

@ -3,24 +3,23 @@ import { toLowerCase } from 'utils/mixins';
const type = 'svg';
export default Component.extend(
{
defaults: {
...Component.prototype.defaults,
export default class ComponentSvg extends Component {
get defaults() {
return {
...super.defaults,
type,
tagName: type,
highlightable: 0,
resizable: { ratioDefault: 1 },
},
};
}
getName() {
let name = this.get('tagName');
let customName = this.get('custom-name');
name = name.charAt(0).toUpperCase() + name.slice(1);
return customName || name;
},
},
{
isComponent: el => toLowerCase(el.tagName) === type,
getName() {
let name = this.get('tagName');
let customName = this.get('custom-name');
name = name.charAt(0).toUpperCase() + name.slice(1);
return customName || name;
}
);
}
ComponentSvg.isComponent = el => toLowerCase(el.tagName) === type;

19
src/dom_components/model/ComponentSvgIn.js

@ -1,18 +1,17 @@
import Component from './ComponentSvg';
import ComponentSvg from './ComponentSvg';
/**
* Component for inner SVG elements
*/
export default Component.extend(
{
defaults: {
...Component.prototype.defaults,
export default class ComponentSvgln extends ComponentSvg {
get defaults() {
return {
...super.defaults,
selectable: false,
hoverable: false,
layerable: false,
},
},
{
isComponent: (el, opts = {}) => !!opts.inSvg,
};
}
);
}
ComponentSvgln.isComponent = (el, opts = {}) => !!opts.inSvg;

27
src/dom_components/model/ComponentTable.js

@ -3,22 +3,21 @@ import { toLowerCase } from 'utils/mixins';
const type = 'table';
export default Component.extend(
{
defaults: {
...Component.prototype.defaults,
export default class ComponentTable extends Component {
get defaults() {
return {
...super.defaults,
type,
tagName: type,
droppable: ['tbody', 'thead', 'tfoot'],
},
};
}
initialize(o, opt) {
Component.prototype.initialize.apply(this, arguments);
const components = this.get('components');
!components.length && components.add({ type: 'tbody' });
},
},
{
isComponent: el => toLowerCase(el.tagName) === type,
initialize(o, opt) {
Component.prototype.initialize.apply(this, arguments);
const components = this.get('components');
!components.length && components.add({ type: 'tbody' });
}
);
}
ComponentTable.isComponent = el => toLowerCase(el.tagName) === type;

72
src/dom_components/model/ComponentTableBody.js

@ -3,51 +3,49 @@ import { toLowerCase } from 'utils/mixins';
const type = 'tbody';
export default Component.extend(
{
defaults: {
...Component.prototype.defaults,
export default class ComponentTableBody extends Component {
get defaults() {
return {
...super.defaults,
type,
tagName: type,
draggable: ['table'],
droppable: ['tr'],
columns: 1,
rows: 1,
},
initialize(o, opt) {
Component.prototype.initialize.apply(this, arguments);
const components = this.get('components');
let columns = this.get('columns');
let rows = this.get('rows');
// Init components if empty
if (!components.length) {
const rowsToAdd = [];
while (rows--) {
const columnsToAdd = [];
let clm = columns;
while (clm--) {
columnsToAdd.push({
type: 'cell',
classes: ['cell'],
});
}
rowsToAdd.push({
type: 'row',
classes: ['row'],
components: columnsToAdd,
};
}
initialize(o, opt) {
Component.prototype.initialize.apply(this, arguments);
const components = this.get('components');
let columns = this.get('columns');
let rows = this.get('rows');
// Init components if empty
if (!components.length) {
const rowsToAdd = [];
while (rows--) {
const columnsToAdd = [];
let clm = columns;
while (clm--) {
columnsToAdd.push({
type: 'cell',
classes: ['cell'],
});
}
components.add(rowsToAdd);
rowsToAdd.push({
type: 'row',
classes: ['row'],
components: columnsToAdd,
});
}
},
},
{
isComponent: el => toLowerCase(el.tagName) === type,
components.add(rowsToAdd);
}
}
);
}
ComponentTableBody.isComponent = el => toLowerCase(el.tagName) === type;

17
src/dom_components/model/ComponentTableCell.js

@ -1,16 +1,15 @@
import Component from './Component';
import { toLowerCase } from 'utils/mixins';
export default Component.extend(
{
defaults: {
...Component.prototype.defaults,
export default class ComponentTableCell extends Component {
get defaults() {
return {
...super.defaults,
type: 'cell',
tagName: 'td',
draggable: ['tr'],
},
},
{
isComponent: el => ['td', 'th'].indexOf(toLowerCase(el.tagName)) >= 0,
};
}
);
}
ComponentTableCell.isComponent = el => ['td', 'th'].indexOf(toLowerCase(el.tagName)) >= 0;

17
src/dom_components/model/ComponentTableFoot.js

@ -3,15 +3,14 @@ import { toLowerCase } from 'utils/mixins';
const type = 'tfoot';
export default ComponentTableBody.extend(
{
defaults: {
...ComponentTableBody.prototype.defaults,
export default class ComponentTableFoot extends ComponentTableBody {
get defaults() {
return {
...super.defaults,
type,
tagName: type,
},
},
{
isComponent: el => toLowerCase(el.tagName) === type,
};
}
);
}
ComponentTableFoot.isComponent = el => toLowerCase(el.tagName) === type;

17
src/dom_components/model/ComponentTableHead.js

@ -3,15 +3,14 @@ import { toLowerCase } from 'utils/mixins';
const type = 'thead';
export default ComponentTableBody.extend(
{
defaults: {
...ComponentTableBody.prototype.defaults,
export default class ComponentTableHead extends ComponentTableBody {
get defaults() {
return {
...super.defaults,
type,
tagName: type,
},
},
{
isComponent: el => toLowerCase(el.tagName) === type,
};
}
);
}
ComponentTableHead.isComponent = el => toLowerCase(el.tagName) === type;

17
src/dom_components/model/ComponentTableRow.js

@ -3,16 +3,15 @@ import { toLowerCase } from 'utils/mixins';
const tagName = 'tr';
export default Component.extend(
{
defaults: {
...Component.prototype.defaults,
export default class ComponentTableRow extends Component {
get defaults() {
return {
...super.defaults,
tagName,
draggable: ['thead', 'tbody', 'tfoot'],
droppable: ['th', 'td'],
},
},
{
isComponent: el => toLowerCase(el.tagName) === tagName,
};
}
);
}
ComponentTableRow.isComponent = el => toLowerCase(el.tagName) === tagName;

18
src/dom_components/model/ComponentText.js

@ -1,10 +1,12 @@
import Component from './Component';
export default class ComponentText extends Component {}
ComponentText.prototype.defaults = {
...Component.getDefaults(),
type: 'text',
droppable: false,
editable: true,
};
export default class ComponentText extends Component {
get defaults() {
return {
...super.defaults,
type: 'text',
droppable: false,
editable: true,
};
}
}

45
src/dom_components/model/ComponentTextNode.js

@ -1,33 +1,32 @@
import Component from './Component';
import { escape } from 'utils/mixins';
export default Component.extend(
{
defaults: {
...Component.prototype.defaults,
export default class ComponentTextNode extends Component {
get defaults() {
return {
...super.defaults,
tagName: '',
droppable: false,
layerable: false,
selectable: false,
editable: true,
},
};
}
toHTML() {
const parent = this.parent();
const cnt = this.get('content');
return parent && parent.is('script') ? cnt : escape(cnt);
}
}
toHTML() {
const parent = this.parent();
const cnt = this.get('content');
return parent && parent.is('script') ? cnt : escape(cnt);
},
},
{
isComponent(el) {
var result = '';
if (el.nodeType === 3) {
result = {
type: 'textnode',
content: el.textContent,
};
}
return result;
},
ComponentTextNode.isComponent = el => {
var result = '';
if (el.nodeType === 3) {
result = {
type: 'textnode',
content: el.textContent,
};
}
);
return result;
};

622
src/dom_components/model/ComponentVideo.js

@ -1,4 +1,4 @@
import Component from './ComponentImage';
import ComponentImage from './ComponentImage';
import { toLowerCase } from 'utils/mixins';
const type = 'video';
@ -8,10 +8,10 @@ const ytnc = 'ytnc';
const hasParam = value => value && value !== '0';
export default Component.extend(
{
defaults: {
...Component.prototype.defaults,
export default class ComponentVideo extends ComponentImage {
get defaults() {
return {
...super.defaults,
type,
tagName: type,
videoId: '',
@ -31,332 +31,330 @@ export default Component.extend(
modestbranding: 0, // YT modest branding
sources: [],
attributes: { allowfullscreen: 'allowfullscreen' },
},
initialize(o, opt) {
this.em = opt.em;
if (this.get('src')) this.parseFromSrc();
this.updateTraits();
this.listenTo(this, 'change:provider', this.updateTraits);
this.listenTo(this, 'change:videoId change:provider', this.updateSrc);
Component.prototype.initialize.apply(this, arguments);
},
/**
* Update traits by provider
* @private
*/
updateTraits() {
const { em } = this;
const prov = this.get('provider');
let tagName = 'iframe';
let traits;
};
}
switch (prov) {
case yt:
case ytnc:
traits = this.getYoutubeTraits();
break;
case vi:
traits = this.getVimeoTraits();
break;
default:
tagName = 'video';
traits = this.getSourceTraits();
}
initialize(o, opt) {
this.em = opt.em;
if (this.get('src')) this.parseFromSrc();
this.updateTraits();
this.listenTo(this, 'change:provider', this.updateTraits);
this.listenTo(this, 'change:videoId change:provider', this.updateSrc);
ComponentImage.prototype.initialize.apply(this, arguments);
}
this.set({ tagName }, { silent: 1 }); // avoid break in view
this.set({ traits });
em.get('ready') && em.trigger('component:toggled');
},
/**
* Update traits by provider
* @private
*/
updateTraits() {
const { em } = this;
const prov = this.get('provider');
let tagName = 'iframe';
let traits;
/**
* Set attributes by src string
*/
parseFromSrc() {
const prov = this.get('provider');
const uri = this.parseUri(this.get('src'));
const qr = uri.query;
switch (prov) {
case yt:
case ytnc:
case vi:
this.set('videoId', uri.pathname.split('/').pop());
qr.list && this.set('list', qr.list);
hasParam(qr.autoplay) && this.set('autoplay', 1);
hasParam(qr.loop) && this.set('loop', 1);
parseInt(qr.controls) === 0 && this.set('controls', 0);
hasParam(qr.color) && this.set('color', qr.color);
qr.rel === '0' && this.set('rel', 0);
qr.modestbranding === '1' && this.set('modestbranding', 1);
break;
default:
}
},
switch (prov) {
case yt:
case ytnc:
traits = this.getYoutubeTraits();
break;
case vi:
traits = this.getVimeoTraits();
break;
default:
tagName = 'video';
traits = this.getSourceTraits();
}
/**
* Update src on change of video ID
* @private
*/
updateSrc() {
const prov = this.get('provider');
let src = '';
this.set({ tagName }, { silent: 1 }); // avoid break in view
this.set({ traits });
em.get('ready') && em.trigger('component:toggled');
}
switch (prov) {
case yt:
src = this.getYoutubeSrc();
break;
case ytnc:
src = this.getYoutubeNoCookieSrc();
break;
case vi:
src = this.getVimeoSrc();
break;
}
/**
* Set attributes by src string
*/
parseFromSrc() {
const prov = this.get('provider');
const uri = this.parseUri(this.get('src'));
const qr = uri.query;
switch (prov) {
case yt:
case ytnc:
case vi:
this.set('videoId', uri.pathname.split('/').pop());
qr.list && this.set('list', qr.list);
hasParam(qr.autoplay) && this.set('autoplay', 1);
hasParam(qr.loop) && this.set('loop', 1);
parseInt(qr.controls) === 0 && this.set('controls', 0);
hasParam(qr.color) && this.set('color', qr.color);
qr.rel === '0' && this.set('rel', 0);
qr.modestbranding === '1' && this.set('modestbranding', 1);
break;
default:
}
}
this.set({ src });
},
/**
* Update src on change of video ID
* @private
*/
updateSrc() {
const prov = this.get('provider');
let src = '';
/**
* Returns object of attributes for HTML
* @return {Object}
* @private
*/
getAttrToHTML(...args) {
var attr = Component.prototype.getAttrToHTML.apply(this, args);
var prov = this.get('provider');
switch (prov) {
case yt:
case ytnc:
case vi:
break;
default:
if (this.get('loop')) attr.loop = 'loop';
if (this.get('autoplay')) attr.autoplay = 'autoplay';
if (this.get('controls')) attr.controls = 'controls';
}
return attr;
},
switch (prov) {
case yt:
src = this.getYoutubeSrc();
break;
case ytnc:
src = this.getYoutubeNoCookieSrc();
break;
case vi:
src = this.getVimeoSrc();
break;
}
// Listen provider change and switch traits, in TraitView listen traits change
this.set({ src });
}
/**
* Return the provider trait
* @return {Object}
* @private
*/
getProviderTrait() {
return {
type: 'select',
label: 'Provider',
name: 'provider',
changeProp: 1,
options: [
{ value: 'so', name: 'HTML5 Source' },
{ value: yt, name: 'Youtube' },
{ value: ytnc, name: 'Youtube (no cookie)' },
{ value: vi, name: 'Vimeo' },
],
};
},
/**
* Returns object of attributes for HTML
* @return {Object}
* @private
*/
getAttrToHTML(...args) {
var attr = Component.prototype.getAttrToHTML.apply(this, args);
var prov = this.get('provider');
switch (prov) {
case yt:
case ytnc:
case vi:
break;
default:
if (this.get('loop')) attr.loop = 'loop';
if (this.get('autoplay')) attr.autoplay = 'autoplay';
if (this.get('controls')) attr.controls = 'controls';
}
return attr;
}
/**
* Return traits for the source provider
* @return {Array<Object>}
* @private
*/
getSourceTraits() {
return [
this.getProviderTrait(),
{
label: 'Source',
name: 'src',
placeholder: 'eg. ./media/video.mp4',
changeProp: 1,
},
{
label: 'Poster',
name: 'poster',
placeholder: 'eg. ./media/image.jpg',
// changeProp: 1
},
this.getAutoplayTrait(),
this.getLoopTrait(),
this.getControlsTrait(),
];
},
/**
* Return traits for the source provider
* @return {Array<Object>}
* @private
*/
getYoutubeTraits() {
return [
this.getProviderTrait(),
{
label: 'Video ID',
name: 'videoId',
placeholder: 'eg. jNQXAC9IVRw',
changeProp: 1,
},
this.getAutoplayTrait(),
this.getLoopTrait(),
this.getControlsTrait(),
{
type: 'checkbox',
label: 'Related',
name: 'rel',
changeProp: 1,
},
{
type: 'checkbox',
label: 'Modest',
name: 'modestbranding',
changeProp: 1,
},
];
},
// Listen provider change and switch traits, in TraitView listen traits change
/**
* Return traits for the source provider
* @return {Array<Object>}
* @private
*/
getVimeoTraits() {
return [
this.getProviderTrait(),
{
label: 'Video ID',
name: 'videoId',
placeholder: 'eg. 123456789',
changeProp: 1,
},
{
label: 'Color',
name: 'color',
placeholder: 'eg. FF0000',
changeProp: 1,
},
this.getAutoplayTrait(),
this.getLoopTrait(),
];
},
/**
* Return the provider trait
* @return {Object}
* @private
*/
getProviderTrait() {
return {
type: 'select',
label: 'Provider',
name: 'provider',
changeProp: 1,
options: [
{ value: 'so', name: 'HTML5 Source' },
{ value: yt, name: 'Youtube' },
{ value: ytnc, name: 'Youtube (no cookie)' },
{ value: vi, name: 'Vimeo' },
],
};
}
/**
* Return object trait
* @return {Object}
* @private
*/
getAutoplayTrait() {
return {
/**
* Return traits for the source provider
* @return {Array<Object>}
* @private
*/
getSourceTraits() {
return [
this.getProviderTrait(),
{
label: 'Source',
name: 'src',
placeholder: 'eg. ./media/video.mp4',
changeProp: 1,
},
{
label: 'Poster',
name: 'poster',
placeholder: 'eg. ./media/image.jpg',
// changeProp: 1
},
this.getAutoplayTrait(),
this.getLoopTrait(),
this.getControlsTrait(),
];
}
/**
* Return traits for the source provider
* @return {Array<Object>}
* @private
*/
getYoutubeTraits() {
return [
this.getProviderTrait(),
{
label: 'Video ID',
name: 'videoId',
placeholder: 'eg. jNQXAC9IVRw',
changeProp: 1,
},
this.getAutoplayTrait(),
this.getLoopTrait(),
this.getControlsTrait(),
{
type: 'checkbox',
label: 'Autoplay',
name: 'autoplay',
label: 'Related',
name: 'rel',
changeProp: 1,
};
},
/**
* Return object trait
* @return {Object}
* @private
*/
getLoopTrait() {
return {
},
{
type: 'checkbox',
label: 'Loop',
name: 'loop',
label: 'Modest',
name: 'modestbranding',
changeProp: 1,
};
},
},
];
}
/**
* Return object trait
* @return {Object}
* @private
*/
getControlsTrait() {
return {
type: 'checkbox',
label: 'Controls',
name: 'controls',
/**
* Return traits for the source provider
* @return {Array<Object>}
* @private
*/
getVimeoTraits() {
return [
this.getProviderTrait(),
{
label: 'Video ID',
name: 'videoId',
placeholder: 'eg. 123456789',
changeProp: 1,
};
},
},
{
label: 'Color',
name: 'color',
placeholder: 'eg. FF0000',
changeProp: 1,
},
this.getAutoplayTrait(),
this.getLoopTrait(),
];
}
/**
* Returns url to youtube video
* @return {string}
* @private
*/
getYoutubeSrc() {
const id = this.get('videoId');
let url = this.get('ytUrl');
const list = this.get('list');
url += id + (id.indexOf('?') < 0 ? '?' : '');
url += list ? `&list=${list}` : '';
url += this.get('autoplay') ? '&autoplay=1' : '';
url += !this.get('controls') ? '&controls=0&showinfo=0' : '';
// Loop works only with playlist enabled
// https://stackoverflow.com/questions/25779966/youtube-iframe-loop-doesnt-work
url += this.get('loop') ? `&loop=1&playlist=${id}` : '';
url += this.get('rel') ? '' : '&rel=0';
url += this.get('modestbranding') ? '&modestbranding=1' : '';
return url;
},
/**
* Return object trait
* @return {Object}
* @private
*/
getAutoplayTrait() {
return {
type: 'checkbox',
label: 'Autoplay',
name: 'autoplay',
changeProp: 1,
};
}
/**
* Returns url to youtube no cookie video
* @return {string}
* @private
*/
getYoutubeNoCookieSrc() {
let url = this.getYoutubeSrc();
url = url.replace(this.get('ytUrl'), this.get('ytncUrl'));
return url;
},
/**
* Return object trait
* @return {Object}
* @private
*/
getLoopTrait() {
return {
type: 'checkbox',
label: 'Loop',
name: 'loop',
changeProp: 1,
};
}
/**
* Returns url to vimeo video
* @return {string}
* @private
*/
getVimeoSrc() {
var url = this.get('viUrl');
url += this.get('videoId') + '?';
url += this.get('autoplay') ? '&autoplay=1' : '';
url += this.get('loop') ? '&loop=1' : '';
url += !this.get('controls') ? '&title=0&portrait=0&badge=0' : '';
url += this.get('color') ? '&color=' + this.get('color') : '';
return url;
},
},
{
/**
* Detect if the passed element is a valid component.
* In case the element is valid an object abstracted
* from the element will be returned
* @param {HTMLElement}
* @return {Object}
* @private
*/
isComponent(el) {
let result = '';
const { tagName, src } = el;
const isYtProv = /youtube\.com\/embed/.test(src);
const isYtncProv = /youtube-nocookie\.com\/embed/.test(src);
const isViProv = /player\.vimeo\.com\/video/.test(src);
const isExtProv = isYtProv || isYtncProv || isViProv;
if (toLowerCase(tagName) == type || (toLowerCase(tagName) == 'iframe' && isExtProv)) {
result = { type: 'video' };
if (src) result.src = src;
if (isExtProv) {
if (isYtProv) result.provider = yt;
else if (isYtncProv) result.provider = ytnc;
else if (isViProv) result.provider = vi;
}
}
return result;
},
/**
* Return object trait
* @return {Object}
* @private
*/
getControlsTrait() {
return {
type: 'checkbox',
label: 'Controls',
name: 'controls',
changeProp: 1,
};
}
/**
* Returns url to youtube video
* @return {string}
* @private
*/
getYoutubeSrc() {
const id = this.get('videoId');
let url = this.get('ytUrl');
const list = this.get('list');
url += id + (id.indexOf('?') < 0 ? '?' : '');
url += list ? `&list=${list}` : '';
url += this.get('autoplay') ? '&autoplay=1' : '';
url += !this.get('controls') ? '&controls=0&showinfo=0' : '';
// Loop works only with playlist enabled
// https://stackoverflow.com/questions/25779966/youtube-iframe-loop-doesnt-work
url += this.get('loop') ? `&loop=1&playlist=${id}` : '';
url += this.get('rel') ? '' : '&rel=0';
url += this.get('modestbranding') ? '&modestbranding=1' : '';
return url;
}
/**
* Returns url to youtube no cookie video
* @return {string}
* @private
*/
getYoutubeNoCookieSrc() {
let url = this.getYoutubeSrc();
url = url.replace(this.get('ytUrl'), this.get('ytncUrl'));
return url;
}
/**
* Returns url to vimeo video
* @return {string}
* @private
*/
getVimeoSrc() {
var url = this.get('viUrl');
url += this.get('videoId') + '?';
url += this.get('autoplay') ? '&autoplay=1' : '';
url += this.get('loop') ? '&loop=1' : '';
url += !this.get('controls') ? '&title=0&portrait=0&badge=0' : '';
url += this.get('color') ? '&color=' + this.get('color') : '';
return url;
}
}
/**
* Detect if the passed element is a valid component.
* In case the element is valid an object abstracted
* from the element will be returned
* @param {HTMLElement}
* @return {Object}
* @private
*/
ComponentVideo.isComponent = el => {
let result = '';
const { tagName, src } = el;
const isYtProv = /youtube\.com\/embed/.test(src);
const isYtncProv = /youtube-nocookie\.com\/embed/.test(src);
const isViProv = /player\.vimeo\.com\/video/.test(src);
const isExtProv = isYtProv || isYtncProv || isViProv;
if (toLowerCase(tagName) == type || (toLowerCase(tagName) == 'iframe' && isExtProv)) {
result = { type: 'video' };
if (src) result.src = src;
if (isExtProv) {
if (isYtProv) result.provider = yt;
else if (isYtncProv) result.provider = ytnc;
else if (isViProv) result.provider = vi;
}
}
);
return result;
};

2
src/dom_components/model/ComponentWrapper.js

@ -3,7 +3,7 @@ import Component from './Component';
export default class ComponentWrapper extends Component {
defaults() {
return {
...Component.getDefaults(),
...super.defaults,
tagName: 'body',
removable: false,
copyable: false,

16
src/domain_abstract/model/StyleableModel.js

@ -123,22 +123,6 @@ export default class StyleableModel extends Model {
}
_validate(attr, opts) {
const { style } = attr;
const em = this.em || opts.em;
const onBeforeStyle = em?.get('CssComposer')?.getConfig().onBeforeStyle;
if (style && onBeforeStyle) {
const newStyle = onBeforeStyle({ ...style });
newStyle &&
keys(style).map(prop => {
if (isUndefined(newStyle[prop])) delete attr.style[prop];
});
newStyle &&
keys(newStyle).map(prop => {
attr.style[prop] = newStyle[prop];
});
}
return true;
}
}

4
test/specs/dom_components/model/ComponentImage.js

@ -25,9 +25,9 @@ describe('ComponentImage', () => {
describe('.initialize', () => {
test('when a base 64 default image is provided, it uses the default image', () => {
let imageUrl = buildBase64UrlFromSvg(ComponentImage.getDefaults().src);
let imageUrl = buildBase64UrlFromSvg(ComponentImage.prototype.defaults.src);
let componentImage = new ComponentImage({ attributes: { src: imageUrl } }, { ...compOpts });
expect(componentImage.get('src')).toEqual(ComponentImage.getDefaults().src);
expect(componentImage.get('src')).toEqual(ComponentImage.prototype.defaults.src);
expect(componentImage.isDefaultSrc()).toBeTruthy();
});

3
test/specs/dom_components/model/ComponentTypes.js

@ -24,6 +24,9 @@ describe('Component Types', () => {
afterEach(() => {
wrapper.components().reset();
editor = new Editor({ allowScripts: 1 });
editor.getModel().get('PageManager').onLoad();
wrapper = editor.getWrapper();
});
test('<img> is correctly recognized', () => {

Loading…
Cancel
Save