Browse Source

Update TraitView with new API methods and update Trait docs

pull/2190/head
Artur Arseniev 7 years ago
parent
commit
a01039c651
  1. 2
      dist/css/grapes.min.css
  2. BIN
      docs/.vuepress/public/docs-init-link-trait.jpg
  3. BIN
      docs/.vuepress/public/docs-link-trait-raw.jpg
  4. 206
      docs/modules/Traits.md
  5. 2
      package.json
  6. 1
      src/styles/scss/_gjs_traits.scss
  7. 49
      src/trait_manager/view/TraitView.js

2
dist/css/grapes.min.css

File diff suppressed because one or more lines are too long

BIN
docs/.vuepress/public/docs-init-link-trait.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

BIN
docs/.vuepress/public/docs-link-trait-raw.jpg

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

206
docs/modules/Traits.md

@ -250,7 +250,7 @@ You can also easily add new traits or remove some other by using [`addTrait`](/a
// Add new trait // Add new trait
const component = editor.getSelected(); const component = editor.getSelected();
component.addTrait({ component.addTrait({
type: 'text', name: 'type',
... ...
}, { at: 0 }); }, { at: 0 });
// The `at` option indicates the index where to place the new trait, // The `at` option indicates the index where to place the new trait,
@ -267,6 +267,8 @@ component.removeTrait('type');
Generally, for most of the cases default types are enough, but sometimes you might need something more. Generally, for most of the cases default types are enough, but sometimes you might need something more.
In that case you can define a totally new type of trait and bind any kind of element to it. In that case you can define a totally new type of trait and bind any kind of element to it.
### Create element
Let's update the default `link` Component with a new kind of trat. This is the default situation of traits for a simple link. Let's update the default `link` Component with a new kind of trat. This is the default situation of traits for a simple link.
<img :src="$withBase('/default-link-comp.jpg')"> <img :src="$withBase('/default-link-comp.jpg')">
@ -274,12 +276,14 @@ Let's update the default `link` Component with a new kind of trat. This is the d
Let's just replace all of its traits with a new one, `href-next`, which will allow the user to select the type of href (eg. 'url', 'email', etc.) Let's just replace all of its traits with a new one, `href-next`, which will allow the user to select the type of href (eg. 'url', 'email', etc.)
```js ```js
// Update component
editor.DomComponents.addType('link', { editor.DomComponents.addType('link', {
model: { model: {
defaults: { defaults: {
traits: [ traits: [
{ {
type: 'href-next', type: 'href-next',
name: 'href',
label: 'New href', label: 'New href',
}, },
] ]
@ -288,42 +292,186 @@ editor.DomComponents.addType('link', {
}); });
``` ```
If built-in types are not enough (eg. something with more complex UI) you can define a new one. Now you'll see a simple text input because we have not yet defined our new trait type, so let's do it:
Let's see this simple `textarea` element which updates contents of the component.
```js
editor.TraitManager.addType('href-next', {
// Expects as return a simple HTML string or an HTML element
createInput({ trait }) {
// Here we can decide to use properties from the trait
const traitOpts = trait.get('options') || [];
const options = traitOpts.lenght ? traitOpts : [
{ id: 'url', name: 'URL' },
{ id: 'email', name: 'Email' },
];
// Create a new element container and add some content
const el = document.createElement('div');
el.innerHTML = `
<select class="href-next__type">
${options.map(opt => `<option value="${opt.id}">${opt.name}</option>`).join('')}
</select>
<div class="href-next__url-inputs">
<input class="href-next__url" placeholder="Insert URL"/>
</div>
<div class="href-next__email-inputs">
<input class="href-next__email" placeholder="Insert email"/>
<input class="href-next__email-subject" placeholder="Insert subject"/>
</div>
`;
// Let's make our content alive
const inputsUrl = el.querySelector('.href-next__url-inputs');
const inputsEmail = el.querySelector('.href-next__email-inputs');
const inputType = el.querySelector('.href-next__type');
inputType.addEventListener('change', ev => {
switch (ev.target.value) {
case 'url':
inputsUrl.style.display = '';
inputsEmail.style.display = 'none';
break;
case 'email':
inputsUrl.style.display = 'none';
inputsEmail.style.display = '';
break;
}
});
return el;
},
});
```
From the example above we simple created our custom inputs (by giving also the possibility to use `option` trait property) and defined some input switch behaviour on the type change. Now the result would be something like this
<img :src="$withBase('/docs-init-link-trait.jpg')">
### Update layout
Before going forward and making our trait work let's talk about the layout structure of a trait. You might have noticed that the trait is composed by the label and input columns, for this reason GrapesJS allows you to customize both of them.
For the label customization you might use `createLabel`
```js
editor.TraitManager.addType('href-next', {
// Expects as return a simple HTML string or an HTML element
createLabel({ label }) {
return `<div>
<div>Before</div>
${label}
<div>After</div>
</div>`;
},
// ...
});
```
You've probably seen already that in trait definition you can setup `label: false` to completely remove the label column, but in case you need to force this behaviour in all istances of this trait type you can use `noLabel` property
```js
editor.TraitManager.addType('href-next', {
noLabel: true,
// ...
});
```
You might also notice that by default GrapesJS applies kind of a wrapper around your inputs, generally is ok for simple inputs but probably is not what you need where you're creating a complex custom trait. To remove the default wrapper you can use the `templateInput` option
```js ```js
// Each new type extends the default Trait editor.TraitManager.addType('href-next', {
editor.TraitManager.addType('content', { // Completely remove the wrapper
events:{ templateInput: '',
'keyup': 'onChange', // trigger parent onChange method on keyup // Use a new one, by specifying with `data-input` attribute where to place the input container
templateInput: `<div class="custom-input-wrapper">
Before input
<div data-input></div>
After input
</div>`,
// It might also be a function, expects an HTML string as the result
templateInput({ trait }) {
return '<div ...';
}, },
});
```
<img :src="$withBase('/docs-link-trait-raw.jpg')">
In this case the result will be quite raw and unstyled but the point of custom trait types is to allow you to reuse your own styled inputs, probably already designed and defined (or impliemented in some UI framework).
For now let's keep the default input wrapper and continue with the integration of our custom trait.
### Bind to component
At the current state, our element created in `createInput` is not binded to the component so nothing happens when you update inputs, so let's do it now
/** ```js
* Returns the input element editor.TraitManager.addType('href-next', {
* @return {HTMLElement} // ...
*/
getInputEl: function() { // Update the component based element changes
if (!this.inputEl) { // `elInput` is the result HTMLElement you get from `createInput`
var input = document.createElement('textarea'); onUpdate({ elInput, component }) {
input.value = this.target.get('content'); const inputType = elInput.querySelector('.href-next__type');
this.inputEl = input; let href = '';
switch (inputType.value) {
case 'url':
const valUrl = elInput.querySelector('.href-next__url').value;
href = valUrl;
break;
case 'email':
const valEmail = elInput.querySelector('.href-next__email').value;
const valSubj = elInput.querySelector('.href-next__email-subject').value;
href = `mailto:${valEmail}${valSubj ? `?subject=${valSubj}` : ''}`;
break;
} }
return this.inputEl;
component.addAttributes({ href })
}, },
});
```
/** Now, most of the stuff should already work (you can update the trait and check the HTML in code preview). You might wonder how the editor captures the input change and if it's possible to change it.
* Triggered when the value of the model is changed By default, the base trait wrapper applies a listener on `change` event and calls `onUpdate` on any captured event (to be captured the event should be able to [bubble](https://stackoverflow.com/questions/4616694/what-is-event-bubbling-and-capturing)). If you want, for example, to update the component on `input` event you can change the `eventCapture` property
*/
onValueChange: function () { ```js
this.target.set('content', this.model.get('value')); editor.TraitManager.addType('href-next', {
} eventCapture: ['input'],
// ...
}); });
```
The last thing, you might have noticed the wrong inital render of our trait, which is not populate our inputs in case of already defined `href` attribute. This step should be done in `onRender` method
// And then use it in your component ```js
... editor.TraitManager.addType('href-next', {
traits: [{ // ...
type: 'content', onRender({ elInput, component }) {
}], const href = component.getAttributes()['href'] || '';
... const inputType = elInput.querySelector('.href-next__type');
let type = 'url';
if (href.indexOf('mailto:') === 0) {
const inputEmail = elInput.querySelector('.href-next__email');
const inputSubject = elInput.querySelector('.href-next__email-subject');
const mailTo = href.replace('mailto:', '').split('?');
const email = mailTo[0];
const params = (mailTo[1] || '').split('&').reduce((acc, item) => {
const items = item.split('=');
acc[items[0]] = items[1];
return acc;
}, {});
type = 'email';
inputEmail.value = email || '';
inputSubject.value = params['subject'] || '';
} else {
elInput.querySelector('.href-next__url').value = href;
}
inputType.value = type;
inputType.dispatchEvent(new CustomEvent('change'));
},
});
``` ```
<!-- <iframe width="100%" height="500" src="//jsfiddle.net/artur_arseniev/yf6amdqb/embedded/result/dark/" allowfullscreen="allowfullscreen" allowpaymentrequest frameborder="0"></iframe> -->

2
package.json

@ -5,7 +5,7 @@
"author": "Artur Arseniev", "author": "Artur Arseniev",
"license": "BSD-3-Clause", "license": "BSD-3-Clause",
"homepage": "http://grapesjs.com", "homepage": "http://grapesjs.com",
"main": "dist/grapes.js", "main": "dist/grapes.min.js",
"sideEffects": false, "sideEffects": false,
"repository": { "repository": {
"type": "git", "type": "git",

1
src/styles/scss/_gjs_traits.scss

@ -34,6 +34,7 @@
padding: 5px 10px; padding: 5px 10px;
font-weight: lighter; font-weight: lighter;
align-items: center; align-items: center;
text-align: left;
&s { &s {
font-size: $fontSizeS; font-size: $fontSizeS;

49
src/trait_manager/view/TraitView.js

@ -5,9 +5,8 @@ import { capitalize } from 'utils/mixins';
const $ = Backbone.$; const $ = Backbone.$;
export default Backbone.View.extend({ export default Backbone.View.extend({
events: { events: {},
change: 'onChange' eventCapture: ['change'],
},
appendInput: 1, appendInput: 1,
@ -28,7 +27,7 @@ export default Backbone.View.extend({
initialize(o = {}) { initialize(o = {}) {
const { config = {} } = o; const { config = {} } = o;
const { model } = this; const { model, eventCapture } = this;
const { target } = model; const { target } = model;
const { type } = model.attributes; const { type } = model.attributes;
this.config = config; this.config = config;
@ -47,9 +46,19 @@ export default Backbone.View.extend({
model.view = this; model.view = this;
this.listenTo(model, 'change:label', this.render); this.listenTo(model, 'change:label', this.render);
this.listenTo(model, 'change:placeholder', this.rerender); this.listenTo(model, 'change:placeholder', this.rerender);
eventCapture.forEach(event => (this.events[event] = 'onChange'));
this.delegateEvents();
this.init(); this.init();
}, },
getClbOpts() {
return {
component: this.target,
trait: this.model,
elInput: this.getInputElem()
};
},
removeView() { removeView() {
this.remove(); this.remove();
this.removed(); this.removed();
@ -58,16 +67,21 @@ export default Backbone.View.extend({
init() {}, init() {},
removed() {}, removed() {},
onRender() {}, onRender() {},
onUpdate() {},
/** /**
* Fires when the input is changed * Fires when the input is changed
* @private * @private
*/ */
onChange() { onChange(event) {
const el = this.getInputElem(); const el = this.getInputElem();
if (el && !isUndefined(el.value)) { if (el && !isUndefined(el.value)) {
this.model.set('value', el.value); this.model.set('value', el.value);
} }
this.onUpdate({
...this.getClbOpts(),
event
});
}, },
getValueForTarget() { getValueForTarget() {
@ -86,6 +100,7 @@ export default Backbone.View.extend({
onValueChange(model, value, opts = {}) { onValueChange(model, value, opts = {}) {
if (opts.fromTarget) { if (opts.fromTarget) {
this.setInputValue(model.get('value')); this.setInputValue(model.get('value'));
this.postRender();
} else { } else {
const val = this.getValueForTarget(); const val = this.getValueForTarget();
model.setTargetValue(val, opts); model.setTargetValue(val, opts);
@ -105,7 +120,8 @@ export default Backbone.View.extend({
tpl = tpl =
this.createLabel({ this.createLabel({
label, label,
component: target component: target,
trait: this
}) || ''; }) || '';
} }
@ -192,14 +208,15 @@ export default Backbone.View.extend({
* @private * @private
* */ * */
renderField() { renderField() {
const { $el, target, appendInput, model } = this; const { $el, appendInput, model } = this;
const inputOpts = { component: target };
const inputs = $el.find('[data-input]'); const inputs = $el.find('[data-input]');
const el = inputs[inputs.length - 1]; const el = inputs[inputs.length - 1];
let tpl = model.el; let tpl = model.el;
if (!tpl) { if (!tpl) {
tpl = this.createInput ? this.createInput(inputOpts) : this.getInputEl(); tpl = this.createInput
? this.createInput(this.getClbOpts())
: this.getInputEl();
} }
if (isString(tpl)) { if (isString(tpl)) {
@ -214,8 +231,8 @@ export default Backbone.View.extend({
}, },
hasLabel() { hasLabel() {
const { noLabel, label } = this.model.attributes; const { label } = this.model.attributes;
return !noLabel && label !== false; return !this.noLabel && label !== false;
}, },
rerender() { rerender() {
@ -223,8 +240,12 @@ export default Backbone.View.extend({
this.render(); this.render();
}, },
postRender() {
this.onRender(this.getClbOpts());
},
render() { render() {
const { $el, pfx, ppfx, model, target } = this; const { $el, pfx, ppfx, model } = this;
const { type } = model.attributes; const { type } = model.attributes;
const hasLabel = this.hasLabel && this.hasLabel(); const hasLabel = this.hasLabel && this.hasLabel();
const cls = `${pfx}trait`; const cls = `${pfx}trait`;
@ -235,7 +256,7 @@ export default Backbone.View.extend({
${ ${
this.templateInput this.templateInput
? isFunction(this.templateInput) ? isFunction(this.templateInput)
? this.templateInput() ? this.templateInput(this.getClbOpts())
: this.templateInput : this.templateInput
: '' : ''
} }
@ -245,7 +266,7 @@ export default Backbone.View.extend({
hasLabel && this.renderLabel(); hasLabel && this.renderLabel();
this.renderField(); this.renderField();
this.el.className = `${cls}__wrp`; this.el.className = `${cls}__wrp`;
this.onRender({ component: target }); this.postRender();
return this; return this;
} }
}); });

Loading…
Cancel
Save