Skip to content

Commit

Permalink
Update to support mobiledoc 0.3.2
Browse files Browse the repository at this point in the history
Updates to mobiledoc-kit 0.12.4
Adds mobiledoc-section-attribute-button for building toolbars.
Adds text-alignment buttons to mobiledoc-toolbar
Updates README accordingly
  • Loading branch information
lukemelia committed Jun 5, 2020
1 parent 59b5edb commit f256fc9
Show file tree
Hide file tree
Showing 15 changed files with 370 additions and 17 deletions.
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ Will render a blank Mobiledoc into the following DOM:
</article>
```

The components accepts these arguments:
The component accepts these arguments:

* `mobiledoc`, a Mobiledoc to be edited
* `cards`, an array of available cards for use by the editor. Jump to
Expand All @@ -66,6 +66,7 @@ The components accepts these arguments:
* `placeholder` string -- the placeholder text to display when the mobiledoc is blank
* `options` hash -- any properties in the `options` hash will be passed to the MobiledocKitEditor constructor
* `serializeVersion` string -- The mobiledoc version to serialize to when firing the on-change action. Default: 0.3.2
* `sectionAttributesConfig` hash -- information about supported section attributes. defaults to `{ 'text-align': { values: ['left', 'center', 'right'], defaultValue: 'left' } }`
* `on-change` -- Accepts an action that the component will send every time the mobiledoc is updated
* `will-create-editor` -- Accepts an action that will be sent when the instance of the MobiledocKitEditor is about to be created
This action may be fired more than once if the component's `mobiledoc` property is set to a new value.
Expand Down Expand Up @@ -140,6 +141,8 @@ Additionally `editor` provides the following actions:
* `addCardInEditMode`, passed a card name and payload will add that card at the end of
a post and render it in "edit" mode initially.
* `addAtom`, passed an atomName, text, and payload, will add that atom at the cursor position.
* `setAttribute`, passed an attribute name and attribute value, will add that attribute to the current section, or remove the attribute if the value is the default value.
* `removeAttribute`, passed an attribute name, will remove that attribute from the current section.

The `editor` object is often used indirectly by passing it to other
components. For example:
Expand Down Expand Up @@ -237,6 +240,33 @@ Custom text for the HTML of the button can be yielded:
When clicked, the presence of a link will be toggled. The user will be prompted
for a URL if required.

#### `{{mobiledoc-section-attribute-button}}`

Requires two properties:

* `attributeName`, the name of the attribute
* `attributeValue`, the value of the attribute
* `editor`, the `editor` instance from `mobiledoc-editor`

And accepts one optional property:

* `title`, added as the `title` attribute on the `button` element

Creates a `<button>` element that has a class of `active` when the provided
attributeValue is used in the current section. For example:

```hbs
{{mobiledoc-section-attribute-button editor=editor attributeName="text-align" attributeValue="center"}}
```

Alternatively, custom text for the HTML of the button can be yielded:

```hbs
{{#mobiledoc-section-attribute-button editor=editor attributeName="text-align" attributeValue="center"}}
Center
{{/mobiledoc-section-attribute-button}}
```

#### `{{mobiledoc-toolbar}}`

Requires one property:
Expand Down
69 changes: 62 additions & 7 deletions addon/components/mobiledoc-editor/component.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,13 @@ const EMPTY_MOBILEDOC = {
sections: []
};

export const DEFAULT_SECTION_ATTRIBUTES_CONFIG = {
'text-align': {
values: ['left', 'center', 'right'],
defaultValue: 'left'
}
}

function arrayToMap(array) {
let map = Object.create(null);
array.forEach(key => {
Expand Down Expand Up @@ -72,14 +79,28 @@ export default Component.extend({
mobiledoc = EMPTY_MOBILEDOC;
this.set('mobiledoc', mobiledoc);
}
let sectionAttributesConfig = this.get('sectionAttributesConfig');
if (!sectionAttributesConfig) {
sectionAttributesConfig = DEFAULT_SECTION_ATTRIBUTES_CONFIG;
this.set('sectionAttributesConfig', sectionAttributesConfig);
}
this.set('componentCards', A([]));
this.set('componentAtoms', A([]));
this.set('linkOffsets', null);
this.set('activeMarkupTagNames', {});
this.set('activeSectionTagNames', {});
this.set('activeSectionAttributes', {});
this._startedRunLoop = false;
},

isDefaultAttributeValue(attributeName, attributeValue) {
let defaultValue = this.sectionAttributesConfig[attributeName].defaultValue;
if (!defaultValue) {
throw new Error(`Default value is not configured for attribute '${attributeName}'`);
}
return attributeValue === defaultValue;
},

actions: {
toggleMarkup(markupTagName) {
let editor = this.get('editor');
Expand All @@ -91,6 +112,20 @@ export default Component.extend({
editor.toggleSection(sectionTagName);
},

setAttribute(attributeName, attributeValue) {
let editor = this.get('editor');
if (this.isDefaultAttributeValue(attributeName, attributeValue)) {
editor.removeAttribute(attributeName);
} else {
editor.setAttribute(attributeName, attributeValue);
}
},

removeAttribute(attributeName) {
let editor = this.get('editor');
editor.setAttribute(attributeName);
},

addCard(cardName, payload={}) {
this._addCard(cardName, payload);
},
Expand Down Expand Up @@ -126,6 +161,10 @@ export default Component.extend({

cancelLink() {
this.set('linkOffsets', null);
},

isDefaultAttributeValue() {
return this.isDefaultAttributeValue(...arguments);
}
},

Expand Down Expand Up @@ -302,23 +341,39 @@ export default Component.extend({
const markupTags = arrayToMap(editor.activeMarkups.map(m => m.tagName));
// editor.activeSections are leaf sections.
// Map parent section tag names (e.g. 'p', 'ul', 'ol') so that list buttons
// are updated.
// can be bound.
// Also build a map of section attributes for the same reason.
let sectionParentTagNames = editor.activeSections.map(s => {
return s.isNested ? s.parent.tagName : s.tagName;
});
const sectionTags = arrayToMap(sectionParentTagNames);
const sectionAttributes = {};
editor.activeSections.forEach(s => {
let attributes = s.isNested ? s.parent.attributes : s.attributes;
Object.keys(attributes || {}).forEach(attrName => {
let camelizedAttrName = camelize(attrName.replace(/^data-md/, ''));
let attrValue = attributes[attrName];
sectionAttributes[camelizedAttrName] = sectionAttributes[camelizedAttrName] || [];
if (!sectionAttributes[camelizedAttrName].includes(attrValue)) {
sectionAttributes[camelizedAttrName].push(attrValue);
}
});
});

let setEditorProps = () => {
this.setProperties({
activeMarkupTagNames: markupTags,
activeSectionTagNames: sectionTags,
activeSectionAttributes: sectionAttributes
});
}
// Avoid updating this component's properties synchronously while
// rendering the editor (after rendering the component) because it
// causes Ember to display deprecation warnings
if (this._isRenderingEditor) {
schedule('afterRender', () => {
this.set('activeMarkupTagNames', markupTags);
this.set('activeSectionTagNames', sectionTags);
});
schedule('afterRender', setEditorProps);
} else {
this.set('activeMarkupTagNames', markupTags);
this.set('activeSectionTagNames', sectionTags);
setEditorProps();
}
},

Expand Down
5 changes: 4 additions & 1 deletion addon/components/mobiledoc-editor/template.hbs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
{{yield (hash
editor=editor
activeSectionTagNames=activeSectionTagNames
activeMarkupTagNames=activeMarkupTagNames
activeSectionTagNames=activeSectionTagNames
activeSectionAttributes=activeSectionAttributes
toggleMarkup=(action 'toggleMarkup')
toggleLink=(action 'toggleLink')
addCard=(action 'addCard')
addAtom=(action 'addAtom')
addCardInEditMode=(action 'addCardInEditMode')
toggleSection=(action 'toggleSection')
setAttribute=(action 'setAttribute')
isDefaultAttributeValue=(action 'isDefaultAttributeValue')
)}}

<div class="mobiledoc-editor__editor-wrapper">
Expand Down
36 changes: 36 additions & 0 deletions addon/components/mobiledoc-section-attribute-button/component.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { defineProperty, observer, computed } from '@ember/object';
import Component from '@ember/component';
import layout from './template';
import { camelize } from '@ember/string';
import { isEmpty } from '@ember/utils';

export default Component.extend({
tagName: 'button',
layout,
attributeBindings: ['type', 'title'],
classNameBindings: ['isActive:active'],
type: 'button',
init() {
this._super(...arguments);
this._updateIsActiveCP();
},
onNameOrValueDidChange: observer('attributeName', 'attributeValue', function() {
this._updateIsActiveCP();
}),
_updateIsActiveCP() {
let attributeName = this.get('attributeName');
let fullPath = `editor.activeSectionAttributes.${camelize(attributeName)}`;
let cp = computed(fullPath, 'attributeValue', function(){
let activeValues = this.get(fullPath) || [];
let attributeValue = this.get('attributeValue');
return activeValues.includes(attributeValue) || (isEmpty(activeValues) && this.editor.isDefaultAttributeValue(attributeName, attributeValue));
});
defineProperty(this, 'isActive', cp);
},
click() {
let editor = this.get('editor');
let attributeName = this.get('attributeName');
let attributeValue = this.get('attributeValue');
editor.setAttribute(attributeName, attributeValue);
}
});
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{#if hasBlock}}{{yield}}{{else}}{{mobiledoc-titleize attributeValue}}{{/if~}}
27 changes: 27 additions & 0 deletions addon/components/mobiledoc-toolbar/template.hbs
Original file line number Diff line number Diff line change
Expand Up @@ -80,5 +80,32 @@
Ordered List
</button>
</li>
<li class="mobiledoc-toolbar__control">
<button
type="button"
title="Align Left"
class="mobiledoc-toolbar__button {{if (includes editor.activeSectionAttributes.textAlign 'left') 'active'}}"
{{action editor.setAttribute 'text-align' 'left'}}>
Align Left
</button>
</li>
<li class="mobiledoc-toolbar__control">
<button
type="button"
title="Align Center"
class="mobiledoc-toolbar__button {{if (includes editor.activeSectionAttributes.textAlign 'center') 'active'}}"
{{action editor.setAttribute 'text-align' 'center'}}>
Align Center
</button>
</li>
<li class="mobiledoc-toolbar__control">
<button
type="button"
title="Align Right"
class="mobiledoc-toolbar__button {{if (includes editor.activeSectionAttributes.textAlign 'right') 'active'}}"
{{action editor.setAttribute 'text-align' 'right'}}>
Align Right
</button>
</li>

{{yield}}
10 changes: 10 additions & 0 deletions addon/helpers/includes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { helper } from '@ember/component/helper';

export function includes([array, item] /* , attributeHash */) {
if (!array || !array.includes) {
return false;
}
return array.includes(item);
}

export default helper(includes);
1 change: 1 addition & 0 deletions app/components/mobiledoc-section-attribute-button.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default } from 'ember-mobiledoc-editor/components/mobiledoc-section-attribute-button/component';
1 change: 1 addition & 0 deletions app/helpers/includes.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default, includes } from 'ember-mobiledoc-editor/helpers/includes';
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
"ember-cli-htmlbars": "^2.0.3",
"ember-wormhole": "^0.5.1",
"mobiledoc-dom-renderer": "^0.7.0",
"mobiledoc-kit": "https://yapp-assets.s3.amazonaws.com/mobiledoc-kit-0c321e804ed3eaa196a44d4291746ff25233b13c.tgz"
"mobiledoc-kit": "~0.12.4"
},
"devDependencies": {
"bower": "^1.8.2",
Expand Down
15 changes: 14 additions & 1 deletion tests/helpers/create-mobiledoc.js
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,20 @@ export function simpleMobileDoc(text) {
};
}

export function alignCenterMobileDoc(text) {
return {
version: MOBILEDOC_VERSION,
markups: [],
atoms: [],
cards: [],
sections: [
[1, 'p', [
[0, [], 0, text]
], ["data-md-text-align", "center"]]
]
};
}

export function mobiledocWithList(text, listTagName='ol') {
return {
version: MOBILEDOC_VERSION,
Expand Down Expand Up @@ -89,4 +103,3 @@ export function linkMobileDoc(text) {
]
};
}

42 changes: 42 additions & 0 deletions tests/integration/components/mobiledoc-editor/component-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,48 @@ test('it exposes "toggleSection" which toggles the section type and fires `on-ch
});
});

test('it exposes "setAttribute" which can be used by section-attribute-button to update the section attribute value and fires `on-change`', function(assert) {
assert.expect(8);

let onChangeCount = 0;

let text = 'Howdy';
this.set('mobiledoc', simpleMobileDoc(text));
this.on('on-change', () => onChangeCount++);
this.render(hbs`
{{#mobiledoc-editor mobiledoc=mobiledoc on-change=(action 'on-change') as |editor|}}
{{mobiledoc-section-attribute-button editor=editor attributeName="text-align" attributeValue="left"}}
{{mobiledoc-section-attribute-button editor=editor attributeName="text-align" attributeValue="center"}}
{{/mobiledoc-editor}}
`);
const paragraphNode = this.$(`p:contains(${text})`)[0];
const textNode = paragraphNode.firstChild;

return selectRange(textNode, 0, textNode, text.length).then(() => {
assert.ok(!this.$(`p:contains(${text})`).attr('data-md-text-align'), 'precond - no attr');
assert.equal(onChangeCount, 0, 'precond - no on-change');
this.$('button:last').click();
return wait();
}).then(() => {
assert.equal(onChangeCount, 1, 'fires on-change');
assert.equal(this.$(`p:contains(${text})`).attr('data-md-text-align'), 'center', 'sets attribute');

onChangeCount = 0;
this.$('button:last').click();
return wait();
}).then(() => {
assert.equal(onChangeCount, 0, 'fires on-change again');
assert.equal(this.$(`p:contains(${text})`).attr('data-md-text-align'), 'center', 'clicking again does nothing');

onChangeCount = 0;
this.$('button:first').click();
return wait();
}).then(() => {
assert.equal(onChangeCount, 1, 'fires on-change again');
assert.equal(this.$(`p:contains(${text})`).attr('data-md-text-align'), undefined, 'clicking left removes attribute since it is the default value');
});
});

test('toolbar buttons can be active', function(assert) {
let text = 'abc';
this.set('mobiledoc', simpleMobileDoc(text));
Expand Down
Loading

0 comments on commit f256fc9

Please sign in to comment.