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');
        });
示例#6
0
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');
示例#7
0
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 () {
示例#9
0
/* 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)) {
示例#12
0
    /* 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;