Browse Source

Convert Component into class and use getter for defaults

pull/4439/head
Alex Ritter 4 years ago
parent
commit
4b729a8a08
  1. 11
      src/dom_components/index.ts
  2. 92
      src/dom_components/model/Component.js
  3. 39
      src/dom_components/model/ComponentComment.js
  4. 31
      src/dom_components/model/ComponentFrame.js
  5. 231
      src/dom_components/model/ComponentImage.js
  6. 19
      src/dom_components/model/ComponentLabel.js
  7. 18
      src/dom_components/model/ComponentLink.js
  8. 126
      src/dom_components/model/ComponentMap.js
  9. 35
      src/dom_components/model/ComponentScript.js
  10. 29
      src/dom_components/model/ComponentSvg.js
  11. 19
      src/dom_components/model/ComponentSvgIn.js
  12. 27
      src/dom_components/model/ComponentTable.js
  13. 72
      src/dom_components/model/ComponentTableBody.js
  14. 17
      src/dom_components/model/ComponentTableCell.js
  15. 17
      src/dom_components/model/ComponentTableFoot.js
  16. 17
      src/dom_components/model/ComponentTableHead.js
  17. 17
      src/dom_components/model/ComponentTableRow.js
  18. 18
      src/dom_components/model/ComponentText.js
  19. 45
      src/dom_components/model/ComponentTextNode.js
  20. 620
      src/dom_components/model/ComponentVideo.js
  21. 2
      src/dom_components/model/ComponentWrapper.js
  22. 4
      test/specs/dom_components/model/ComponentImage.js
  23. 3
      test/specs/dom_components/model/ComponentTypes.js

11
src/dom_components/index.ts

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

92
src/dom_components/model/Component.js

@ -94,6 +94,51 @@ export const keyUpdateInside = `${keyUpdate}-inside`;
* @module docsjs.Component * @module docsjs.Component
*/ */
export default class Component extends StyleableModel { 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 * Hook method, called once the model is created
*/ */
@ -1962,50 +2007,3 @@ Component.checkId = (components, styles = [], list = {}, opts = {}) => {
components && 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( export default class ComponentComment extends ComponentTextNode {
{ get defaults() {
defaults: { return { ...super.defaults };
...Component.prototype.defaults, }
},
toHTML() {
return `<!--${this.get('content')}-->`;
}
}
toHTML() { ComponentComment.isComponent = el => {
return `<!--${this.get('content')}-->`; if (el.nodeType == 8) {
}, return {
}, tagName: 'NULL',
{ type: 'comment',
isComponent(el) { content: el.textContent,
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'; const type = 'iframe';
export default Component.extend( export default class ComponentFrame extends Component {
{ get defaults() {
defaults() { return {
return { ...super.defaults,
...Component.prototype.defaults, type,
type, tagName: type,
tagName: type, droppable: false,
droppable: false, resizable: true,
resizable: true, traits: ['id', 'title', 'src'],
traits: ['id', 'title', 'src'], attributes: { frameborder: '0' },
attributes: { frameborder: '0' }, };
};
},
},
{
isComponent: el => toLowerCase(el.tagName) === type,
} }
); }
ComponentFrame.isComponent = el => toLowerCase(el.tagName) === type;

231
src/dom_components/model/ComponentImage.js

@ -5,10 +5,10 @@ import { toLowerCase, buildBase64UrlFromSvg } from 'utils/mixins';
const svgAttrs = 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)"'; '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( export default class ComponentImage extends Component {
{ get defaults() {
defaults: { return {
...Component.prototype.defaults, ...super.defaults,
type: 'image', type: 'image',
tagName: 'img', tagName: 'img',
void: true, void: true,
@ -19,130 +19,129 @@ export default Component.extend(
traits: ['alt'], traits: ['alt'],
src: `<svg ${svgAttrs}> src: `<svg ${svgAttrs}>
<path d="M8.5 13.5l2.5 3 3.5-4.5 4.5 6H5m16 1V5a2 2 0 0 0-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2z"></path> <path d="M8.5 13.5l2.5 3 3.5-4.5 4.5 6H5m16 1V5a2 2 0 0 0-2-2H5c-1.1 0-2 .9-2 2v14c0 1.1.9 2 2 2h14c1.1 0 2-.9 2-2z"></path>
</svg>`, </svg>`,
// Fallback image in case the src can't be loaded // Fallback image in case the src can't be loaded
// If you use SVG, xmlns="http://www.w3.org/2000/svg" is required // If you use SVG, xmlns="http://www.w3.org/2000/svg" is required
fallback: `<svg ${svgAttrs}> fallback: `<svg ${svgAttrs}>
<path d="M2.28 3L1 4.27l2 2V19c0 1.1.9 2 2 2h12.73l2 2L21 21.72 2.28 3m2.55 0L21 19.17V5a2 2 0 0 0-2-2H4.83M8.5 13.5l2.5 3 1-1.25L14.73 18H5l3.5-4.5z"></path> <path d="M2.28 3L1 4.27l2 2V19c0 1.1.9 2 2 2h12.73l2 2L21 21.72 2.28 3m2.55 0L21 19.17V5a2 2 0 0 0-2-2H4.83M8.5 13.5l2.5 3 1-1.25L14.73 18H5l3.5-4.5z"></path>
</svg>`, </svg>`,
// File to load asynchronously once the model is rendered // File to load asynchronously once the model is rendered
file: '', file: '',
}, };
}
initialize(o, opt) { initialize(o, opt) {
Component.prototype.initialize.apply(this, arguments); Component.prototype.initialize.apply(this, arguments);
const { src } = this.get('attributes'); const { src } = this.get('attributes');
if (src && buildBase64UrlFromSvg(result(this, 'defaults').src) !== src) { if (src && buildBase64UrlFromSvg(result(this, 'defaults').src) !== src) {
this.set('src', src, { silent: 1 }); this.set('src', src, { silent: 1 });
} }
}, }
initToolbar(...args) { initToolbar(...args) {
Component.prototype.initToolbar.apply(this, args); Component.prototype.initToolbar.apply(this, args);
const em = this.em; const em = this.em;
if (em) { if (em) {
var cmd = em.get('Commands'); var cmd = em.get('Commands');
var cmdName = 'image-editor'; 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;
}
}
if (!hasButtonBool) { // Add Image Editor button only if the default command exists
tb.push({ if (cmd.has(cmdName)) {
attributes: { class: 'fa fa-pencil' }, let hasButtonBool = false;
command: cmdName, var tb = this.get('toolbar');
});
this.set('toolbar', tb); 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; if (!hasButtonBool) {
}, tb.push({
attributes: { class: 'fa fa-pencil' },
isDefaultSrc() { command: cmdName,
const src = this.get('src'); });
const srcDef = result(this, 'defaults').src; this.set('toolbar', tb);
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;
} }
}
}
return obj; /**
}, * Returns object of attributes for HTML
* @return {Object}
/** * @private
* Parse uri */
* @param {string} uri getAttrToHTML(...args) {
* @return {object} const attr = Component.prototype.getAttrToHTML.apply(this, args);
* @private const src = this.getSrcResult();
*/ if (src) attr.src = src;
parseUri(uri) { return attr;
var el = document.createElement('a');
el.href = uri;
var query = {};
var qrs = el.search.substring(1).split('&');
for (var i = 0; i < qrs.length; i++) {
var pair = qrs[i].split('=');
var name = decodeURIComponent(pair[0]);
if (name) query[name] = decodeURIComponent(pair[1]);
}
return {
hostname: el.hostname,
pathname: el.pathname,
protocol: el.protocol,
search: el.search,
hash: el.hash,
port: el.port,
query,
};
},
},
{
isComponent: el => toLowerCase(el.tagName) === 'img',
} }
);
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;
}
return obj;
}
/**
* Parse uri
* @param {string} uri
* @return {object}
* @private
*/
parseUri(uri) {
var el = document.createElement('a');
el.href = uri;
var query = {};
var qrs = el.search.substring(1).split('&');
for (var i = 0; i < qrs.length; i++) {
var pair = qrs[i].split('=');
var name = decodeURIComponent(pair[0]);
if (name) query[name] = decodeURIComponent(pair[1]);
}
return {
hostname: el.hostname,
pathname: el.pathname,
protocol: el.protocol,
search: el.search,
hash: el.hash,
port: el.port,
query,
};
}
}
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'; import { toLowerCase } from 'utils/mixins';
const type = 'label'; const type = 'label';
export default Component.extend( export default class ComponentLabel extends ComponentText {
{ get defaults() {
defaults: { return {
...Component.prototype.defaults, ...super.defaults,
type, type,
tagName: type, tagName: type,
traits: ['id', 'title', 'for'], 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'; const type = 'link';
export default class ComponentLink extends ComponentText {} export default class ComponentLink extends ComponentText {
get defaults() {
ComponentLink.prototype.defaults = { return {
...ComponentText.getDefaults(), ...super.defaults,
type, type,
tagName: 'a', tagName: 'a',
traits: ['title', 'href', 'target'], traits: ['title', 'href', 'target'],
}; };
}
}
ComponentLink.isComponent = (el, opts = {}) => { ComponentLink.isComponent = (el, opts = {}) => {
let result; let result;

126
src/dom_components/model/ComponentMap.js

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

35
src/dom_components/model/ComponentScript.js

@ -3,29 +3,28 @@ import { toLowerCase } from 'utils/mixins';
const type = 'script'; const type = 'script';
export default Component.extend( export default class ComponentScript extends Component {
{ get defaults() {
defaults: { return {
...Component.prototype.defaults, ...super.defaults,
type, type,
tagName: type, tagName: type,
droppable: false, droppable: false,
draggable: false, draggable: false,
layerable: false, layerable: false,
}, };
}, }
{ }
isComponent(el) {
if (toLowerCase(el.tagName) == type) { ComponentScript.isComponent = el => {
const result = { type }; if (toLowerCase(el.tagName) == type) {
const result = { type };
if (el.src) { if (el.src) {
result.src = el.src; result.src = el.src;
result.onload = el.onload; 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'; const type = 'svg';
export default Component.extend( export default class ComponentSvg extends Component {
{ get defaults() {
defaults: { return {
...Component.prototype.defaults, ...super.defaults,
type, type,
tagName: type, tagName: type,
highlightable: 0, highlightable: 0,
resizable: { ratioDefault: 1 }, resizable: { ratioDefault: 1 },
}, };
}
getName() { getName() {
let name = this.get('tagName'); let name = this.get('tagName');
let customName = this.get('custom-name'); let customName = this.get('custom-name');
name = name.charAt(0).toUpperCase() + name.slice(1); name = name.charAt(0).toUpperCase() + name.slice(1);
return customName || name; return customName || name;
},
},
{
isComponent: el => toLowerCase(el.tagName) === type,
} }
); }
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 * Component for inner SVG elements
*/ */
export default Component.extend( export default class ComponentSvgln extends ComponentSvg {
{ get defaults() {
defaults: { return {
...Component.prototype.defaults, ...super.defaults,
selectable: false, selectable: false,
hoverable: false, hoverable: false,
layerable: 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'; const type = 'table';
export default Component.extend( export default class ComponentTable extends Component {
{ get defaults() {
defaults: { return {
...Component.prototype.defaults, ...super.defaults,
type, type,
tagName: type, tagName: type,
droppable: ['tbody', 'thead', 'tfoot'], droppable: ['tbody', 'thead', 'tfoot'],
}, };
}
initialize(o, opt) { initialize(o, opt) {
Component.prototype.initialize.apply(this, arguments); Component.prototype.initialize.apply(this, arguments);
const components = this.get('components'); const components = this.get('components');
!components.length && components.add({ type: 'tbody' }); !components.length && components.add({ type: 'tbody' });
},
},
{
isComponent: el => toLowerCase(el.tagName) === type,
} }
); }
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'; const type = 'tbody';
export default Component.extend( export default class ComponentTableBody extends Component {
{ get defaults() {
defaults: { return {
...Component.prototype.defaults, ...super.defaults,
type, type,
tagName: type, tagName: type,
draggable: ['table'], draggable: ['table'],
droppable: ['tr'], droppable: ['tr'],
columns: 1, columns: 1,
rows: 1, rows: 1,
}, };
}
initialize(o, opt) {
Component.prototype.initialize.apply(this, arguments); initialize(o, opt) {
const components = this.get('components'); Component.prototype.initialize.apply(this, arguments);
let columns = this.get('columns'); const components = this.get('components');
let rows = this.get('rows'); let columns = this.get('columns');
let rows = this.get('rows');
// Init components if empty
if (!components.length) { // Init components if empty
const rowsToAdd = []; if (!components.length) {
const rowsToAdd = [];
while (rows--) {
const columnsToAdd = []; while (rows--) {
let clm = columns; const columnsToAdd = [];
let clm = columns;
while (clm--) {
columnsToAdd.push({ while (clm--) {
type: 'cell', columnsToAdd.push({
classes: ['cell'], type: 'cell',
}); classes: ['cell'],
}
rowsToAdd.push({
type: 'row',
classes: ['row'],
components: columnsToAdd,
}); });
} }
components.add(rowsToAdd); rowsToAdd.push({
type: 'row',
classes: ['row'],
components: columnsToAdd,
});
} }
},
}, components.add(rowsToAdd);
{ }
isComponent: el => toLowerCase(el.tagName) === type,
} }
); }
ComponentTableBody.isComponent = el => toLowerCase(el.tagName) === type;

17
src/dom_components/model/ComponentTableCell.js

@ -1,16 +1,15 @@
import Component from './Component'; import Component from './Component';
import { toLowerCase } from 'utils/mixins'; import { toLowerCase } from 'utils/mixins';
export default Component.extend( export default class ComponentTableCell extends Component {
{ get defaults() {
defaults: { return {
...Component.prototype.defaults, ...super.defaults,
type: 'cell', type: 'cell',
tagName: 'td', tagName: 'td',
draggable: ['tr'], 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'; const type = 'tfoot';
export default ComponentTableBody.extend( export default class ComponentTableFoot extends ComponentTableBody {
{ get defaults() {
defaults: { return {
...ComponentTableBody.prototype.defaults, ...super.defaults,
type, type,
tagName: 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'; const type = 'thead';
export default ComponentTableBody.extend( export default class ComponentTableHead extends ComponentTableBody {
{ get defaults() {
defaults: { return {
...ComponentTableBody.prototype.defaults, ...super.defaults,
type, type,
tagName: 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'; const tagName = 'tr';
export default Component.extend( export default class ComponentTableRow extends Component {
{ get defaults() {
defaults: { return {
...Component.prototype.defaults, ...super.defaults,
tagName, tagName,
draggable: ['thead', 'tbody', 'tfoot'], draggable: ['thead', 'tbody', 'tfoot'],
droppable: ['th', 'td'], 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'; import Component from './Component';
export default class ComponentText extends Component {} export default class ComponentText extends Component {
get defaults() {
ComponentText.prototype.defaults = { return {
...Component.getDefaults(), ...super.defaults,
type: 'text', type: 'text',
droppable: false, droppable: false,
editable: true, editable: true,
}; };
}
}

45
src/dom_components/model/ComponentTextNode.js

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

620
src/dom_components/model/ComponentVideo.js

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

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

@ -25,9 +25,9 @@ describe('ComponentImage', () => {
describe('.initialize', () => { describe('.initialize', () => {
test('when a base 64 default image is provided, it uses the default image', () => { 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 }); 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(); expect(componentImage.isDefaultSrc()).toBeTruthy();
}); });

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

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

Loading…
Cancel
Save