Browse Source

Merge branch 'GrapesJS:dev' into dev

pull/5719/head
Brian Ernesto 3 years ago
committed by GitHub
parent
commit
512e997085
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      dist/css/grapes.min.css
  2. 4
      dist/grapes.min.js
  3. 2
      dist/grapes.min.js.map
  4. 8830
      dist/grapes.mjs
  5. 2
      dist/grapes.mjs.map
  6. 1615
      dist/index.d.ts
  7. 18
      docs/api/canvas.md
  8. 2
      package.json
  9. 3
      src/dom_components/model/Component.ts
  10. 4
      src/dom_components/view/ComponentLinkView.ts
  11. 2
      src/dom_components/view/ComponentTextView.ts
  12. 10
      src/domain_abstract/ui/InputColor.ts
  13. 15
      src/parser/config/config.ts
  14. 20
      src/parser/model/ParserHtml.ts
  15. 4
      src/styles/scss/_gjs_canvas.scss
  16. 32
      src/styles/scss/main.scss
  17. 8
      src/utils/ColorPicker.ts
  18. 6
      test/specs/dom_components/model/Component.ts
  19. 55
      test/specs/parser/model/ParserHtml.ts

2
dist/css/grapes.min.css

File diff suppressed because one or more lines are too long

4
dist/grapes.min.js

File diff suppressed because one or more lines are too long

2
dist/grapes.min.js.map

File diff suppressed because one or more lines are too long

8830
dist/grapes.mjs

File diff suppressed because it is too large

2
dist/grapes.mjs.map

File diff suppressed because one or more lines are too long

1615
dist/index.d.ts

File diff suppressed because it is too large

18
docs/api/canvas.md

@ -90,6 +90,14 @@ editor.on('canvas:pointer', () => {
});
```
* `canvas:refresh` Canvas was refreshed to update elements on top,
like spots/tools (eg. via `editor.Canvas.refresh()` or on frame resize).
```javascript
editor.on('canvas:refresh', (canvasRefreshOptions) => {
console.log('Canvas refreshed with options:', canvasRefreshOptions);
});
```
* `canvas:frame:load` Frame loaded in canvas.
The event is triggered right after iframe's `onload`.
```javascript
@ -396,6 +404,16 @@ Transform a box rect from the world coordinate system to the screen one.
Returns **[Object][2]**
## refresh
Update canvas for spots/tools positioning.
### Parameters
* `opts` **[Object][2]?** Options. (optional, default `{}`)
* `opts.spots` **[Object][2]** Update the position of spots. (optional, default `false`)
[1]: https://github.com/GrapesJS/grapesjs/blob/master/src/canvas/config/config.ts
[2]: https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object

2
package.json

@ -1,7 +1,7 @@
{
"name": "grapesjs",
"description": "Free and Open Source Web Builder Framework",
"version": "0.21.8",
"version": "0.21.9",
"author": "Artur Arseniev",
"license": "BSD-3-Clause",
"homepage": "http://grapesjs.com",

3
src/dom_components/model/Component.ts

@ -1590,7 +1590,8 @@ export default class Component extends StyleableModel<ComponentProperties> {
forEach(props, (value, key) => {
const skipProps = ['classes', 'attributes', 'components'];
if (key[0] !== '_' && skipProps.indexOf(key) < 0) {
attributes[`data-gjs-${key}`] = isArray(value) || isObject(value) ? JSON.stringify(value) : value;
attributes[`data-gjs-${key}`] =
isArray(value) || isObject(value) ? JSON.stringify(value) : isBoolean(value) ? `${value}` : value;
}
});
}

4
src/dom_components/view/ComponentLinkView.ts

@ -1,7 +1,7 @@
import ComponentLink from '../model/ComponentLink';
import ComponentView from './ComponentView';
import ComponentTextView from './ComponentTextView';
export default class ComponentLinkView extends ComponentView<ComponentLink> {
export default class ComponentLinkView extends ComponentTextView<ComponentLink> {
render() {
super.render();
// I need capturing instead of bubbling as bubbled clicks from other

2
src/dom_components/view/ComponentTextView.ts

@ -10,7 +10,7 @@ import ComponentText from '../model/ComponentText';
import { ComponentDefinition } from '../model/types';
import ComponentView from './ComponentView';
export default class ComponentTextView extends ComponentView<ComponentText> {
export default class ComponentTextView<TComp extends ComponentText = ComponentText> extends ComponentView<TComp> {
rte?: RichTextEditorModule;
rteEnabled?: boolean;
activeRte?: RichTextEditor;

10
src/domain_abstract/ui/InputColor.ts

@ -101,10 +101,9 @@ export default class InputColor extends Input {
const ppfx = this.ppfx;
const { onChange } = opts;
var colorEl = $(`<div class="${this.ppfx}field-color-picker"></div>`);
var cpStyle = colorEl.get(0)!.style;
var elToAppend = em && em.config ? em.config.el : '';
var colorPickerConfig = (em && em.getConfig && em.getConfig().colorPicker) || {};
const colorEl = $(`<div class="${this.ppfx}field-color-picker"></div>`);
const cpStyle = colorEl.get(0)!.style;
const colorPickerConfig = (em && em.getConfig && em.getConfig().colorPicker) || {};
this.movedColor = '';
let changed = false;
@ -123,8 +122,7 @@ export default class InputColor extends Input {
// @ts-ignore
colorEl.spectrum({
color: model.getValue() || false,
containerClassName: `${ppfx}one-bg ${ppfx}two-color`,
appendTo: elToAppend || 'body',
containerClassName: `${ppfx}one-bg ${ppfx}two-color ${ppfx}editor-sp`,
maxSelectionSize: 8,
showPalette: true,
showAlpha: true,

15
src/parser/config/config.ts

@ -32,11 +32,25 @@ export interface HTMLParserOptions {
*/
allowUnsafeAttr?: boolean;
/**
* Allow unsafe HTML attribute values (eg. `src="javascript:..."`).
* @default false
*/
allowUnsafeAttrValue?: boolean;
/**
* When false, removes empty text nodes when parsed, unless they contain a space.
* @default false
*/
keepEmptyTextNodes?: boolean;
/**
* Custom transformer to run before passing the input HTML to the parser.
* A common use case might be to sanitize the input string.
* @example
* preParser: htmlString => DOMPurify.sanitize(htmlString)
*/
preParser?: (input: string, opts: { editor: Editor }) => string;
}
export interface ParserConfig {
@ -84,6 +98,7 @@ const config: ParserConfig = {
htmlType: 'text/html',
allowScripts: false,
allowUnsafeAttr: false,
allowUnsafeAttrValue: false,
keepEmptyTextNodes: false,
},
};

20
src/parser/model/ParserHtml.ts

@ -310,7 +310,9 @@ const ParserHtml = (em?: EditorModel, config: ParserConfig & { returnArray?: boo
htmlType: config.optionsHtml?.htmlType || config.htmlType,
...opts,
};
const el = isFunction(cf.parserHtml) ? cf.parserHtml(str, options) : BrowserParserHtml(str, options);
const { preParser } = options;
const input = isFunction(preParser) ? preParser(str, { editor: em?.getEditor()! }) : str;
const el = isFunction(cf.parserHtml) ? cf.parserHtml(input, options) : BrowserParserHtml(input, options);
const scripts = el.querySelectorAll('script');
let i = scripts.length;
@ -323,8 +325,8 @@ const ParserHtml = (em?: EditorModel, config: ParserConfig & { returnArray?: boo
}
// Remove unsafe attributes
if (!options.allowUnsafeAttr) {
this.__clearUnsafeAttr(el);
if (!options.allowUnsafeAttr || !options.allowUnsafeAttrValue) {
this.__sanitizeNode(el, options);
}
// Detach style tags and parse them
@ -341,26 +343,28 @@ const ParserHtml = (em?: EditorModel, config: ParserConfig & { returnArray?: boo
if (styleStr) res.css = parserCss.parse(styleStr);
}
em && em.trigger(`${event}:root`, { input: str, root: el });
em?.trigger(`${event}:root`, { input, root: el });
const result = this.parseNode(el, cf);
// I have to keep it otherwise it breaks the DomComponents.addComponent (returns always array)
const resHtml = result.length === 1 && !cf.returnArray ? result[0] : result;
res.html = resHtml;
em && em.trigger(event, { input: str, output: res });
em?.trigger(event, { input, output: res });
return res;
},
__clearUnsafeAttr(node: HTMLElement) {
__sanitizeNode(node: HTMLElement, opts: HTMLParserOptions) {
const attrs = node.attributes || [];
const nodes = node.childNodes || [];
const toRemove: string[] = [];
each(attrs, attr => {
const name = attr.nodeName || '';
name.indexOf('on') === 0 && toRemove.push(name);
const value = attr.nodeValue || '';
!opts.allowUnsafeAttr && name.startsWith('on') && toRemove.push(name);
!opts.allowUnsafeAttrValue && value.startsWith('javascript:') && toRemove.push(name);
});
toRemove.map(name => node.removeAttribute(name));
each(nodes, node => this.__clearUnsafeAttr(node as HTMLElement));
each(nodes, node => this.__sanitizeNode(node as HTMLElement, opts));
},
};
};

4
src/styles/scss/_gjs_canvas.scss

@ -271,6 +271,10 @@ $guide_pad: 5px !default;
outline: none;
z-index: 1;
}
/* Always place the tools above the highlighter */
##{$app-prefix}tools {
z-index: 2;
}
/* This simulate body behaviour */
// > div:first-child {

32
src/styles/scss/main.scss

@ -444,17 +444,18 @@ $colorsAll: (one, var(--gjs-primary-color)),
/********* Spectrum **********/
.#{$app-prefix}editor-cont {
.#{$app-prefix}editor-sp {
border: 1px solid var(--gjs-main-dark-color);
box-shadow: 0 0 7px var(--gjs-main-dark-color);
border-radius: 3px;
}
.#{$app-prefix}editor-sp {
.sp-hue, .sp-slider{ cursor: row-resize;}
.sp-color, .sp-dragger{ cursor: crosshair;}
.sp-alpha-inner, .sp-alpha-handle{cursor: col-resize;}
.sp-hue{ left: 90%; }
.sp-color{right: 15%;}
.sp-container {
border: 1px solid var(--gjs-main-dark-color);
box-shadow: 0 0 7px var(--gjs-main-dark-color);
border-radius: 3px;
}
.sp-picker-container{border:none;}
.colpick_dark .colpick_color { outline: 1px solid var(--gjs-main-dark-color);}
.sp-cancel, .sp-cancel:hover {
@ -484,14 +485,17 @@ $colorsAll: (one, var(--gjs-primary-color)),
width: 100%;
position: relative;
text-align: right;
}
.sp-container button, .sp-container button:hover, .sp-container button:active{
background: var(--gjs-main-dark-color);
border-color: var(--gjs-main-dark-color);
color: var(--gjs-font-color);
text-shadow: none;
box-shadow: none;
padding: 3px 5px;
.sp-choose,
.sp-choose:hover,
.sp-choose:active {
background: var(--gjs-main-dark-color);
border-color: var(--gjs-main-dark-color);
color: var(--gjs-font-color);
text-shadow: none;
box-shadow: none;
padding: 3px 5px;
}
}
.sp-palette-container{
border: none;

8
src/utils/ColorPicker.ts

@ -13,7 +13,7 @@ export interface ColorPickerOptions {
change?: () => void;
show?: () => void;
hide?: () => void;
color?: boolean;
color?: boolean | string;
flat?: boolean;
showInput?: boolean;
allowEmpty?: boolean;
@ -25,8 +25,8 @@ export interface ColorPickerOptions {
hideAfterPaletteSelect?: boolean;
togglePaletteOnly?: boolean;
showSelectionPalette?: boolean;
localStorageKey?: boolean;
appendTo?: string;
localStorageKey?: boolean | string;
appendTo?: string | HTMLElement;
maxSelectionSize?: number;
cancelText?: string;
chooseText?: string;
@ -34,7 +34,7 @@ export interface ColorPickerOptions {
togglePaletteLessText?: string;
clearText?: string;
noColorSelectedText?: string;
preferredFormat?: boolean;
preferredFormat?: boolean | string;
containerClassName?: string;
replacerClassName?: string;
showAlpha?: boolean;

6
test/specs/dom_components/model/Component.ts

@ -152,7 +152,7 @@ describe('Component', () => {
obj = new Component({}, compOpts);
obj.set({
bool: true,
boolf: false,
removable: false,
string: 'st\'ri"ng',
array: [1, 'string', true],
object: { a: 1, b: 'string', c: true },
@ -165,12 +165,12 @@ describe('Component', () => {
let resStr = "st'ri&quot;ng";
let resArr = '[1,&quot;string&quot;,true]';
let resObj = '{&quot;a&quot;:1,&quot;b&quot;:&quot;string&quot;,&quot;c&quot;:true}';
let res = `<div data-gjs-bool data-gjs-string="${resStr}" data-gjs-array="${resArr}" data-gjs-object="${resObj}" data-gjs-empty="" data-gjs-zero="0"></div>`;
let res = `<div data-gjs-removable="false" data-gjs-bool="true" data-gjs-string="${resStr}" data-gjs-array="${resArr}" data-gjs-object="${resObj}" data-gjs-empty="" data-gjs-zero="0"></div>`;
expect(obj.toHTML({ withProps: true })).toEqual(res);
resStr = 'st&apos;ri"ng';
resArr = '[1,"string",true]';
resObj = '{"a":1,"b":"string","c":true}';
res = `<div data-gjs-bool data-gjs-string='${resStr}' data-gjs-array='${resArr}' data-gjs-object='${resObj}' data-gjs-empty="" data-gjs-zero="0"></div>`;
res = `<div data-gjs-removable="false" data-gjs-bool="true" data-gjs-string='${resStr}' data-gjs-array='${resArr}' data-gjs-object='${resObj}' data-gjs-empty="" data-gjs-zero="0"></div>`;
expect(obj.toHTML({ withProps: true, altQuoteAttr: true })).toEqual(res);
});

55
test/specs/parser/model/ParserHtml.ts

@ -600,4 +600,59 @@ describe('ParserHtml', () => {
];
expect(obj.parse(str).html).toEqual(result);
});
describe('Options', () => {
test('Remove unsafe attributes', () => {
const str = '<img src="path/img" data-test="1" class="test" onload="unsafe"/>';
const result = {
type: 'image',
tagName: 'img',
classes: ['test'],
attributes: {
src: 'path/img',
'data-test': '1',
},
};
expect(obj.parse(str).html).toEqual([result]);
expect(obj.parse(str, null, { allowUnsafeAttr: true }).html).toEqual([
{
...result,
attributes: {
...result.attributes,
onload: 'unsafe',
},
},
]);
});
test('Remove unsafe attribute values', () => {
const str = '<iframe src="javascript:alert(1)"></iframe>';
const result = {
type: 'iframe',
tagName: 'iframe',
};
expect(obj.parse(str).html).toEqual([result]);
expect(obj.parse(str, null, { allowUnsafeAttrValue: true }).html).toEqual([
{
...result,
attributes: {
src: 'javascript:alert(1)',
},
},
]);
});
test('Custom preParser option', () => {
const str = '<iframe src="javascript:alert(1)"></iframe>';
const result = {
type: 'iframe',
tagName: 'iframe',
attributes: {
src: 'test:alert(1)',
},
};
const preParser = (str: string) => str.replace('javascript:', 'test:');
expect(obj.parse(str, null, { preParser }).html).toEqual([result]);
});
});
});

Loading…
Cancel
Save