<commit_before>//   (c) 2007-2013 by Kajona, www.kajona.de
//       Published under the GNU LGPL v2.1, see /system/licence_lgpl.txt
//       $Id$

$(function () {

    $.widget('custom.catcomplete', $.ui.autocomplete, {
        _renderMenu: function(ul, items) {
            var self = this;
            var currentCategory = '';

            $.each(items, function(index, item) {
                if (item.module != currentCategory) {
                    ul.append('

' + item.module + '

');
                    currentCategory = item.module;
                }
                self._renderItemData(ul, item);
            });

            ul.append('
View detailed search results
');
            ul.addClass('dropdown-menu');
            ul.addClass('search-dropdown-menu');

            ul.find('.detailedResults a').click(function () {
                $('.navbar-search').submit();
            });
        },
        _renderItemData: function (ul, item) {
            return $('

')
                .data('ui-autocomplete-item', item)
                .append('' + item.icon + item.description + '')
                .appendTo(ul);
        }
    });

    $('#globalSearchInput').catcomplete({
        //source: '_skinwebpath_/search.json',
        source: function(request, response) {
            $.ajax({
                url: KAJONA_WEBPATH+'/xml.php?admin=1',
                type: 'POST',
                dataType: 'json',
                data: {
                    search_query: request.term,
                    module: 'search',
                    action: 'searchXml',
                    asJson: '1'
                },
                success: response
            });
        },
        select: function (event, ui) {
            if(ui.item) {
                document.location = ui.item.link;
            }
        },
        messages: {
            noResults: '',
            results: function() {}
        },
        search: function(event, ui) {
            $(this).css("background-image", "url("+KAJONA_WEBPATH+"/core/module_v4skin/admin/skins/kajona_v4/img/loading-small.gif)").css("background-repeat", "no-repeat").
            css("background-position", "right center");
        },
        response: function(event, ui) {
            $(this).css("background-image", "none");
        }
    });

    // init popovers & tooltips
    $('#content a[rel=popover]').popover();
    KAJONA.admin.tooltip.initTooltip();

    KAJONA.admin.statusDisplay.classOfMessageBox = "alert alert-info";
    KAJONA.admin.statusDisplay.classOfErrorBox = "alert alert-error";

    KAJONA.admin.scroll = null;
    $(window).scroll(function() {
        var scroll = $(this).scrollTop();
        if(scroll > 10 && KAJONA.admin.scroll != 'top') {
            $("ul.breadcrumb").addClass("breadcrumbTop");
            KAJONA.admin.scroll = "top";
        }
        else if(scroll <= 10 && KAJONA.admin.scroll != 'margin') {
            $("ul.breadcrumb").removeClass("breadcrumbTop");
            KAJONA.admin.scroll = "fixed";
        }

    });

    KAJONA.v4skin.breadcrumb.updatePathNavigationEllipsis();
    $(window).on("resize", function() {
        KAJONA.v4skin.breadcrumb.updatePathNavigationEllipsis();

    })

});

if (typeof KAJONA == "undefined") {
    alert('load kajona.js before!');
}

KAJONA.v4skin = {
    appendLinkToPathNavigation : function(strLinkContent) {
        var link = $("

").append(strLinkContent);
        $("div.pathNaviContainer  ul.breadcrumb").append(link);
    },

    defaultAutoComplete : function() {

        this.minLength = 2;

        this.delay = KAJONA.util.isTouchDevice() ? 2000 : 0;

        this.messages = {
            noResults: '',
            results: function() {return ''}
        };

        this.search = function(event, ui) {
            $(this).css('background-image', 'url('+KAJONA_WEBPATH+'/core/module_v4skin/admin/skins/kajona_v4/img/loading-small.gif)');
        };

        this.response = function(event, ui) {
            $(this).css('background-image', 'none');
        };

        this.focus = function() {
            return false;
        };

        this.select = function( event, ui ) {
            if(ui.item) {
                $objCur = $(this);
                $objCur.val( ui.item.title );
                if($('#'+$objCur.attr('id')+'_id'))
                    $( '#'+$objCur.attr('id')+'_id' ).val( ui.item.systemid );
            }
            return false;
        };

        this.create = function( event, ui ) {
            $(this).css('background-image', 'url('+KAJONA_WEBPATH+'/core/module_v4skin/admin/skins/kajona_v4/img/loading-small-still.gif)').css('background-repeat', 'no-repeat').css('background-position', 'right center');
        }
    }
};

KAJONA.v4skin.breadcrumb = {
    updatePathNavigationEllipsis : function() {

        var $arrPathLIs = $(".pathNaviContainer  .breadcrumb  li.pathentry");
        var $objBreadcrumb = $(".pathNaviContainer  .breadcrumb");

        //first run: get the number of entries and a first styling
        var intEntries = ($arrPathLIs.length);
        var intWidth = $objBreadcrumb.width();
        var intMaxWidth = Math.ceil(intWidth/intEntries);

        $arrPathLIs.css("max-width", intMaxWidth);

        //second run: calc the remaining x-space
        var intTotalUnused = KAJONA.v4skin.breadcrumb.getUnusedSpace(intMaxWidth);

        if(intTotalUnused > intMaxWidth) {
            intMaxWidth = Math.ceil(intWidth/ (intEntries - (Math.floor(intTotalUnused / intMaxWidth)) ));
            $arrPathLIs.css("max-width", intMaxWidth);
        }

    },

    getUnusedSpace : function(intMaxWidth) {
        var intTotalUnused = 0;
        $(".pathNaviContainer  .breadcrumb  li.pathentry").each(function() {
            var $li = $(this);
            if($li.width() < intMaxWidth) {
                intTotalUnused += (intMaxWidth - $li.width());
            }
        });

        return intTotalUnused;
    }

};

<commit_msg>NEW: moved appendLinkToPathNavigation() from KAJONA.v4skin to KAJONA.breadcrumb.v4skin<commit_after>//   (c) 2007-2013 by Kajona, www.kajona.de
//       Published under the GNU LGPL v2.1, see /system/licence_lgpl.txt
//       $Id$

$(function () {

    $.widget('custom.catcomplete', $.ui.autocomplete, {
        _renderMenu: function(ul, items) {
            var self = this;
            var currentCategory = '';

            $.each(items, function(index, item) {
                if (item.module != currentCategory) {
                    ul.append('

' + item.module + '

');
                    currentCategory = item.module;
                }
                self._renderItemData(ul, item);
            });

            ul.append('
View detailed search results
');
            ul.addClass('dropdown-menu');
            ul.addClass('search-dropdown-menu');

            ul.find('.detailedResults a').click(function () {
                $('.navbar-search').submit();
            });
        },
        _renderItemData: function (ul, item) {
            return $('

')
                .data('ui-autocomplete-item', item)
                .append('' + item.icon + item.description + '')
                .appendTo(ul);
        }
    });

    $('#globalSearchInput').catcomplete({
        //source: '_skinwebpath_/search.json',
        source: function(request, response) {
            $.ajax({
                url: KAJONA_WEBPATH+'/xml.php?admin=1',
                type: 'POST',
                dataType: 'json',
                data: {
                    search_query: request.term,
                    module: 'search',
                    action: 'searchXml',
                    asJson: '1'
                },
                success: response
            });
        },
        select: function (event, ui) {
            if(ui.item) {
                document.location = ui.item.link;
            }
        },
        messages: {
            noResults: '',
            results: function() {}
        },
        search: function(event, ui) {
            $(this).css("background-image", "url("+KAJONA_WEBPATH+"/core/module_v4skin/admin/skins/kajona_v4/img/loading-small.gif)").css("background-repeat", "no-repeat").
            css("background-position", "right center");
        },
        response: function(event, ui) {
            $(this).css("background-image", "none");
        }
    });

    // init popovers & tooltips
    $('#content a[rel=popover]').popover();
    KAJONA.admin.tooltip.initTooltip();

    KAJONA.admin.statusDisplay.classOfMessageBox = "alert alert-info";
    KAJONA.admin.statusDisplay.classOfErrorBox = "alert alert-error";

    KAJONA.admin.scroll = null;
    $(window).scroll(function() {
        var scroll = $(this).scrollTop();
        if(scroll > 10 && KAJONA.admin.scroll != 'top') {
            $("ul.breadcrumb").addClass("breadcrumbTop");
            KAJONA.admin.scroll = "top";
        }
        else if(scroll <= 10 && KAJONA.admin.scroll != 'margin') {
            $("ul.breadcrumb").removeClass("breadcrumbTop");
            KAJONA.admin.scroll = "fixed";
        }

    });

    KAJONA.v4skin.breadcrumb.updatePathNavigationEllipsis();
    $(window).on("resize", function() {
        KAJONA.v4skin.breadcrumb.updatePathNavigationEllipsis();

    })

});

if (typeof KAJONA == "undefined") {
    alert('load kajona.js before!');
}

KAJONA.v4skin = {

    defaultAutoComplete : function() {

        this.minLength = 2;

        this.delay = KAJONA.util.isTouchDevice() ? 2000 : 0;

        this.messages = {
            noResults: '',
            results: function() {return ''}
        };

        this.search = function(event, ui) {
            $(this).css('background-image', 'url('+KAJONA_WEBPATH+'/core/module_v4skin/admin/skins/kajona_v4/img/loading-small.gif)');
        };

        this.response = function(event, ui) {
            $(this).css('background-image', 'none');
        };

        this.focus = function() {
            return false;
        };

        this.select = function( event, ui ) {
            if(ui.item) {
                $objCur = $(this);
                $objCur.val( ui.item.title );
                if($('#'+$objCur.attr('id')+'_id'))
                    $( '#'+$objCur.attr('id')+'_id' ).val( ui.item.systemid );
            }
            return false;
        };

        this.create = function( event, ui ) {
            $(this).css('background-image', 'url('+KAJONA_WEBPATH+'/core/module_v4skin/admin/skins/kajona_v4/img/loading-small-still.gif)').css('background-repeat', 'no-repeat').css('background-position', 'right center');
        }
    }
};

KAJONA.v4skin.breadcrumb = {
    updatePathNavigationEllipsis : function() {

        var $arrPathLIs = $(".pathNaviContainer  .breadcrumb  li.pathentry");
        var $objBreadcrumb = $(".pathNaviContainer  .breadcrumb");

        //first run: get the number of entries and a first styling
        var intEntries = ($arrPathLIs.length);
        var intWidth = $objBreadcrumb.width();
        var intMaxWidth = Math.ceil(intWidth/intEntries);

        $arrPathLIs.css("max-width", intMaxWidth);

        //second run: calc the remaining x-space
        var intTotalUnused = KAJONA.v4skin.breadcrumb.getUnusedSpace(intMaxWidth);

        if(intTotalUnused > intMaxWidth) {
            intMaxWidth = Math.ceil(intWidth/ (intEntries - (Math.floor(intTotalUnused / intMaxWidth)) ));
            $arrPathLIs.css("max-width", intMaxWidth);
        }

    },

    getUnusedSpace : function(intMaxWidth) {
        var intTotalUnused = 0;
        $(".pathNaviContainer  .breadcrumb  li.pathentry").each(function() {
            var $li = $(this);
            if($li.width() < intMaxWidth) {
                intTotalUnused += (intMaxWidth - $li.width());
            }
        });

        return intTotalUnused;
    },

    appendLinkToPathNavigation : function(strLinkContent) {
        var link = $("

").append(strLinkContent+" ");
        $("div.pathNaviContainer  ul.breadcrumb").append(link);
        KAJONA.v4skin.breadcrumb.updatePathNavigationEllipsis();
    }

};

<|endoftext|>


<commit_before>import Post from 'discourse/models/post'
import Topic from 'discourse/models/topic'
import User from 'discourse/models/user'
import elementIsVisible from '../lib/element-is-visible'
import lastVisibleElement from '../lib/last-visible-element'
import debounce from 'discourse/lib/debounce'
import { ajax } from 'discourse/lib/ajax'
import { scrollToPost, setupScrollContainer, playNotification, setupComposer, teardownComposer, hasChatElements } from '../lib/chat-element-utils'
import { syncWithPostStream, latestPostFor, latestPostIsMine, teardownPresence, setupLastReadMarker, applyPostStream } from '../lib/chat-topic-utils'
import { forEachTopicContainer } from '../lib/chat-topic-iterators'
import { rerender } from '../lib/chat-component-utils'
import { setupLiveUpdate, teardownLiveUpdate, messageBus } from '../lib/chat-live-update-utils'
import BabbleRegistry from '../lib/babble-registry'
import showModal from 'discourse/lib/show-modal'

export default Ember.Object.create({
  summary: {},

  disabled() {
    return !User.current() ||
           User.currentProp('custom_fields.babble_disabled') ||
           !Discourse.SiteSettings.babble_enabled
  },

  bindById(component, topicId) {
    return this.loadTopic(topicId).then((topic) => {
      return this.bind(component, topic)
    }, console.log)
  },

  bind(component, topic, postNumber) {
    if (!topic) { return }

    postNumber = postNumber || topic.last_read_post_number

    this.unbind(component)
    topic = BabbleRegistry.bind(component, topic)
    setupLastReadMarker(topic)

    Ember.run.scheduleOnce('afterRender', () => {
      setupLiveUpdate(topic, {
        '':       ((data) => { this.buildTopic(data) }),
        'typing': ((data) => { this.handleTyping(topic, data) }),
        'online': ((data) => { this.handleOnline(topic, data) })
      })

      if (hasChatElements(component.element)) {
        setupScrollContainer(topic)
        setupComposer(topic)
        scrollToPost(topic, postNumber, 0)
      }
    })

    rerender(topic)
    return topic
  },

  unbind(component) {
    let topic = BabbleRegistry.topicForComponent(component)
    if (!topic) { return }

    teardownLiveUpdate(topic, '', 'typing', 'online')

    if (hasChatElements(component.element)) {
      teardownPresence(topic)
    }

    BabbleRegistry.unbind(component)
  },

  loadTopic(id, opts = {}) {
    this.set('loadingTopicId', id)
    let path  = opts.pm ? '/pm' : ''
    let query = opts.postNumber ? `?near_post=${opts.postNumber}` : ''
    return ajax(`/babble/topics/${path}/${id}.json${query}`).then((data) => {
      return this.buildTopic(data)
    }).finally(() => {
      this.set('loadingTopicId', null)
    })
  },

  subscribeToNotifications(component) {
    if (!User.current()) { return }
    messageBus().subscribe(`/babble/notifications/${User.current().id}`, (data) => {
      this.handleNewPost(data.post)
      BabbleRegistry.storeNotification(data.notification)
      if (!component.initialized) {
        this.set('summary.notificationCount', this.summary.notificationCount + 1)
      }
      playNotification()
      component.appEvents.trigger('babble-rerender')
    })
  },

  loadBoot(component) {
    this.set('loadingBoot', true)
    return ajax(`/babble/boot.json`).then((data) => {
      _.each(data.notifications, (n) => { BabbleRegistry.storeNotification(n) })
      _.each(data.users,         (u) => { BabbleRegistry.storeUser(User.create(u)) })
      _.each(data.topics,        (t) => { this.setupTopicListener(t, component) })
    }).finally(() => {
      component.appEvents.trigger('babble-rerender')
      this.set('booted', true)
      this.set('loadingBoot', false)
    })
  },

  loadSummary(component) {
    this.set('loadingSummary', true)
    return ajax(`/babble/summary.json`).then((data) => {
      this.set('summary.topicCount', data.topic_count)
      this.set('summary.unreadCount', data.unread_count)
      this.set('summary.notificationCount', data.notification_count)
      this.set('summary.defaultId', data.default_id)

      if (this.summary.topicCount > 0) {
        component.appEvents.trigger('babble-has-topics')
      }
    }).finally(() => {
      component.appEvents.trigger('babble-rerender')
      this.set('loadingSummary', null)
    })
  },

  setupTopicListener(t, component) {
    setupLiveUpdate(this.buildTopic(t), {
      'posts': (data) => {
        this.handleNewPost(data)
        component.appEvents.trigger('babble-rerender')
      }
    })
  },

  unreadCount() {
    var unreadCount, notificationCount
    if (this.booted) {
      unreadCount       = this.availableTopics().reduce((total, topic) => { return total + topic.unreadCount }, 0)
      notificationCount = this.availableNotifications().length
    } else {
      unreadCount       = this.summary.unreadCount || 0
      notificationCount = this.summary.notificationCount || 0
    }
    if (unreadCount + notificationCount == 0) { return }
    return (notificationCount || '•').toString()
  },

  singleChannel() {
    return this.summary.topicCount == 1 && !Discourse.SiteSettings.babble_enable_pms
  },

  openByDefault() {
    return User.currentProp('custom_fields.babble_open_by_default')
  },

  availableTopics() {
    return BabbleRegistry.allTopics()
  },

  availableUsers() {
    return BabbleRegistry.allUsers()
  },

  availableNotifications() {
    return BabbleRegistry.allNotifications()
  },

  notificationsFor(item) {
    return this.availableNotifications().filter((n) => {
      if (item.constructor == User) {
        return n.sender_id == item.id
      } else {
        return n.topic_id == item.id
      }
    })
  },

  fetchDefault() {
    return this.fetchTopic(this.summary.defaultId)
  },

  fetchTopic(topicId) {
    return BabbleRegistry.fetchTopic(topicId)
  },

  topicForComponent(component) {
    return BabbleRegistry.topicForComponent(component)
  },

  buildTopic(data) {
    if (!data.id) { return }
    let topic = Topic.create(data)
    applyPostStream(topic)
    syncWithPostStream(topic)
    return BabbleRegistry.storeTopic(topic)
  },

  createPost(topic, text) {
    this.stagePost(topic, text)
    return ajax(`/babble/topics/${topic.id}/posts`, {
      type: 'POST',
      data: { raw: text }
    }).then((data) => {
      this.handleNewPost(data)
    })
  },

  readPost(topic, postNumber) {
    this.notificationsFor(topic).map((n) => {
      if (n.post_number <= postNumber) { BabbleRegistry.removeNotification(n.id) }
    })
    return ajax(`/babble/topics/${topic.id}/read/${postNumber}.json`)
  },

  editPost(topic, post) {
    if (post) {
      topic.set('editingPostId', post.id)
      scrollToPost(topic, post.post_number)
      setupComposer(topic)
    } else {
      topic.set('editingPostId', null)
    }
  },

  flagPost(topic, post) {
    if (post.get('actions_summary')) {
      showModal('flag', { model: post, babble: true })
    } else {
      // we have to get some more info from the API before we can properly display the flag modal
      ajax(`/posts/${post.id}`).then((data) => {
        data = Post.munge(data)
        post.set('actionByName', data.actionByName)
        post.set('actions_summary', data.actions_summary)
        showModal('flag', { model: post, babble: true })
      })
    }
  },

  updatePost(topic, post, text) {
    this.editPost(topic, null)
    topic.set('loadingEditId', post.id)
    return ajax(`/babble/topics/${post.topic_id}/posts/${post.id}`, {
      type: 'POST',
      data: { raw: text }
    }).then((data) => {
      this.handleNewPost(data)
    }).finally(() => {
      topic.set('loadingEditId', null)
    })
  },

  destroyPost(topic, post) {
    topic.set('loadingEditId', post.id)
    return ajax(`/babble/topics/${post.topic_id}/posts/${post.id}`, {
      type: 'DELETE'
    }).finally(() => {
      topic.set('loadingEditId', null)
    })
  },

  populatePermissions(data) {
    const user = User.current()

    if (!user || data.user_id != user.id) {
      delete data.can_edit
      delete data.can_flag
      delete data.can_delete
    }

    if(!_.contains(_.keys(data), 'can_edit')) {
      data.can_edit = user.staff ||
                      data.user_id == user.id ||
                      user.trust_level >= 4
    }
    if(!_.contains(_.keys(data), 'can_flag')) {
      data.can_flag = !data.user_id != user.id &&
                      (user.staff || user.trust_level >= 1)
    }
    if(!_.contains(_.keys(data), 'can_delete')) {
      data.can_delete = user.staff || data.user_id == user.id
    }

    return data
  },

  handleNewPost(data) {
    let topic = BabbleRegistry.fetchTopic(data.topic_id)
    if (!topic) { return }

    delete topic.typing[data.username]

    let post = Post.create(this.populatePermissions(data))
    if (post.created_at.match(/UTC/)) {
      post.created_at = moment(post.created_at.replace(' UTC', 'Z')).local().toString()
    }

    if (data.is_edit || data.is_delete) {
      topic.postStream.storePost(post)
    } else {

      let performScroll = _.any(forEachTopicContainer(topic, function($container) {
        return lastVisibleElement($container.find('.babble-chat'), '.babble-post', 'post-number') == topic.lastLoadedPostNumber
      }))
      let performNotification = BabbleRegistry.componentsForTopic(topic).length &&
                                post.user_id != User.currentProp('id')

      if (topic.lastLoadedPostNumber < post.post_number) {
        topic.set('lastLoadedPostNumber', post.post_number)
      }

      if (latestPostIsMine(topic)) {
        // clear staged post
        let staged =  topic.postStream.findLoadedPost(-1)
        if (staged) { topic.postStream.removePosts([staged]) }
        topic.postStream.commitPost(post)
      } else {
        topic.postStream.appendPost(post)
      }

      if(performNotification) { playNotification() }
      if (performScroll) { scrollToPost(topic, post.post_number) }

      forEachTopicContainer(topic, ($container) => { this.ensureRead(topic, $container) })
    }

    return syncWithPostStream(topic)
  },

  handleTyping(topic, data) {
    if (data.id == User.currentProp('id')) { return }
    topic.typing[data.username] = { user: data, lastTyped: moment() }
    rerender(topic)
  },

  handleOnline(topic, data) {
    if (data.id == User.currentProp('id')) { return }
    topic.online[data.username] = { user: data, lastSeen: moment() }
    rerender(topic)
  },

  ensureRead(topic, $container) {
    Ember.run.scheduleOnce('afterRender', () => {
      let postNumber = lastVisibleElement($container.find('.babble-chat'), '.babble-post', 'post-number')
      if (postNumber <= topic.last_read_post_number) { return }
      topic.set('last_read_post_number', postNumber)
      syncWithPostStream(topic)
      this.readPost(topic, postNumber)
    })
  },

  loadPosts(topic, order) {
    topic.set('loadingPosts', order)
    rerender(topic)
    let starterPostField = order === 'desc' ? 'firstLoadedPostNumber' : 'lastLoadedPostNumber'
    let postNumber = topic.get(starterPostField)

    return ajax(`/babble/topics/${topic.id}/posts/${postNumber}/${order}`).then((data) => {
      data.posts.map(function(post) { topic.postStream.appendPost(Post.create(post)) })
      syncWithPostStream(topic)
      scrollToPost(topic, topic.get(starterPostField))
    }).finally(() => {
      topic.set('loadingPosts', null)
    })
  },

  stagePost(topic, text) {
    const user = User.current()

    let post = Post.create({
      raw:                text,
      cooked:             text,
      name:               user.get('name'),
      display_username:   user.get('name'),
      username:           user.get('username'),
      user_id:            user.get('id'),
      user_title:         user.get('title'),
      avatar_template:    user.get('avatar_template'),
      user_custom_fields: user.get('custom_fields'),
      moderator:          user.get('moderator'),
      admin:              user.get('admin'),
      created_at:         moment()
    })
    topic.postStream.set('loadedAllPosts', true)
    topic.postStream.stagePost(post, user)
    topic.set('lastLoadedPostNumber', post.post_number)
    scrollToPost(topic, post.post_number)
    teardownComposer(topic)
    rerender(topic)
  }
})
<commit_msg>XSS patch<commit_after>import Post from 'discourse/models/post'
import Topic from 'discourse/models/topic'
import User from 'discourse/models/user'
import elementIsVisible from '../lib/element-is-visible'
import lastVisibleElement from '../lib/last-visible-element'
import debounce from 'discourse/lib/debounce'
import { cookAsync } from 'discourse/lib/text'
import { ajax } from 'discourse/lib/ajax'
import { scrollToPost, setupScrollContainer, playNotification, setupComposer, teardownComposer, hasChatElements } from '../lib/chat-element-utils'
import { syncWithPostStream, latestPostFor, latestPostIsMine, teardownPresence, setupLastReadMarker, applyPostStream } from '../lib/chat-topic-utils'
import { forEachTopicContainer } from '../lib/chat-topic-iterators'
import { rerender } from '../lib/chat-component-utils'
import { setupLiveUpdate, teardownLiveUpdate, messageBus } from '../lib/chat-live-update-utils'
import BabbleRegistry from '../lib/babble-registry'
import showModal from 'discourse/lib/show-modal'

export default Ember.Object.create({
  summary: {},

  disabled() {
    return !User.current() ||
           User.currentProp('custom_fields.babble_disabled') ||
           !Discourse.SiteSettings.babble_enabled
  },

  bindById(component, topicId) {
    return this.loadTopic(topicId).then((topic) => {
      return this.bind(component, topic)
    }, console.log)
  },

  bind(component, topic, postNumber) {
    if (!topic) { return }

    postNumber = postNumber || topic.last_read_post_number

    this.unbind(component)
    topic = BabbleRegistry.bind(component, topic)
    setupLastReadMarker(topic)

    Ember.run.scheduleOnce('afterRender', () => {
      setupLiveUpdate(topic, {
        '':       ((data) => { this.buildTopic(data) }),
        'typing': ((data) => { this.handleTyping(topic, data) }),
        'online': ((data) => { this.handleOnline(topic, data) })
      })

      if (hasChatElements(component.element)) {
        setupScrollContainer(topic)
        setupComposer(topic)
        scrollToPost(topic, postNumber, 0)
      }
    })

    rerender(topic)
    return topic
  },

  unbind(component) {
    let topic = BabbleRegistry.topicForComponent(component)
    if (!topic) { return }

    teardownLiveUpdate(topic, '', 'typing', 'online')

    if (hasChatElements(component.element)) {
      teardownPresence(topic)
    }

    BabbleRegistry.unbind(component)
  },

  loadTopic(id, opts = {}) {
    this.set('loadingTopicId', id)
    let path  = opts.pm ? '/pm' : ''
    let query = opts.postNumber ? `?near_post=${opts.postNumber}` : ''
    return ajax(`/babble/topics/${path}/${id}.json${query}`).then((data) => {
      return this.buildTopic(data)
    }).finally(() => {
      this.set('loadingTopicId', null)
    })
  },

  subscribeToNotifications(component) {
    if (!User.current()) { return }
    messageBus().subscribe(`/babble/notifications/${User.current().id}`, (data) => {
      this.handleNewPost(data.post)
      BabbleRegistry.storeNotification(data.notification)
      if (!component.initialized) {
        this.set('summary.notificationCount', this.summary.notificationCount + 1)
      }
      playNotification()
      component.appEvents.trigger('babble-rerender')
    })
  },

  loadBoot(component) {
    this.set('loadingBoot', true)
    return ajax(`/babble/boot.json`).then((data) => {
      _.each(data.notifications, (n) => { BabbleRegistry.storeNotification(n) })
      _.each(data.users,         (u) => { BabbleRegistry.storeUser(User.create(u)) })
      _.each(data.topics,        (t) => { this.setupTopicListener(t, component) })
    }).finally(() => {
      component.appEvents.trigger('babble-rerender')
      this.set('booted', true)
      this.set('loadingBoot', false)
    })
  },

  loadSummary(component) {
    this.set('loadingSummary', true)
    return ajax(`/babble/summary.json`).then((data) => {
      this.set('summary.topicCount', data.topic_count)
      this.set('summary.unreadCount', data.unread_count)
      this.set('summary.notificationCount', data.notification_count)
      this.set('summary.defaultId', data.default_id)

      if (this.summary.topicCount > 0) {
        component.appEvents.trigger('babble-has-topics')
      }
    }).finally(() => {
      component.appEvents.trigger('babble-rerender')
      this.set('loadingSummary', null)
    })
  },

  setupTopicListener(t, component) {
    setupLiveUpdate(this.buildTopic(t), {
      'posts': (data) => {
        this.handleNewPost(data)
        component.appEvents.trigger('babble-rerender')
      }
    })
  },

  unreadCount() {
    var unreadCount, notificationCount
    if (this.booted) {
      unreadCount       = this.availableTopics().reduce((total, topic) => { return total + topic.unreadCount }, 0)
      notificationCount = this.availableNotifications().length
    } else {
      unreadCount       = this.summary.unreadCount || 0
      notificationCount = this.summary.notificationCount || 0
    }
    if (unreadCount + notificationCount == 0) { return }
    return (notificationCount || '•').toString()
  },

  singleChannel() {
    return this.summary.topicCount == 1 && !Discourse.SiteSettings.babble_enable_pms
  },

  openByDefault() {
    return User.currentProp('custom_fields.babble_open_by_default')
  },

  availableTopics() {
    return BabbleRegistry.allTopics()
  },

  availableUsers() {
    return BabbleRegistry.allUsers()
  },

  availableNotifications() {
    return BabbleRegistry.allNotifications()
  },

  notificationsFor(item) {
    return this.availableNotifications().filter((n) => {
      if (item.constructor == User) {
        return n.sender_id == item.id
      } else {
        return n.topic_id == item.id
      }
    })
  },

  fetchDefault() {
    return this.fetchTopic(this.summary.defaultId)
  },

  fetchTopic(topicId) {
    return BabbleRegistry.fetchTopic(topicId)
  },

  topicForComponent(component) {
    return BabbleRegistry.topicForComponent(component)
  },

  buildTopic(data) {
    if (!data.id) { return }
    let topic = Topic.create(data)
    applyPostStream(topic)
    syncWithPostStream(topic)
    return BabbleRegistry.storeTopic(topic)
  },

  createPost(topic, text) {
    this.stagePost(topic, text)
    return ajax(`/babble/topics/${topic.id}/posts`, {
      type: 'POST',
      data: { raw: text }
    }).then((data) => {
      this.handleNewPost(data)
    })
  },

  readPost(topic, postNumber) {
    this.notificationsFor(topic).map((n) => {
      if (n.post_number <= postNumber) { BabbleRegistry.removeNotification(n.id) }
    })
    return ajax(`/babble/topics/${topic.id}/read/${postNumber}.json`)
  },

  editPost(topic, post) {
    if (post) {
      topic.set('editingPostId', post.id)
      scrollToPost(topic, post.post_number)
      setupComposer(topic)
    } else {
      topic.set('editingPostId', null)
    }
  },

  flagPost(topic, post) {
    if (post.get('actions_summary')) {
      showModal('flag', { model: post, babble: true })
    } else {
      // we have to get some more info from the API before we can properly display the flag modal
      ajax(`/posts/${post.id}`).then((data) => {
        data = Post.munge(data)
        post.set('actionByName', data.actionByName)
        post.set('actions_summary', data.actions_summary)
        showModal('flag', { model: post, babble: true })
      })
    }
  },

  updatePost(topic, post, text) {
    this.editPost(topic, null)
    topic.set('loadingEditId', post.id)
    return ajax(`/babble/topics/${post.topic_id}/posts/${post.id}`, {
      type: 'POST',
      data: { raw: text }
    }).then((data) => {
      this.handleNewPost(data)
    }).finally(() => {
      topic.set('loadingEditId', null)
    })
  },

  destroyPost(topic, post) {
    topic.set('loadingEditId', post.id)
    return ajax(`/babble/topics/${post.topic_id}/posts/${post.id}`, {
      type: 'DELETE'
    }).finally(() => {
      topic.set('loadingEditId', null)
    })
  },

  populatePermissions(data) {
    const user = User.current()

    if (!user || data.user_id != user.id) {
      delete data.can_edit
      delete data.can_flag
      delete data.can_delete
    }

    if(!_.contains(_.keys(data), 'can_edit')) {
      data.can_edit = user.staff ||
                      data.user_id == user.id ||
                      user.trust_level >= 4
    }
    if(!_.contains(_.keys(data), 'can_flag')) {
      data.can_flag = !data.user_id != user.id &&
                      (user.staff || user.trust_level >= 1)
    }
    if(!_.contains(_.keys(data), 'can_delete')) {
      data.can_delete = user.staff || data.user_id == user.id
    }

    return data
  },

  handleNewPost(data) {
    let topic = BabbleRegistry.fetchTopic(data.topic_id)
    if (!topic) { return }

    delete topic.typing[data.username]

    let post = Post.create(this.populatePermissions(data))
    if (post.created_at.match(/UTC/)) {
      post.created_at = moment(post.created_at.replace(' UTC', 'Z')).local().toString()
    }

    if (data.is_edit || data.is_delete) {
      topic.postStream.storePost(post)
    } else {

      let performScroll = _.any(forEachTopicContainer(topic, function($container) {
        return lastVisibleElement($container.find('.babble-chat'), '.babble-post', 'post-number') == topic.lastLoadedPostNumber
      }))
      let performNotification = BabbleRegistry.componentsForTopic(topic).length &&
                                post.user_id != User.currentProp('id')

      if (topic.lastLoadedPostNumber < post.post_number) {
        topic.set('lastLoadedPostNumber', post.post_number)
      }

      if (latestPostIsMine(topic)) {
        // clear staged post
        let staged =  topic.postStream.findLoadedPost(-1)
        if (staged) { topic.postStream.removePosts([staged]) }
        topic.postStream.commitPost(post)
      } else {
        topic.postStream.appendPost(post)
      }

      if(performNotification) { playNotification() }
      if (performScroll) { scrollToPost(topic, post.post_number) }

      forEachTopicContainer(topic, ($container) => { this.ensureRead(topic, $container) })
    }

    return syncWithPostStream(topic)
  },

  handleTyping(topic, data) {
    if (data.id == User.currentProp('id')) { return }
    topic.typing[data.username] = { user: data, lastTyped: moment() }
    rerender(topic)
  },

  handleOnline(topic, data) {
    if (data.id == User.currentProp('id')) { return }
    topic.online[data.username] = { user: data, lastSeen: moment() }
    rerender(topic)
  },

  ensureRead(topic, $container) {
    Ember.run.scheduleOnce('afterRender', () => {
      let postNumber = lastVisibleElement($container.find('.babble-chat'), '.babble-post', 'post-number')
      if (postNumber <= topic.last_read_post_number) { return }
      topic.set('last_read_post_number', postNumber)
      syncWithPostStream(topic)
      this.readPost(topic, postNumber)
    })
  },

  loadPosts(topic, order) {
    topic.set('loadingPosts', order)
    rerender(topic)
    let starterPostField = order === 'desc' ? 'firstLoadedPostNumber' : 'lastLoadedPostNumber'
    let postNumber = topic.get(starterPostField)

    return ajax(`/babble/topics/${topic.id}/posts/${postNumber}/${order}`).then((data) => {
      data.posts.map(function(post) { topic.postStream.appendPost(Post.create(post)) })
      syncWithPostStream(topic)
      scrollToPost(topic, topic.get(starterPostField))
    }).finally(() => {
      topic.set('loadingPosts', null)
    })
  },

  stagePost(topic, text) {
    const user = User.current()

    cookAsync(text).then((cooked) => {
      let post = Post.create({
        raw:                text,
        cooked:             cooked.string,
        name:               user.get('name'),
        display_username:   user.get('name'),
        username:           user.get('username'),
        user_id:            user.get('id'),
        user_title:         user.get('title'),
        avatar_template:    user.get('avatar_template'),
        user_custom_fields: user.get('custom_fields'),
        moderator:          user.get('moderator'),
        admin:              user.get('admin'),
        created_at:         moment()
      })
      topic.postStream.set('loadedAllPosts', true)
      topic.postStream.stagePost(post, user)
      topic.set('lastLoadedPostNumber', post.post_number)
      scrollToPost(topic, post.post_number)
      teardownComposer(topic)
      rerender(topic)
    })
  }
})
<|endoftext|>


<commit_before>module.exports = {
    "env": {
        "es6": true,
        "node": true
    },
    "extends": "eslint:recommended",
    "rules": {
        "accessor-pairs": "error",
        "array-bracket-spacing": "error",
        "array-callback-return": "error",
        "arrow-body-style": "off",
        "arrow-parens": "off",
        "arrow-spacing": [
            "error",
            {
                "after": true,
                "before": true
            }
        ],
        "block-scoped-var": "error",
        "block-spacing": "error",
        "brace-style": "off",
        "callback-return": "off",
        "camelcase": [
            "error",
            {
                "properties": "never"
            }
        ],
        "capitalized-comments": [
            "error",
            "never"
        ],
        "class-methods-use-this": "error",
        "comma-dangle": "error",
        "comma-spacing": [
            "error",
            {
                "after": true,
                "before": false
            }
        ],
        "comma-style": [
            "error",
            "last"
        ],
        "complexity": "error",
        "computed-property-spacing": [
            "error",
            "never"
        ],
        "consistent-return": "error",
        "consistent-this": "error",
        "curly": "error",
        "default-case": "error",
        "dot-location": [
            "error",
            "property"
        ],
        "dot-notation": "error",
        "eol-last": "error",
        "eqeqeq": "off",
        "func-call-spacing": "error",
        "func-name-matching": "error",
        "func-names": "error",
        "func-style": [
            "error",
            "declaration"
        ],
        "generator-star-spacing": "error",
        "global-require": "off",
        "guard-for-in": "error",
        "handle-callback-err": "off",
        "id-blacklist": "error",
        "id-length": "off",
        "id-match": "error",
        "indent": "off",
        "init-declarations": "error",
        "jsx-quotes": "error",
        "key-spacing": "error",
        "keyword-spacing": [
            "error",
            {
                "after": true,
                "before": true
            }
        ],
        "line-comment-position": "error",
        "linebreak-style": [
            "error",
            "unix"
        ],
        "lines-around-comment": "error",
        "lines-around-directive": "error",
        "max-depth": "error",
        "max-len": "off",
        "max-lines": "off",
        "max-nested-callbacks": "error",
        "max-params": "off",
        "max-statements": "off",
        "max-statements-per-line": "error",
        "multiline-ternary": [
            "error",
            "never"
        ],
        "new-parens": "error",
        "newline-after-var": "off",
        "newline-before-return": "off",
        "newline-per-chained-call": "off",
        "no-alert": "error",
        "no-array-constructor": "error",
        "no-await-in-loop": "error",
        "no-bitwise": "error",
        "no-caller": "error",
        "no-catch-shadow": "error",
        "no-compare-neg-zero": "error",
        "no-confusing-arrow": "error",
        "no-continue": "error",
        "no-div-regex": "error",
        "no-duplicate-imports": "error",
        "no-else-return": "off",
        "no-empty-function": "error",
        "no-eq-null": "error",
        "no-eval": "error",
        "no-extend-native": "error",
        "no-extra-bind": "error",
        "no-extra-label": "error",
        "no-extra-parens": "error",
        "no-floating-decimal": "error",
        "no-implicit-coercion": "error",
        "no-implicit-globals": "error",
        "no-implied-eval": "error",
        "no-inline-comments": "error",
        "no-invalid-this": "error",
        "no-iterator": "error",
        "no-label-var": "error",
        "no-labels": "error",
        "no-lone-blocks": "error",
        "no-lonely-if": "error",
        "no-loop-func": "error",
        "no-magic-numbers": "off",
        "no-mixed-operators": "error",
        "no-mixed-requires": "error",
        "no-multi-assign": "error",
        "no-multi-spaces": "error",
        "no-multi-str": "error",
        "no-multiple-empty-lines": "error",
        "no-native-reassign": "error",
        "no-negated-condition": "error",
        "no-negated-in-lhs": "error",
        "no-nested-ternary": "error",
        "no-new": "error",
        "no-new-func": "error",
        "no-new-object": "error",
        "no-new-require": "error",
        "no-new-wrappers": "error",
        "no-octal-escape": "error",
        "no-param-reassign": "error",
        "no-path-concat": "error",
        "no-plusplus": "error",
        "no-process-env": "error",
        "no-process-exit": "error",
        "no-proto": "error",
        "no-prototype-builtins": "error",
        "no-restricted-globals": "error",
        "no-restricted-imports": "error",
        "no-restricted-modules": "error",
        "no-restricted-properties": "error",
        "no-restricted-syntax": "error",
        "no-return-assign": "error",
        "no-return-await": "error",
        "no-script-url": "error",
        "no-self-compare": "error",
        "no-sequences": "error",
        "no-shadow": "off",
        "no-shadow-restricted-names": "error",
        "no-spaced-func": "error",
        "no-sync": "off",
        "no-tabs": "error",
        "no-template-curly-in-string": "error",
        "no-ternary": "off",
        "no-throw-literal": "error",
        "no-trailing-spaces": "error",
        "no-undef-init": "error",
        "no-undefined": "off",
        "no-underscore-dangle": "error",
        "no-unmodified-loop-condition": "error",
        "no-unneeded-ternary": "error",
        "no-unused-expressions": "error",
        "no-use-before-define": "off",
        "no-useless-call": "error",
        "no-useless-computed-key": "error",
        "no-useless-concat": "error",
        "no-useless-constructor": "error",
        "no-useless-escape": "off",
        "no-useless-rename": "error",
        "no-useless-return": "error",
        "no-var": "error",
        "no-void": "error",
        "no-warning-comments": "error",
        "no-whitespace-before-property": "error",
        "no-with": "error",
        "nonblock-statement-body-position": "error",
        "object-curly-newline": "error",
        "object-curly-spacing": "off",
        "object-property-newline": [
            "error",
            {
                "allowMultiplePropertiesPerLine": true
            }
        ],
        "object-shorthand": "off",
        "one-var": "off",
        "one-var-declaration-per-line": "error",
        "operator-assignment": "error",
        "operator-linebreak": "error",
        "padded-blocks": "off",
        "prefer-arrow-callback": "error",
        "prefer-const": "error",
        "prefer-destructuring": "error",
        "prefer-numeric-literals": "error",
        "prefer-promise-reject-errors": "error",
        "prefer-reflect": "off",
        "prefer-rest-params": "error",
        "prefer-spread": "error",
        "prefer-template": "off",
        "quote-props": "off",
        "quotes": "off",
        "radix": "error",
        "require-await": "error",
        "require-jsdoc": "off",
        "rest-spread-spacing": "error",
        "semi": "off",
        "semi-spacing": "error",
        "sort-imports": "error",
        "sort-keys": "off",
        "sort-vars": "error",
        "space-before-blocks": "error",
        "space-before-function-paren": "off",
        "space-in-parens": [
            "error",
            "never"
        ],
        "space-infix-ops": "error",
        "space-unary-ops": "error",
        "spaced-comment": [
            "error",
            "always"
        ],
        "strict": [
            "error",
            "never"
        ],
        "symbol-description": "error",
        "template-curly-spacing": [
            "error",
            "never"
        ],
        "template-tag-spacing": "error",
        "unicode-bom": [
            "error",
            "never"
        ],
        "valid-jsdoc": "error",
        "vars-on-top": "error",
        "wrap-iife": "error",
        "wrap-regex": "error",
        "yield-star-spacing": "error",
        "yoda": [
            "error",
            "never"
        ]
    }
};<commit_msg>Disable no-console eslint rule<commit_after>module.exports = {
    "env": {
        "es6": true,
        "node": true
    },
    "extends": "eslint:recommended",
    "rules": {
        "accessor-pairs": "error",
        "array-bracket-spacing": "error",
        "array-callback-return": "error",
        "arrow-body-style": "off",
        "arrow-parens": "off",
        "arrow-spacing": [
            "error",
            {
                "after": true,
                "before": true
            }
        ],
        "block-scoped-var": "error",
        "block-spacing": "error",
        "brace-style": "off",
        "callback-return": "off",
        "camelcase": [
            "error",
            {
                "properties": "never"
            }
        ],
        "capitalized-comments": [
            "error",
            "never"
        ],
        "class-methods-use-this": "error",
        "comma-dangle": "error",
        "comma-spacing": [
            "error",
            {
                "after": true,
                "before": false
            }
        ],
        "comma-style": [
            "error",
            "last"
        ],
        "complexity": "error",
        "computed-property-spacing": [
            "error",
            "never"
        ],
        "consistent-return": "error",
        "consistent-this": "error",
        "curly": "error",
        "default-case": "error",
        "dot-location": [
            "error",
            "property"
        ],
        "dot-notation": "error",
        "eol-last": "error",
        "eqeqeq": "off",
        "func-call-spacing": "error",
        "func-name-matching": "error",
        "func-names": "error",
        "func-style": [
            "error",
            "declaration"
        ],
        "generator-star-spacing": "error",
        "global-require": "off",
        "guard-for-in": "error",
        "handle-callback-err": "off",
        "id-blacklist": "error",
        "id-length": "off",
        "id-match": "error",
        "indent": "off",
        "init-declarations": "error",
        "jsx-quotes": "error",
        "key-spacing": "error",
        "keyword-spacing": [
            "error",
            {
                "after": true,
                "before": true
            }
        ],
        "line-comment-position": "error",
        "linebreak-style": [
            "error",
            "unix"
        ],
        "lines-around-comment": "error",
        "lines-around-directive": "error",
        "max-depth": "error",
        "max-len": "off",
        "max-lines": "off",
        "max-nested-callbacks": "error",
        "max-params": "off",
        "max-statements": "off",
        "max-statements-per-line": "error",
        "multiline-ternary": [
            "error",
            "never"
        ],
        "new-parens": "error",
        "newline-after-var": "off",
        "newline-before-return": "off",
        "newline-per-chained-call": "off",
        "no-alert": "error",
        "no-array-constructor": "error",
        "no-await-in-loop": "error",
        "no-bitwise": "error",
        "no-caller": "error",
        "no-catch-shadow": "error",
        "no-compare-neg-zero": "error",
        "no-confusing-arrow": "error",
        "no-console": "off",
        "no-continue": "error",
        "no-div-regex": "error",
        "no-duplicate-imports": "error",
        "no-else-return": "off",
        "no-empty-function": "error",
        "no-eq-null": "error",
        "no-eval": "error",
        "no-extend-native": "error",
        "no-extra-bind": "error",
        "no-extra-label": "error",
        "no-extra-parens": "error",
        "no-floating-decimal": "error",
        "no-implicit-coercion": "error",
        "no-implicit-globals": "error",
        "no-implied-eval": "error",
        "no-inline-comments": "error",
        "no-invalid-this": "error",
        "no-iterator": "error",
        "no-label-var": "error",
        "no-labels": "error",
        "no-lone-blocks": "error",
        "no-lonely-if": "error",
        "no-loop-func": "error",
        "no-magic-numbers": "off",
        "no-mixed-operators": "error",
        "no-mixed-requires": "error",
        "no-multi-assign": "error",
        "no-multi-spaces": "error",
        "no-multi-str": "error",
        "no-multiple-empty-lines": "error",
        "no-native-reassign": "error",
        "no-negated-condition": "error",
        "no-negated-in-lhs": "error",
        "no-nested-ternary": "error",
        "no-new": "error",
        "no-new-func": "error",
        "no-new-object": "error",
        "no-new-require": "error",
        "no-new-wrappers": "error",
        "no-octal-escape": "error",
        "no-param-reassign": "error",
        "no-path-concat": "error",
        "no-plusplus": "error",
        "no-process-env": "error",
        "no-process-exit": "error",
        "no-proto": "error",
        "no-prototype-builtins": "error",
        "no-restricted-globals": "error",
        "no-restricted-imports": "error",
        "no-restricted-modules": "error",
        "no-restricted-properties": "error",
        "no-restricted-syntax": "error",
        "no-return-assign": "error",
        "no-return-await": "error",
        "no-script-url": "error",
        "no-self-compare": "error",
        "no-sequences": "error",
        "no-shadow": "off",
        "no-shadow-restricted-names": "error",
        "no-spaced-func": "error",
        "no-sync": "off",
        "no-tabs": "error",
        "no-template-curly-in-string": "error",
        "no-ternary": "off",
        "no-throw-literal": "error",
        "no-trailing-spaces": "error",
        "no-undef-init": "error",
        "no-undefined": "off",
        "no-underscore-dangle": "error",
        "no-unmodified-loop-condition": "error",
        "no-unneeded-ternary": "error",
        "no-unused-expressions": "error",
        "no-use-before-define": "off",
        "no-useless-call": "error",
        "no-useless-computed-key": "error",
        "no-useless-concat": "error",
        "no-useless-constructor": "error",
        "no-useless-escape": "off",
        "no-useless-rename": "error",
        "no-useless-return": "error",
        "no-var": "error",
        "no-void": "error",
        "no-warning-comments": "error",
        "no-whitespace-before-property": "error",
        "no-with": "error",
        "nonblock-statement-body-position": "error",
        "object-curly-newline": "error",
        "object-curly-spacing": "off",
        "object-property-newline": [
            "error",
            {
                "allowMultiplePropertiesPerLine": true
            }
        ],
        "object-shorthand": "off",
        "one-var": "off",
        "one-var-declaration-per-line": "error",
        "operator-assignment": "error",
        "operator-linebreak": "error",
        "padded-blocks": "off",
        "prefer-arrow-callback": "error",
        "prefer-const": "error",
        "prefer-destructuring": "error",
        "prefer-numeric-literals": "error",
        "prefer-promise-reject-errors": "error",
        "prefer-reflect": "off",
        "prefer-rest-params": "error",
        "prefer-spread": "error",
        "prefer-template": "off",
        "quote-props": "off",
        "quotes": "off",
        "radix": "error",
        "require-await": "error",
        "require-jsdoc": "off",
        "rest-spread-spacing": "error",
        "semi": "off",
        "semi-spacing": "error",
        "sort-imports": "error",
        "sort-keys": "off",
        "sort-vars": "error",
        "space-before-blocks": "error",
        "space-before-function-paren": "off",
        "space-in-parens": [
            "error",
            "never"
        ],
        "space-infix-ops": "error",
        "space-unary-ops": "error",
        "spaced-comment": [
            "error",
            "always"
        ],
        "strict": [
            "error",
            "never"
        ],
        "symbol-description": "error",
        "template-curly-spacing": [
            "error",
            "never"
        ],
        "template-tag-spacing": "error",
        "unicode-bom": [
            "error",
            "never"
        ],
        "valid-jsdoc": "error",
        "vars-on-top": "error",
        "wrap-iife": "error",
        "wrap-regex": "error",
        "yield-star-spacing": "error",
        "yoda": [
            "error",
            "never"
        ]
    }
};
<|endoftext|>


<commit_before>import React from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { isEqual, isEmpty } from 'lodash/lang';

import * as navigationActions from './actions';
import { METHODS } from './router';
import { extractQuery, createQuery } from './pageUtils';
import shouldGoBack from './shouldGoBack';

const T = React.PropTypes;

const isNewTabClick = (e) => e.metaKey || e.ctrlKey || e.button === 1 || e.button === 4;

// ****** Anchor
export class _Anchor extends React.Component {
  static propTypes = {
    href: T.string,
    noop: T.bool,
    className: T.string,
    style: T.object,
    navigateToPage: T.func,
    onClick: T.func,
  };

  static defaultProps = {
    href: '#',
    noop: false,
    navigateToPage: () => {},
    onClick: () => {},
  };

  handleClick = e => {
    if (isNewTabClick(e)) { return; }

    this.props.onClick(e);
    if (e.defaultPrevented) { return; }

    e.stopPropagation();
    e.preventDefault();

    const url = this.props.href.split('?')[0];
    const queryParams = extractQuery(this.props.href);

    this.props.navigateToPage(url, queryParams);
  }

  render() {
    const { href, className, style, children } = this.props;

    return (
      <a
        href={ href }
        className={ className }
        style={ style }
        onClick={ this.handleClick }
      >
        { children }
      </a>
    );
  }
}

export class _BackAnchor extends React.Component {
  static propTypes = {
    href: T.string,
    backupHref: T.string,
    noop: T.bool,
    className: T.string,
    style: T.object,
    referrer: T.string,
    navigateToPage: T.func,
    onClick: T.func,
  };

  static defaultProps = {
    href: '#',
    backupHref: '#',
    noop: false,
    referrer: '',
    navigateToPage: () => {},
    onClick: () => {},
  };

  static AUTO_ROUTE = '__backanchor-auto-route';

  handleClick = e => {
    if (isNewTabClick(e)) { return; }

    this.props.onClick(e);
    if (e.defaultPrevented) { return; }

    e.stopPropagation();
    e.preventDefault();

    const { urlHistory, currentIndex, href, referrer, backupHref } = this.props;

    const unParsedUrl = href === _BackAnchor.AUTO_ROUTE ? referrer || backupHref : href;
    const url = unParsedUrl.split('?')[0];
    const queryParams = extractQuery(unParsedUrl);

    if (shouldGoBack(urlHistory, currentIndex, url, queryParams)) {
      history.back();
    } else {
      this.props.navigateToPage(url, queryParams);
    }
  }

  render() {
    const { href, className, style, children, referrer, backupHref } = this.props;
    const renderHref = href === _BackAnchor.AUTO_ROUTE
      ? referrer || backupHref
      : href;

    return (
      <a
        href={ renderHref }
        className={ className }
        style={ style }
        onClick={ this.handleClick }
      >
        { children }
      </a>
    );
  }
}

const anchorSelector = createSelector(
  state => state.platform.history,
  state => state.platform.currentPageIndex,
  state => state.platform.currentPage.referrer,
  (urlHistory, currentIndex, referrer) => ({ urlHistory, currentIndex, referrer })
);

const anchorDispatcher = dispatch => ({
  navigateToPage: (url, queryParams) => dispatch(
    navigationActions.navigateToUrl(METHODS.GET, url, { queryParams })
  ),
});

export const Anchor = connect(null, anchorDispatcher)(_Anchor);
export const BackAnchor = connect(anchorSelector, anchorDispatcher)(_BackAnchor);
BackAnchor.AUTO_ROUTE = _BackAnchor.AUTO_ROUTE;

// ****** Form
const getValues = form => {
  if (!form || form.nodeName.toLowerCase() !== 'form') {
    return {};
  }

  return Array.from(form.elements).reduce((values, el) => {
    if (el.name) {
      switch (el.type) {
        case 'checkbox': {
          if (!values[el.name]) { values[el.name] = []; }
          if (el.value) { values[el.name].push(el.value); }
          break;
        }
        case 'select-multiple': {
          values[el.name] = Array.from(el.options).map(o => o.value);
          break;
        }
        case 'radio': {
          if (el.checked) { values[el.name] = el.value; }
          break;
        }
        default: {
          values[el.name] = el.value;
          break;
        }
      }
    }
    return values;
  }, {});
};

export class _Form extends React.Component {
  static propTypes = {
    action: T.string.isRequired,
    method: T.oneOf([METHODS.POST, METHODS.PUT, METHODS.DELETE, METHODS.PATCH]),
    className: T.string,
    style: T.object,
    onSubmit: T.func,
  };

  static defaultProps = {
    method: METHODS.POST,
    onSubmit: () => {},
  };

  handleSubmit = e => {
    e.preventDefault();

    const form = e.target;
    this.props.onSubmit(this.props.action, this.props.method, getValues(form));
  }

  render() {
    const { className, action, method, style, children } = this.props;

    return (
      <form
        className={ className }
        action={ action }
        method={ method }
        style={ style }
        onSubmit={ this.handleSubmit }
      >
        { children }
      </form>
    );
  }
}

const formDispatcher = dispatch => ({
  onSubmit: (url, method, bodyParams) => dispatch(
    navigationActions.navigateToUrl(method, url, { bodyParams })
  ),
});

export const Form = connect(null, formDispatcher)(_Form);

// ****** UrlSync
export class _UrlSync extends React.Component {
  static propTypes = {
    pageIndex: T.number.isRequired,
    history: T.array.isRequired,
    gotoPageIndex: T.func,
    navigateToPage: T.func,
  };

  static defaultProps = {
    gotoPageIndex: () => {},
    navigateToPage: () => {},
  };

  componentDidMount() {
    const handlePopstate = () => {
      const pathname = self.location.pathname;
      const currentQuery = extractQuery(self.location.search);
      const currentHash = {}; // TODO: address how hashes are displayed
      let pageIndex = -1;

      for (let i = this.props.history.length - 1; i >= 0; i--) {
        const hist = this.props.history[i];
        if (hist.url === pathname && isEqual(hist.queryParams, currentQuery)) {
          pageIndex = i;
          break;
        }
      }

      if (pageIndex > -1) {
        this.props.gotoPageIndex(pageIndex);
      } else {
        // can't find the url, just navigate
        this.props.navigateToPage(pathname, currentQuery, currentHash);
      }
    };

    self.addEventListener('popstate', handlePopstate);
    self.addEventListener('hashchange', handlePopstate);
  }

  componentWillUpdate(nextProps) {
    const currentQuery = extractQuery(self.location.search);
    const { pageIndex, history } = nextProps;
    const page = history[pageIndex];
    const newUrl = page.url;
    const newQuery = page.queryParams;

    if ((self.location.pathname !== newUrl) || (!isEqual(currentQuery, newQuery))) {
      if (self.history && self.history.pushState) {
        let newHref = newUrl;
        if (!isEmpty(newQuery)) { newHref += createQuery(newQuery); }
        self.history.pushState({}, '', newHref);
      } else {
        self.location = newUrl;
      }
    }
  }

  render() {
    return false;
  }
}

const urlSelector = createSelector(
  state => state.platform.currentPageIndex,
  state => state.platform.history,
  (pageIndex, history) => ({ pageIndex, history })
);

const urlDispatcher = dispatch => ({
  gotoPageIndex: index => dispatch(navigationActions.gotoPageIndex(index)),
  navigateToPage: (url, queryParams, hashParams) => dispatch(
    navigationActions.navigateToUrl(METHODS.GET, url, { queryParams, hashParams })
  ),
});

export const UrlSync = connect(urlSelector, urlDispatcher)(_UrlSync);
<commit_msg>Remove invalid form methods<commit_after>import React from 'react';
import { connect } from 'react-redux';
import { createSelector } from 'reselect';
import { isEqual, isEmpty } from 'lodash/lang';

import * as navigationActions from './actions';
import { METHODS } from './router';
import { extractQuery, createQuery } from './pageUtils';
import shouldGoBack from './shouldGoBack';

const T = React.PropTypes;

const isNewTabClick = (e) => e.metaKey || e.ctrlKey || e.button === 1 || e.button === 4;

// ****** Anchor
export class _Anchor extends React.Component {
  static propTypes = {
    href: T.string,
    noop: T.bool,
    className: T.string,
    style: T.object,
    navigateToPage: T.func,
    onClick: T.func,
  };

  static defaultProps = {
    href: '#',
    noop: false,
    navigateToPage: () => {},
    onClick: () => {},
  };

  handleClick = e => {
    if (isNewTabClick(e)) { return; }

    this.props.onClick(e);
    if (e.defaultPrevented) { return; }

    e.stopPropagation();
    e.preventDefault();

    const url = this.props.href.split('?')[0];
    const queryParams = extractQuery(this.props.href);

    this.props.navigateToPage(url, queryParams);
  }

  render() {
    const { href, className, style, children } = this.props;

    return (
      <a
        href={ href }
        className={ className }
        style={ style }
        onClick={ this.handleClick }
      >
        { children }
      </a>
    );
  }
}

export class _BackAnchor extends React.Component {
  static propTypes = {
    href: T.string,
    backupHref: T.string,
    noop: T.bool,
    className: T.string,
    style: T.object,
    referrer: T.string,
    navigateToPage: T.func,
    onClick: T.func,
  };

  static defaultProps = {
    href: '#',
    backupHref: '#',
    noop: false,
    referrer: '',
    navigateToPage: () => {},
    onClick: () => {},
  };

  static AUTO_ROUTE = '__backanchor-auto-route';

  handleClick = e => {
    if (isNewTabClick(e)) { return; }

    this.props.onClick(e);
    if (e.defaultPrevented) { return; }

    e.stopPropagation();
    e.preventDefault();

    const { urlHistory, currentIndex, href, referrer, backupHref } = this.props;

    const unParsedUrl = href === _BackAnchor.AUTO_ROUTE ? referrer || backupHref : href;
    const url = unParsedUrl.split('?')[0];
    const queryParams = extractQuery(unParsedUrl);

    if (shouldGoBack(urlHistory, currentIndex, url, queryParams)) {
      history.back();
    } else {
      this.props.navigateToPage(url, queryParams);
    }
  }

  render() {
    const { href, className, style, children, referrer, backupHref } = this.props;
    const renderHref = href === _BackAnchor.AUTO_ROUTE
      ? referrer || backupHref
      : href;

    return (
      <a
        href={ renderHref }
        className={ className }
        style={ style }
        onClick={ this.handleClick }
      >
        { children }
      </a>
    );
  }
}

const anchorSelector = createSelector(
  state => state.platform.history,
  state => state.platform.currentPageIndex,
  state => state.platform.currentPage.referrer,
  (urlHistory, currentIndex, referrer) => ({ urlHistory, currentIndex, referrer })
);

const anchorDispatcher = dispatch => ({
  navigateToPage: (url, queryParams) => dispatch(
    navigationActions.navigateToUrl(METHODS.GET, url, { queryParams })
  ),
});

export const Anchor = connect(null, anchorDispatcher)(_Anchor);
export const BackAnchor = connect(anchorSelector, anchorDispatcher)(_BackAnchor);
BackAnchor.AUTO_ROUTE = _BackAnchor.AUTO_ROUTE;

// ****** Form
const getValues = form => {
  if (!form || form.nodeName.toLowerCase() !== 'form') {
    return {};
  }

  return Array.from(form.elements).reduce((values, el) => {
    if (el.name) {
      switch (el.type) {
        case 'checkbox': {
          if (!values[el.name]) { values[el.name] = []; }
          if (el.value) { values[el.name].push(el.value); }
          break;
        }
        case 'select-multiple': {
          values[el.name] = Array.from(el.options).map(o => o.value);
          break;
        }
        case 'radio': {
          if (el.checked) { values[el.name] = el.value; }
          break;
        }
        default: {
          values[el.name] = el.value;
          break;
        }
      }
    }
    return values;
  }, {});
};

export class _Form extends React.Component {
  static propTypes = {
    action: T.string.isRequired,
    method: T.oneOf([METHODS.POST, METHODS.GET]),
    className: T.string,
    style: T.object,
    onSubmit: T.func,
  };

  static defaultProps = {
    method: METHODS.POST,
    onSubmit: () => {},
  };

  handleSubmit = e => {
    e.preventDefault();

    const form = e.target;
    this.props.onSubmit(this.props.action, this.props.method, getValues(form));
  }

  render() {
    const { className, action, method, style, children } = this.props;

    return (
      <form
        className={ className }
        action={ action }
        method={ method }
        style={ style }
        onSubmit={ this.handleSubmit }
      >
        { children }
      </form>
    );
  }
}

const formDispatcher = dispatch => ({
  onSubmit: (url, method, bodyParams) => dispatch(
    navigationActions.navigateToUrl(method, url, { bodyParams })
  ),
});

export const Form = connect(null, formDispatcher)(_Form);

// ****** UrlSync
export class _UrlSync extends React.Component {
  static propTypes = {
    pageIndex: T.number.isRequired,
    history: T.array.isRequired,
    gotoPageIndex: T.func,
    navigateToPage: T.func,
  };

  static defaultProps = {
    gotoPageIndex: () => {},
    navigateToPage: () => {},
  };

  componentDidMount() {
    const handlePopstate = () => {
      const pathname = self.location.pathname;
      const currentQuery = extractQuery(self.location.search);
      const currentHash = {}; // TODO: address how hashes are displayed
      let pageIndex = -1;

      for (let i = this.props.history.length - 1; i >= 0; i--) {
        const hist = this.props.history[i];
        if (hist.url === pathname && isEqual(hist.queryParams, currentQuery)) {
          pageIndex = i;
          break;
        }
      }

      if (pageIndex > -1) {
        this.props.gotoPageIndex(pageIndex);
      } else {
        // can't find the url, just navigate
        this.props.navigateToPage(pathname, currentQuery, currentHash);
      }
    };

    self.addEventListener('popstate', handlePopstate);
    self.addEventListener('hashchange', handlePopstate);
  }

  componentWillUpdate(nextProps) {
    const currentQuery = extractQuery(self.location.search);
    const { pageIndex, history } = nextProps;
    const page = history[pageIndex];
    const newUrl = page.url;
    const newQuery = page.queryParams;

    if ((self.location.pathname !== newUrl) || (!isEqual(currentQuery, newQuery))) {
      if (self.history && self.history.pushState) {
        let newHref = newUrl;
        if (!isEmpty(newQuery)) { newHref += createQuery(newQuery); }
        self.history.pushState({}, '', newHref);
      } else {
        self.location = newUrl;
      }
    }
  }

  render() {
    return false;
  }
}

const urlSelector = createSelector(
  state => state.platform.currentPageIndex,
  state => state.platform.history,
  (pageIndex, history) => ({ pageIndex, history })
);

const urlDispatcher = dispatch => ({
  gotoPageIndex: index => dispatch(navigationActions.gotoPageIndex(index)),
  navigateToPage: (url, queryParams, hashParams) => dispatch(
    navigationActions.navigateToUrl(METHODS.GET, url, { queryParams, hashParams })
  ),
});

export const UrlSync = connect(urlSelector, urlDispatcher)(_UrlSync);
<|endoftext|>


<commit_before>var backbone = require('backbone');
var backboneBase = require('backbone-base');
var jsonEditor = require('json-editor');
var highlight = require('highlight-redux');
var UploadView = require('./upload');
var registry = require('./registry');
var _ = require('underscore');
var $ = require('jquery');

// Upload data file and populate .resource array with item
DataUploadView = backbone.BaseView.extend({
  events: {
    'click [data-id=upload-data-file]': function() { this.$('[data-id=input]').trigger('click'); }
  },

  render: function() {
    this.$el
      .append('<input data-id="input" type="file" style="display: none;">')

      .append(
        $(this.options.form.theme.getButton('Upload data file', '', 'Upload data file'))
          .attr('data-id', 'upload-data-file')
      );

    return this;
  }
});

module.exports = {
  DescriptorEditView: backbone.BaseView.extend({
    activate: function(state) {
      backbone.BaseView.prototype.activate.call(this, state);
      this.layout.upload.activate(state);
      return this;
    },

    initialize: function() {
      highlight.configure({useBR: true});
      return backbone.BaseView.prototype.initialize.apply(this, arguments);
    },

    render: function() {
      this.layout.upload = new UploadView({el: window.APP.$('#upload-data-package'), parent: this});
      this.layout.registryList = new registry.ListView({el: window.APP.$('#registry-list'), parent: this});
      return this;
    },

    // Utility method to remove empty values from object recursive
    compactObject: function(data) {
      _.each(data, function(v, k) {
        if(_.isEmpty(v)) {
          delete data[k];
        } else if(_.isArray(v) || _.isObject(v)) {
          v = this.compactObject(v);

          if(_.isArray(v))
            v = _.compact(v);

          if(_.isEmpty(v))
            delete data[k];
          else data[k] = v;
        }
      }, this);

      return data;
    },

    getFilledValues: function() { return this.compactObject(this.layout.form.getValue()); },
    getValue: function () { return this.layout.form.getValue(); },
    hasChanges: function() { return Boolean(this.changed); },

    reset: function(schema) {
      var formData;

      // Clean up previous state
      if(this.layout.form) {
        formData = this.getFilledValues();
        this.layout.form.destroy();
        this.layout.uploadData.undelegateEvents().remove();
      }

      this.layout.form = new JSONEditor(this.el, {
        schema: schema,
        theme: 'bootstrap3'
      });

      this.layout.uploadData = (new DataUploadView({
        el: this.layout.form.theme.getHeaderButtonHolder(),
        form: this.layout.form,
        parent: this
      })).render();

      this.layout.form.on('ready', (function() {
        // There is no any good way to bind events to custom button or even add cutsom button
        $(this.layout.form.getEditor('root.resources').container)
          .children('h3').append(this.layout.uploadData.el);

        // Detecting changes
        this.changed = false;

        // After `ready` event fired, editor fire `change` event regarding to the initial changes
        this.layout.form.on('change', _.after(2, (function() {
          this.changed = true;
          window.APP.layout.download.reset(this.layout.form.getValue()).activate();
          this.showResult();
        }).bind(this)));

        // If on the previous form was entered values try to apply it to new form
        if(formData)
          this.layout.form.setValue(_.extend({}, this.layout.form.getValue(formData), formData));
      }).bind(this));
    },

    showResult: function() {
      $('#json-code').html(highlight.fixMarkup(
        highlight.highlight('json', JSON.stringify(this.layout.form.getValue(), undefined, 2)).value
      ));

      return this;
    }
  })
};<commit_msg>Populate resources array with item with name and path properties defined (#31)<commit_after>require('fileapi');

var backbone = require('backbone');
var backboneBase = require('backbone-base');
var jsonEditor = require('json-editor');
var highlight = require('highlight-redux');
var UploadView = require('./upload');
var registry = require('./registry');
var _ = require('underscore');
var $ = require('jquery');

// Upload data file and populate .resource array with item
DataUploadView = backbone.BaseView.extend({
  events: {
    'change [data-id=input]': function(E) {
      FileAPI.readAsText(FileAPI.getFiles(E.currentTarget)[0], (function (EV) {
        if(EV.type === 'load') {
          this.options.form.getEditor('root.resources').addRow({
            name: EV.target.name,
            path: EV.target.name
          });
        } else if( EV.type ==='progress' ){
          this.setProgress(EV.loaded/EV.total * 100);
        } else {
          this.setError('File upload failed');
        }

        this.$('[data-id=input]').val('');
      }).bind(this));
    },

    'click [data-id=upload-data-file]': function() { this.$('[data-id=input]').trigger('click'); }
  },

  render: function() {
    this.$el
      .append('<input data-id="input" type="file" style="display: none;">')

      .append(
        $(this.options.form.theme.getButton('Upload data file', '', 'Upload data file'))
          .attr('data-id', 'upload-data-file')
      );

    return this;
  },

  setError: function() { return this; },
  setProgress: function() { return this; }
});

module.exports = {
  DescriptorEditView: backbone.BaseView.extend({
    activate: function(state) {
      backbone.BaseView.prototype.activate.call(this, state);
      this.layout.upload.activate(state);
      return this;
    },

    initialize: function() {
      highlight.configure({useBR: true});
      return backbone.BaseView.prototype.initialize.apply(this, arguments);
    },

    render: function() {
      this.layout.upload = new UploadView({el: window.APP.$('#upload-data-package'), parent: this});
      this.layout.registryList = new registry.ListView({el: window.APP.$('#registry-list'), parent: this});
      return this;
    },

    // Utility method to remove empty values from object recursive
    compactObject: function(data) {
      _.each(data, function(v, k) {
        if(_.isEmpty(v)) {
          delete data[k];
        } else if(_.isArray(v) || _.isObject(v)) {
          v = this.compactObject(v);

          if(_.isArray(v))
            v = _.compact(v);

          if(_.isEmpty(v))
            delete data[k];
          else data[k] = v;
        }
      }, this);

      return data;
    },

    getFilledValues: function() { return this.compactObject(this.layout.form.getValue()); },
    getValue: function () { return this.layout.form.getValue(); },
    hasChanges: function() { return Boolean(this.changed); },

    reset: function(schema) {
      var formData;

      // Clean up previous state
      if(this.layout.form) {
        formData = this.getFilledValues();
        this.layout.form.destroy();
        this.layout.uploadData.undelegateEvents().remove();
      }

      this.layout.form = new JSONEditor(this.el, {
        schema: schema,
        theme: 'bootstrap3'
      });

      this.layout.uploadData = (new DataUploadView({
        el: this.layout.form.theme.getHeaderButtonHolder(),
        form: this.layout.form,
        parent: this
      })).render();

      this.layout.form.on('ready', (function() {
        // There is no any good way to bind events to custom button or even add cutsom button
        $(this.layout.form.getEditor('root.resources').container)
          .children('h3').append(this.layout.uploadData.el);

        // Detecting changes
        this.changed = false;

        // After `ready` event fired, editor fire `change` event regarding to the initial changes
        this.layout.form.on('change', _.after(2, (function() {
          this.changed = true;
          window.APP.layout.download.reset(this.layout.form.getValue()).activate();
          this.showResult();
        }).bind(this)));

        // If on the previous form was entered values try to apply it to new form
        if(formData)
          this.layout.form.setValue(_.extend({}, this.layout.form.getValue(formData), formData));
      }).bind(this));
    },

    showResult: function() {
      $('#json-code').html(highlight.fixMarkup(
        highlight.highlight('json', JSON.stringify(this.layout.form.getValue(), undefined, 2)).value
      ));

      return this;
    }
  })
};<|endoftext|>