4minitz/4minitz

View on GitHub
client/templates/topic/topicElement.js

Summary

Maintainability
C
1 day
Test Coverage
import { Minutes } from '/imports/minutes';
import { Meteor } from 'meteor/meteor';
import { Template } from 'meteor/templating';
import { Session } from 'meteor/session';
import { ReactiveVar } from 'meteor/reactive-var';
import { $ } from 'meteor/jquery';
import { MeetingSeries } from '/imports/meetingseries';
import { Topic } from '/imports/topic';
import { ConfirmationDialogFactory } from '../../helpers/confirmationDialogFactory';
import { TopicInfoItemListContext } from './topicInfoItemList';
import {LabelResolver} from '../../../imports/services/labelResolver';
import {ResponsibleResolver} from '../../../imports/services/responsibleResolver';
import {labelSetFontColor} from './helpers/label-set-font-color';
import { handleError } from '../../helpers/handleError';
import {detectTypeAndCreateItem} from './helpers/create-item';
import {resizeTextarea} from './helpers/resize-textarea';
import { FlowRouter } from 'meteor/ostrio:flow-router-extra';
import { i18n } from 'meteor/universe:i18n';


let _minutesId;
const INITIAL_ITEMS_LIMIT = 4;

const isFeatureShowItemInputFieldOnDemandEnabled = () => {
    return !(Meteor.settings && Meteor.settings.public && Meteor.settings.public.isEnd2EndTest);
};

Template.topicElement.onCreated(function () {
    let tmplData = Template.instance().data;
    _minutesId = tmplData.minutesID;

    this.isItemsLimited = new ReactiveVar(tmplData.topic.infoItems.length > INITIAL_ITEMS_LIMIT);
    this.isCollapsed = new ReactiveVar(false);
});

Template.topicElement.helpers({
    isTopicFinallyCompleted() {
        let aTopic = undefined;
        if (this.minutesID) {                       // on minutes edit view
            aTopic = new Topic(this.minutesID, this.topic._id);
        } else if (this.parentMeetingSeriesId) {    // on meeting series topic view
            aTopic = new Topic(this.parentMeetingSeriesId, this.topic._id);
        }
        if (aTopic) {
            return aTopic.isFinallyCompleted();
        }
    },

    hideAddItemInputField() {
        return isFeatureShowItemInputFieldOnDemandEnabled();
    },

    getLabels: function() {
        let tmplData = Template.instance().data;
        return LabelResolver.resolveLabels(this.topic.labels, tmplData.parentMeetingSeriesId).map(labelSetFontColor);
    },

    checkedState: function () {
        if (this.topic.isOpen) {
            return '';
        } else {
            return {checked: 'checked'};
        }
    },

    disabledState: function () {
        if ((this.isEditable) && (!this.topic.isSkipped)) {
            return '';
        } else {
            return {disabled: 'disabled'};
        }
    },

    // determine if this topic shall be rendered collapsed
    isCollapsed() {
        let collapseState = Session.get('minutesedit.collapsetopics.'+_minutesId);
        return collapseState ? collapseState[this.topic._id] : false;
    },

    showRecurringIcon() {
        return (this.isEditable || this.topic.isRecurring);
    },

    responsiblesHelper() {
        try {
            const responsible = ResponsibleResolver.resolveAndformatResponsiblesString(this.topic.responsibles);
            return (responsible) ? `(${responsible})` : '';
        } catch (e) {
            // intentionally left blank.
            // on deletion of a topic blaze once calls this method on the just deleted topic
            // we handle this gracefully with this empty exception handler
        }
        return '';
    },

    getData() {
        const data = Template.instance().data;
        const parentElement = (data.minutesID) ? data.minutesID : data.parentMeetingSeriesId;
        return TopicInfoItemListContext.createContextForItemsOfOneTopic(
            data.topic.infoItems,
            !data.isEditable,
            parentElement,
            data.topic._id
        );
    },

    classForEdit() {
        return this.isEditable ? 'btnEditTopic' : '';
    },
    
    classForSkippedTopics() {
        return this.topic.isSkipped ? 'strikethrough' : '';
    },
    
    cursorForEdit() {
        return this.isEditable ? 'pointer' : '';
    },
    
    showMenu() {
        return ((this.isEditable) || // Context: Current non-finalized Minute
            (!this.minutesID && !this.topic.isOpen && new MeetingSeries(this.parentMeetingSeriesId).isCurrentUserModerator())); // Context: Closed Topic within MeetingSeries, user is moderator;
    }
});

const editTopicEventHandler = (evt, context, manipulateTopic) => {
    evt.preventDefault();
    if (!context.minutesID || !context.isEditable) {
        return;
    }
    const aTopic = new Topic(context.minutesID, context.topic._id);
    manipulateTopic(aTopic);
};

const openAddItemDialog = (itemType, topicId) => {
    Session.set('topicInfoItemEditTopicId', topicId);
    Session.set('topicInfoItemType', itemType);
};

const showHideItemInput = (tmpl, show = true, then = () => {}) => {
    if (!isFeatureShowItemInputFieldOnDemandEnabled()) {
        return;
    }

    const addItemEl = tmpl.$('.addItemForm');
    const theItemTextarea = tmpl.$('.add-item-field');
    if (show) {
        addItemEl.show(250);
        Meteor.setTimeout(() => {
            resizeTextarea(theItemTextarea);
            then();

            Meteor.setTimeout(() => {
                // only focus, if parent topic element fits in view port
                if (tmpl.$('.topic-element').outerHeight() < window.innerHeight) {
                    theItemTextarea.focus();
                }
            },50);
        }, 300);
    } else {
        addItemEl.hide(250);
    }
};

let savingNewItem = false;

Template.topicElement.events({

    'click .topic-element'(evt, tmpl) {
        // return if the current target is not the original target of the event
        if (evt.currentTarget.getAttribute('class') !== evt.target.getAttribute('class')) {
            return;
        }

        if (tmpl.$('.topic-element').hasClass('focus')) {   // guard against multiple nested calls
            return;
        }
        tmpl.$('.topic-element').addClass('focus');
        showHideItemInput(tmpl);
    },

    'blur .topic-element'(evt, tmpl) {
        if (! tmpl.$('.topic-element').hasClass('focus')) { // guard against multiple nested calls
            return;
        }
        if (savingNewItem) {
            savingNewItem = false;
            return;
        }
        const nextElement = evt.relatedTarget;
        const topicElement = tmpl.find('.topic-element');
        if (!nextElement || !topicElement.contains(nextElement)) {
            tmpl.$('.topic-element').removeClass('focus');
            // Meteor.setTimeout(() => { showHideItemInput(tmpl, false); }, 500);
            showHideItemInput(tmpl, false);
        }
    },

    'click #btnDelTopic'(evt) {
        evt.preventDefault();

        if (!this.minutesID) {
            return;
        }
        console.log('Delete topics: '+this.topic._id+' from minutes '+this.minutesID);

        let aMin = new Minutes(this.minutesID);

        let topic = new Topic(this.minutesID, this.topic);
        const deleteAllowed = topic.isDeleteAllowed();

        if (!topic.isFinallyCompleted() || deleteAllowed) {
            ConfirmationDialogFactory.makeWarningDialogWithTemplate(
                () => {
                    if (deleteAllowed) {
                        aMin.removeTopic(this.topic._id).catch(handleError);
                    } else {
                        topic.closeTopicAndAllOpenActionItems().catch(handleError);
                    }
                },
                deleteAllowed ? i18n.__('Dialog.ConfirmTopicDelete.title1') : i18n.__('Dialog.ConfirmTopicDelete.title2'),
                'confirmDeleteTopic',
                {
                    deleteAllowed: topic.isDeleteAllowed(),
                    hasOpenActionItems: topic.hasOpenActionItem(),
                    subject: topic.getSubject()
                },
                deleteAllowed ? i18n.__('Buttons.delete') : i18n.__('Dialog.ConfirmTopicDelete.button2')
            ).show();
        } else {
            ConfirmationDialogFactory.makeInfoDialog(
                i18n.__('Dialog.ConfirmTopicDelete.errortitle'),
                i18n.__('Dialog.ConfirmTopicDelete.errorcontent')
            ).show();
        }
    },

    'click .btnToggleState'(evt) {
        editTopicEventHandler(evt, this, (aTopic) => {
            aTopic.toggleState().catch(handleError);
        });
    },

    'click #btnShowTopic'() {
        FlowRouter.go('/topic/'+this.topic._id);
    },

    'click .js-toggle-recurring'(evt) {
        editTopicEventHandler(evt, this, (aTopic) => {
            aTopic.toggleRecurring();
            aTopic.save().catch(handleError);
        });
    },
    
    'click .js-toggle-skipped'(evt) {
        editTopicEventHandler(evt, this, (aTopic) => {
            aTopic.toggleSkip();
            aTopic.save().catch(handleError);
        });
    },

    'click #btnEditTopic'(evt) {
        evt.preventDefault();
        if (!this.minutesID || getSelection().toString()) { // don't fire while selection is ongoing
            return;
        }
        Session.set('topicEditTopicId', this.topic._id);
        $('#dlgAddTopic').modal('show');
    },

    'click .addTopicInfoItem'(evt) {
        evt.preventDefault();
        // will be called before the modal dialog is shown
        openAddItemDialog('infoItem', this.topic._id);
    },

    'click .addTopicActionItem'(evt) {
        evt.preventDefault();
        // will be called before the modal dialog is shown
        openAddItemDialog('actionItem', this.topic._id);
    },

    'blur .addItemForm' (evt, tmpl) {
        if (!tmpl.data.isEditable) {
            throw new Meteor.Error('illegal-state', 'Tried to call an illegal event in read-only mode');
        }

        const theItemTextarea = tmpl.find('.add-item-field');
        const inputText = theItemTextarea.value;

        if (inputText === '') {
            return;
        }

        savingNewItem = true;
        const splitIndex = inputText.indexOf('\n');
        const subject = (splitIndex === -1) ? inputText : inputText.substring(0, splitIndex);
        const detail = (splitIndex === -1) ? '' : inputText.substring(splitIndex + 1).trim();

        const itemDoc = {
            subject: subject,
            responsibles: [],
            createdInMinute: this.minutesID
        };

        const topic = new Topic(this.minutesID, this.topic);
        const minutes = new Minutes(this.minutesID);
        const newItem = detectTypeAndCreateItem(itemDoc, topic, this.minutesID, minutes.parentMeetingSeries());
        if (detail) {
            newItem.addDetails(this.minutesID, detail);
        }
        newItem.saveAtBottom().catch(error => {
            theItemTextarea.value = inputText; // set desired value again!
            handleError(error);
        });

        let collapseState = Session.get('minutesedit.collapsetopics.'+_minutesId);
        if (!collapseState) {
            collapseState = {};
        }
        collapseState[this.topic._id] = false;
        Session.set('minutesedit.collapsetopics.'+_minutesId, collapseState);

        // Clean & focus for next usage after saving last item
        theItemTextarea.value = '';
        resizeTextarea(theItemTextarea);
        Meteor.setTimeout(() => {
            theItemTextarea.focus();
        },100);
    },

    'keydown .addItemForm' (evt, tmpl) {
        const inputEl = tmpl.$('.add-item-field');
        if (evt.which === 13/*enter*/ && ( evt.ctrlKey || evt.metaKey)) {
            evt.preventDefault();
            inputEl.blur();
        }

        resizeTextarea(inputEl);
    },

    'keydown #btnTopicExpandCollapse'(evt) {
        evt.preventDefault();
        // since we do not have a link-href the link will not be clicked when hitting enter by default...
        if (evt.which === 13/*enter*/) {
            evt.currentTarget.click();
        }
    },

    'click #btnTopicExpandCollapse'(evt) {
        console.log('btnTopicExpandCollapse()'+this.topic._id);
        evt.preventDefault();
        let collapseState = Session.get('minutesedit.collapsetopics.'+_minutesId);
        if (!collapseState) {
            collapseState = {};
        }
        collapseState[this.topic._id] = ! collapseState[this.topic._id];
        Session.set('minutesedit.collapsetopics.'+_minutesId, collapseState);
    },
    
    'click #btnReopenTopic'(evt) {
        evt.preventDefault();
        let reopenTopic = () => {
            Meteor.call('workflow.reopenTopicFromMeetingSeries', this.parentMeetingSeriesId, this.topic._id);
        };
        ConfirmationDialogFactory.makeSuccessDialog(
            reopenTopic,
            i18n.__('Dialog.ConfirmReOpenTopic.title'),
            i18n.__('Dialog.ConfirmReOpenTopic.body', {
                topicSubject: Template.instance().data.topic.subject
            }),
            {},
            i18n.__('Dialog.ConfirmReOpenTopic.button')
        ).show(); 
    }
});