it('metaDescriptionScratch is one-way bound to post.metaDescription', function () { let component = this.subject({ post: EmberObject.extend({ metaDescription: 'a description', metaDescriptionScratch: boundOneWay('metaDescription') }).create() }); expect(component.get('post.metaDescription')).to.equal('a description'); expect(component.get('metaDescriptionScratch')).to.equal('a description'); run(function () { component.set('post.metaDescription', 'a different description'); expect(component.get('metaDescriptionScratch')).to.equal('a different description'); }); run(function () { component.set('metaDescriptionScratch', 'changed directly'); expect(component.get('post.metaDescription')).to.equal('a different description'); expect(component.get('metaDescriptionScratch')).to.equal('changed directly'); }); run(function () { // test that the one-way binding is still in place component.set('post.metaDescription', 'should update'); expect(component.get('metaDescriptionScratch')).to.equal('should update'); }); });
it('should be the metaDescription if one exists', function () { let component = this.subject({ post: EmberObject.extend({ metaDescription: 'a description', metaDescriptionScratch: boundOneWay('metaDescription') }).create() }); expect(component.get('seoDescription')).to.equal('a description'); });
it('should be the metaTitle if both title and metaTitle exist', function () { let component = this.subject({ post: EmberObject.extend({ titleScratch: 'a title', metaTitle: 'a meta-title', metaTitleScratch: boundOneWay('metaTitle') }).create() }); expect(component.get('seoTitle')).to.equal('a meta-title'); });
it('should revert to the title if explicit metaTitle is removed', function () { let component = this.subject({ post: EmberObject.extend({ titleScratch: 'a title', metaTitle: 'a meta-title', metaTitleScratch: boundOneWay('metaTitle') }).create() }); expect(component.get('seoTitle')).to.equal('a meta-title'); run(function () { component.set('post.metaTitle', ''); expect(component.get('seoTitle')).to.equal('a title'); }); });
it('should be generated from the rendered mobiledoc if not explicitly set', function () { let component = this.subject({ post: EmberObject.extend({ metaDescription: null, metaDescriptionScratch: boundOneWay('metaDescription'), author: RSVP.resolve(), init() { this._super(...arguments); this.scratch = { cards: [ ['markdown-card', { markdown: '# This is a <strong>test</strong> <script>foo</script>' }] ] }; } }).create() }); expect(component.get('seoDescription')).to.equal('This is a test'); });
import Component from 'ember-component'; import boundOneWay from 'ghost-admin/utils/bound-one-way'; import injectService from 'ember-service/inject'; import moment from 'moment'; import {InvokeActionMixin} from 'ember-invoke-action'; import {formatDate} from 'ghost-admin/utils/date-formatting'; export default Component.extend(InvokeActionMixin, { tagName: 'span', classNames: 'gh-input-icon gh-icon-calendar', datetime: boundOneWay('value'), inputClass: null, inputId: null, inputName: null, settings: injectService(), didReceiveAttrs() { let datetime = this.get('datetime') || moment.utc(); let blogTimezone = this.get('settings.activeTimezone'); if (!this.get('update')) { throw new Error(`You must provide an \`update\` action to \`{{${this.templateName}}}\`.`); } this.set('datetime', formatDate(datetime || moment.utc(), blogTimezone)); }, focusOut() { let datetime = this.get('datetime');
export default Controller.extend({ ghostPaths: service(), ajax: service(), notifications: service(), settings: service(), leaveSettingsTransition: null, slackArray: null, init() { this._super(...arguments); this.slackArray = []; }, slackSettings: boundOneWay('settings.slack.firstObject'), testNotificationDisabled: empty('slackSettings.url'), actions: { save() { this.save.perform(); }, updateURL(value) { value = typeof value === 'string' ? value.trim() : value; this.set('slackSettings.url', value); this.get('slackSettings.errors').clear(); }, updateUsername(value) { value = typeof value === 'string' ? value.trimLeft() : value;
post: null, selectedAuthor: null, _showSettingsMenu: false, _showThrobbers: false, customExcerptScratch: alias('post.customExcerptScratch'), codeinjectionFootScratch: alias('post.codeinjectionFootScratch'), codeinjectionHeadScratch: alias('post.codeinjectionHeadScratch'), metaDescriptionScratch: alias('post.metaDescriptionScratch'), metaTitleScratch: alias('post.metaTitleScratch'), ogDescriptionScratch: alias('post.ogDescriptionScratch'), ogTitleScratch: alias('post.ogTitleScratch'), twitterDescriptionScratch: alias('post.twitterDescriptionScratch'), twitterTitleScratch: alias('post.twitterTitleScratch'), slugValue: boundOneWay('post.slug'), facebookDescription: or('ogDescriptionScratch', 'customExcerptScratch', 'seoDescription'), facebookImage: or('post.ogImage', 'post.featureImage'), facebookTitle: or('ogTitleScratch', 'seoTitle'), seoTitle: or('metaTitleScratch', 'post.titleScratch'), twitterDescription: or('twitterDescriptionScratch', 'customExcerptScratch', 'seoDescription'), twitterImage: or('post.twitterImage', 'post.featureImage'), twitterTitle: or('twitterTitleScratch', 'seoTitle'), twitterImageStyle: computed('twitterImage', function () { let image = this.get('twitterImage'); return htmlSafe(`background-image: url(${image})`); }), facebookImageStyle: computed('facebookImage', function () {
/* global CodeMirror */ import Component from 'ember-component'; import run, {bind, scheduleOnce} from 'ember-runloop'; import injectService from 'ember-service/inject'; import boundOneWay from 'ghost-admin/utils/bound-one-way'; import {InvokeActionMixin} from 'ember-invoke-action'; const CmEditorComponent = Component.extend(InvokeActionMixin, { classNameBindings: ['isFocused:focused'], _value: boundOneWay('value'), // make sure a value exists isFocused: false, // options for the editor lineNumbers: true, indentUnit: 4, mode: 'htmlmixed', theme: 'xq-light', _editor: null, // reference to CodeMirror editor lazyLoader: injectService(), didInsertElement() { this._super(...arguments); this.get('lazyLoader').loadStyle('codemirror', 'codemirror/codemirror.css'); this.get('lazyLoader').loadScript('codemirror', 'codemirror/codemirror.js').then(() => { scheduleOnce('afterRender', this, function () {
}; timedSaveId = run.throttle(this, 'send', 'save', saveOptions, 60000, false); this._timedSaveId = timedSaveId; autoSaveId = run.debounce(this, 'send', 'save', saveOptions, 3000); this._autoSaveId = autoSaveId; } }), /** * By default, a post will not change its publish state. * Only with a user-set value (via setSaveType action) * can the post's status change. */ willPublish: boundOneWay('model.isPublished'), willSchedule: boundOneWay('model.isScheduled'), scheduledWillPublish: boundOneWay('model.isPublished'), // set by the editor route and `hasDirtyAttributes`. useful when checking // whether the number of tags has changed for `hasDirtyAttributes`. previousTagNames: null, tagNames: mapBy('model.tags', 'name'), postOrPage: computed('model.page', function () { return this.get('model.page') ? 'Page' : 'Post'; }), // countdown timer to show the time left until publish time for a scheduled post // starts 15 minutes before scheduled time
let deferred = {}; deferred.promise = this.store.query('user', {limit: 'all'}).then((users) => { return users.rejectBy('id', 'me').sortBy('name'); }).then((users) => { return users.filter((user) => { return user.get('active'); }); }); return ArrayProxy .extend(PromiseProxyMixin) .create(deferred); }), slugValue: boundOneWay('model.slug'), // Requests slug from title generateAndSetSlug(destination) { let title = this.get('model.titleScratch'); let afterSave = this.get('lastPromise'); let promise; // Only set an "untitled" slug once per post if (title === '(Untitled)' && this.get('model.slug')) { return; } promise = RSVP.resolve(afterSave).then(() => { return this.get('slugGenerator').generateSlug('post', title).then((slug) => { if (!isBlank(slug)) {
/* private properties ----------------------------------------------------*/ // set by setPost and _postSaved, used in hasDirtyAttributes _previousTagNames: null, /* computed properties ---------------------------------------------------*/ post: alias('model'), // used within {{gh-editor}} as a trigger for responsive css changes navIsClosed: reads('application.autoNav'), // store the desired post status locally without updating the model, // the model will only be updated when a save occurs willPublish: boundOneWay('post.isPublished'), willSchedule: boundOneWay('post.isScheduled'), // updateSlug and save should always be enqueued so that we don't run into // problems with concurrency, for example when Cmd-S is pressed whilst the // cursor is in the slug field - that would previously trigger a simultaneous // slug update and save resulting in ember data errors and inconsistent save // results saveTasks: taskGroup().enqueue(), _tagNames: mapBy('post.tags', 'name'), markdown: computed('post.mobiledoc', function () { if (this.get('post').isCompatibleWithMarkdownEditor()) { let mobiledoc = this.get('post.mobiledoc'); let markdown = mobiledoc.cards[0][1].markdown;