mirror of
https://github.com/TryGhost/Ghost.git
synced 2024-11-28 14:03:48 +03:00
🔢 Add wordcount feature. (#668)
closes TryGhost/Ghost#8202 - added wordcount for mobiledoc text and html/markdown cards (cards will only update word count when leaving edit state) - word count is only displayed on wide screens
This commit is contained in:
parent
c6ed77a0fc
commit
e604b255af
@ -42,6 +42,7 @@ export default Mixin.create({
|
|||||||
clock: injectService(),
|
clock: injectService(),
|
||||||
slugGenerator: injectService(),
|
slugGenerator: injectService(),
|
||||||
|
|
||||||
|
wordcount: 0,
|
||||||
cards: [], // for apps
|
cards: [], // for apps
|
||||||
atoms: [], // for apps
|
atoms: [], // for apps
|
||||||
toolbar: [], // for apps
|
toolbar: [], // for apps
|
||||||
@ -580,6 +581,10 @@ export default Mixin.create({
|
|||||||
|
|
||||||
editorMenuIsClosed() {
|
editorMenuIsClosed() {
|
||||||
this.set('editorMenuIsOpen', false);
|
this.set('editorMenuIsOpen', false);
|
||||||
|
},
|
||||||
|
|
||||||
|
wordcountDidChange(wordcount) {
|
||||||
|
this.set('wordcount', wordcount);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -251,7 +251,18 @@
|
|||||||
line-height: 0;
|
line-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.gh-editor-header-small .post-settings {
|
.gh-editor-header-small .post-settings {
|
||||||
padding: 13px 15px;
|
padding: 13px 15px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.gh-editor-wordcount {
|
||||||
|
position: fixed;
|
||||||
|
bottom: 0px;
|
||||||
|
padding:10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1200px) {
|
||||||
|
.gh-editor-wordcount {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
}
|
@ -50,9 +50,11 @@
|
|||||||
setEditor=(action "setEditor")
|
setEditor=(action "setEditor")
|
||||||
menuIsOpen=(action "editorMenuIsOpen")
|
menuIsOpen=(action "editorMenuIsOpen")
|
||||||
menuIsClosed=(action "editorMenuIsClosed")
|
menuIsClosed=(action "editorMenuIsClosed")
|
||||||
|
wordcountDidChange=(action "wordcountDidChange")
|
||||||
}}
|
}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="gh-editor-wordcount">{{pluralize wordcount 'word'}}.</div>
|
||||||
{{/gh-editor}}
|
{{/gh-editor}}
|
||||||
|
|
||||||
{{#if showDeletePostModal}}
|
{{#if showDeletePostModal}}
|
||||||
|
@ -3,13 +3,16 @@ import layout from '../../templates/components/card-html';
|
|||||||
import computed from 'ember-computed';
|
import computed from 'ember-computed';
|
||||||
import observer from 'ember-metal/observer';
|
import observer from 'ember-metal/observer';
|
||||||
import {invokeAction} from 'ember-invoke-action';
|
import {invokeAction} from 'ember-invoke-action';
|
||||||
|
import counter from 'ghost-admin/utils/word-count';
|
||||||
|
|
||||||
export default Component.extend({
|
export default Component.extend({
|
||||||
layout,
|
layout,
|
||||||
hasRendered: false,
|
hasRendered: false,
|
||||||
|
|
||||||
save: observer('doSave', function () {
|
save: observer('doSave', function () {
|
||||||
this.get('env').save(this.get('payload'), false);
|
let payload = this.get('payload');
|
||||||
|
payload.wordcount = counter(payload.html);
|
||||||
|
this.get('env').save(payload, false);
|
||||||
}),
|
}),
|
||||||
|
|
||||||
value: computed('payload', {
|
value: computed('payload', {
|
||||||
|
@ -8,6 +8,7 @@ import {isBlank} from 'ember-utils';
|
|||||||
import computed from 'ember-computed';
|
import computed from 'ember-computed';
|
||||||
import observer from 'ember-metal/observer';
|
import observer from 'ember-metal/observer';
|
||||||
import run from 'ember-runloop';
|
import run from 'ember-runloop';
|
||||||
|
import counter from 'ghost-admin/utils/word-count';
|
||||||
import {
|
import {
|
||||||
isRequestEntityTooLargeError,
|
isRequestEntityTooLargeError,
|
||||||
isUnsupportedMediaTypeError,
|
isUnsupportedMediaTypeError,
|
||||||
@ -29,6 +30,7 @@ export default Component.extend({
|
|||||||
save: observer('doSave', function () {
|
save: observer('doSave', function () {
|
||||||
let payload = this.get('payload');
|
let payload = this.get('payload');
|
||||||
payload.markdown = this.$('textarea').val();
|
payload.markdown = this.$('textarea').val();
|
||||||
|
payload.wordcount = counter(payload.markdown);
|
||||||
this.set('value', this.$('textarea').val());
|
this.set('value', this.$('textarea').val());
|
||||||
this.set('payload', payload);
|
this.set('payload', payload);
|
||||||
this.get('env').save(payload, false);
|
this.get('env').save(payload, false);
|
||||||
|
@ -8,8 +8,8 @@ import createCardFactory from '../lib/card-factory';
|
|||||||
import defaultCommands from '../options/default-commands';
|
import defaultCommands from '../options/default-commands';
|
||||||
import editorCards from '../cards/index';
|
import editorCards from '../cards/index';
|
||||||
import {getCardFromDoc, checkIfClickEventShouldCloseCard, getPositionOnScreenFromRange} from '../lib/utils';
|
import {getCardFromDoc, checkIfClickEventShouldCloseCard, getPositionOnScreenFromRange} from '../lib/utils';
|
||||||
|
import counter from 'ghost-admin/utils/word-count';
|
||||||
import $ from 'jquery';
|
import $ from 'jquery';
|
||||||
// import { VALID_MARKUP_SECTION_TAGNAMES } from 'mobiledoc-kit/models/markup-section'; //the block elements supported by mobile-doc
|
|
||||||
|
|
||||||
export const BLANK_DOC = {
|
export const BLANK_DOC = {
|
||||||
version: MOBILEDOC_VERSION,
|
version: MOBILEDOC_VERSION,
|
||||||
@ -103,6 +103,7 @@ export default Component.extend({
|
|||||||
this._firstChange = true;
|
this._firstChange = true;
|
||||||
this.sendAction('onFirstChange', this._cachedDoc);
|
this.sendAction('onFirstChange', this._cachedDoc);
|
||||||
}
|
}
|
||||||
|
this.processWordcount();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@ -151,6 +152,7 @@ export default Component.extend({
|
|||||||
}
|
}
|
||||||
|
|
||||||
editor.cursorDidChange(() => this.cursorMoved());
|
editor.cursorDidChange(() => this.cursorMoved());
|
||||||
|
this.processWordcount();
|
||||||
},
|
},
|
||||||
|
|
||||||
// makes sure the cursor is on screen except when selection is happening in which case the browser mostly ensures it.
|
// makes sure the cursor is on screen except when selection is happening in which case the browser mostly ensures it.
|
||||||
@ -193,7 +195,21 @@ export default Component.extend({
|
|||||||
this.send('deselectCard');
|
this.send('deselectCard');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// Note: This wordcount function doesn't count words that have been entered in cards.
|
||||||
|
// We should either allow cards to report their own wordcount or use the DOM (innerText) to calculate the wordcount.
|
||||||
|
processWordcount() {
|
||||||
|
let wordcount = 0;
|
||||||
|
if (this.editor.post.sections.length) {
|
||||||
|
this.editor.post.sections.forEach((section) => {
|
||||||
|
if (section.isMarkerable && section.text.length) {
|
||||||
|
wordcount += counter(section.text);
|
||||||
|
} else if (section.isCardSection && section.payload.wordcount) {
|
||||||
|
wordcount += Number(section.payload.wordcount);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
this.sendAction('wordcountDidChange', wordcount);
|
||||||
|
},
|
||||||
willDestroy() {
|
willDestroy() {
|
||||||
this.editor.destroy();
|
this.editor.destroy();
|
||||||
this.send('deselectCard');
|
this.send('deselectCard');
|
||||||
|
@ -17,7 +17,7 @@
|
|||||||
{{/ember-wormhole}}
|
{{/ember-wormhole}}
|
||||||
{{/each}}
|
{{/each}}
|
||||||
<div class='gh-koenig'>
|
<div class='gh-koenig'>
|
||||||
<div class='surface needsclick' tabindex="{{tabindex}}" ondrop={{action "dropImage"}} ondragover={{action "dragOver"}} />
|
<div class='surface' tabindex="{{tabindex}}" ondrop={{action "dropImage"}} ondragover={{action "dragOver"}} />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{{yield}}
|
{{yield}}
|
||||||
|
56
ghost/admin/tests/integration/components/gh-koenig-test.js
Normal file
56
ghost/admin/tests/integration/components/gh-koenig-test.js
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
/* jshint expr:true */
|
||||||
|
import {describe, it} from 'mocha';
|
||||||
|
import {setupComponentTest} from 'ember-mocha';
|
||||||
|
import hbs from 'htmlbars-inline-precompile';
|
||||||
|
import {editorRendered, testInput} from '../../helpers/editor-helpers';
|
||||||
|
import sinon from 'sinon';
|
||||||
|
|
||||||
|
describe.skip('Integration: Component: gh-koenig - General Editor Tests.', function () {
|
||||||
|
setupComponentTest('gh-koenig', {
|
||||||
|
integration: true
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(function () {
|
||||||
|
// set defaults
|
||||||
|
this.set('onFirstChange', sinon.spy());
|
||||||
|
this.set('onChange', sinon.spy());
|
||||||
|
|
||||||
|
this.set('wordcount', 0);
|
||||||
|
this.set('actions.wordcountDidChange', function (wordcount) {
|
||||||
|
this.set('wordcount', wordcount);
|
||||||
|
});
|
||||||
|
|
||||||
|
this.set('value', {
|
||||||
|
version: '0.3.1',
|
||||||
|
atoms: [],
|
||||||
|
markups: [],
|
||||||
|
cards: [],
|
||||||
|
sections: []});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('Check that events have fired', function (done) {
|
||||||
|
this.render(hbs`{{gh-koenig
|
||||||
|
apiRoot='/todo'
|
||||||
|
assetPath='/assets'
|
||||||
|
containerSelector='.editor-holder'
|
||||||
|
value=value
|
||||||
|
onChange=(action onChange)
|
||||||
|
onFirstChange=(action onFirstChange)
|
||||||
|
wordcountDidChange=(action 'wordcountDidChange')
|
||||||
|
}}`);
|
||||||
|
|
||||||
|
editorRendered()
|
||||||
|
.then(() => {
|
||||||
|
let {editor} = window;
|
||||||
|
editor.element.focus();
|
||||||
|
return testInput('abcd efg hijk lmnop', '<p>abcd efg hijk lmnop</p>', expect);
|
||||||
|
})
|
||||||
|
.then(() => {
|
||||||
|
expect(this.get('onFirstChange').calledOnce).to.be.true;
|
||||||
|
expect(this.get('onChange').calledOnce).to.be.true;
|
||||||
|
expect(this.get('wordcount')).to.equal(4);
|
||||||
|
done();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user