import Component from 'flarum/component'; import ItemList from 'flarum/utils/item-list'; import ActionButton from 'flarum/components/action-button'; import icon from 'flarum/helpers/icon'; import listItems from 'flarum/helpers/list-items'; import classList from 'flarum/utils/class-list'; import computed from 'flarum/utils/computed'; class Composer extends Component { constructor(props) { super(props); this.position = m.prop(Composer.PositionEnum.HIDDEN); this.height = m.prop(); // Calculate the composer's current height, based on the intended height // (which is set when the resizing handle is dragged), and the composer's // current state. this.computedHeight = computed('height', 'position', function(height, position) { if (position === Composer.PositionEnum.MINIMIZED) { return ''; } else if (position === Composer.PositionEnum.FULLSCREEN) { return $(window).height(); } else { return Math.max(200, Math.min(height, $(window).height() - $('#header').outerHeight())); } }); } view() { var classes = { 'minimized': this.position() === Composer.PositionEnum.MINIMIZED, 'full-screen': this.position() === Composer.PositionEnum.FULLSCREEN }; classes.visible = this.position() === Composer.PositionEnum.NORMAL || classes.minimized || classes.fullScreen; if (this.component) this.component.props.disabled = classes.minimized; return m('div.composer', {config: this.onload.bind(this), className: classList(classes)}, [ m('div.composer-handle', {config: this.configHandle.bind(this)}), m('ul.composer-controls', listItems(this.controlItems().toArray())), m('div.composer-content', {onclick: () => { if (this.position() === Composer.PositionEnum.MINIMIZED) this.show(); }}, this.component ? this.component.view() : '') ]); } onload(element, isInitialized, context) { this.element(element); if (isInitialized) { return; } context.retain = true; // Hide the composer to begin with. this.height(localStorage.getItem('composerHeight') || this.$().height()); this.$().hide(); // Modulate the view's active property/class according to the focus // state of any inputs. this.$().on('focus blur', ':input', (e) => this.$().toggleClass('active', e.type === 'focusin')); // When the escape key is pressed on any inputs, close the composer. this.$().on('keydown', ':input', 'esc', () => this.close()); context.onunload = this.ondestroy.bind(this); this.handlers = {}; $(window).on('resize', this.handlers.onresize = this.onresize.bind(this)).resize(); $(document) .on('mousemove', this.handlers.onmousemove = this.onmousemove.bind(this)) .on('mouseup', this.handlers.onmouseup = this.onmouseup.bind(this)); } configHandle(element, isInitialized) { if (isInitialized) { return; } var self = this; $(element).css('cursor', 'row-resize') .mousedown(function(e) { self.mouseStart = e.clientY; self.heightStart = self.$().height(); self.handle = $(this); $('body').css('cursor', 'row-resize'); }).bind('dragstart mousedown', function(e) { e.preventDefault(); }); } ondestroy() { $(window).off('resize', this.handlers.onresize); $(document) .off('mousemove', this.handlers.onmousemove) .off('mouseup', this.handlers.onmouseup); } updateHeight() { this.$().height(this.computedHeight()); this.setContentHeight(this.computedHeight()); } onresize() { this.updateHeight(); } onmousemove(e) { if (!this.handle) { return; } // Work out how much the mouse has been moved, and set the height // relative to the old one based on that. Then update the content's // height so that it fills the height of the composer, and update the // body's padding. var deltaPixels = this.mouseStart - e.clientY; var height = this.heightStart + deltaPixels; this.height(height); this.updateHeight(); var scrollTop = $(window).scrollTop(); this.updateBodyPadding(scrollTop > 0 && scrollTop + $(window).height() >= $(document).height()); localStorage.setItem('composerHeight', height); } onmouseup(e) { if (!this.handle) { return; } this.handle = null; $('body').css('cursor', ''); } preventExit() { return this.component && this.component.preventExit(); } render(anchorToBottom) { var $composer = this.$().stop(true); var oldHeight = $composer.is(':visible') ? $composer.outerHeight() : 0; var scrollTop = $(window).scrollTop(); m.redraw(true); this.$().height(this.computedHeight()); var newHeight = $composer.outerHeight(); switch (this.position()) { case Composer.PositionEnum.HIDDEN: $composer.css({height: oldHeight}).animate({bottom: -newHeight}, 'fast', () => { $composer.hide(); this.clear(); m.redraw(); }); break; case Composer.PositionEnum.NORMAL: if (this.oldPosition !== Composer.PositionEnum.FULLSCREEN) { $composer.show(); $composer.css({height: oldHeight}).animate({bottom: 0, height: newHeight}, 'fast', this.component.focus.bind(this.component)); } else { this.component.focus(); } break; case Composer.PositionEnum.MINIMIZED: $composer.css({height: oldHeight}).animate({height: newHeight}, 'fast', this.component.focus.bind(this.component)); break; } if (this.position() !== Composer.PositionEnum.FULLSCREEN) { this.updateBodyPadding(); $('html, body').scrollTop(scrollTop); } else { this.component.focus(); } $('body').toggleClass('composer-open', this.position() !== Composer.PositionEnum.HIDDEN); this.oldPosition = this.position(); if (this.position() !== Composer.PositionEnum.HIDDEN) { this.setContentHeight(this.computedHeight()); } } // Update the amount of padding-bottom on the body so that the page's // content will still be visible above the composer when the page is // scrolled right to the bottom. updateBodyPadding() { var paddingBottom = this.position() !== Composer.PositionEnum.HIDDEN && this.position() !== Composer.PositionEnum.MINIMIZED ? this.computedHeight() - parseInt($('#page').css('padding-bottom')) : 0; $('#content').css({paddingBottom}); } // Update the height of the stuff inside of the composer. There should be // an element with the class .flexible-height — this element is intended // to fill up the height of the composer, minus the space taken up by the // composer's header/footer/etc. setContentHeight(height) { var flexible = this.$('.flexible-height'); if (flexible.length) { flexible.height(height - (flexible.offset().top - this.$().offset().top) - this.$('.text-editor-controls').outerHeight(true)); } } load(component) { if (!this.preventExit()) { // If we load a similar component into the composer, then Mithril will be // able to diff the old/new contents and some DOM-related state from the // old composer will remain. To prevent this from happening, we clear the // component and force a redraw, so that the new component will be working // on a blank slate. if (this.component) { this.clear(); m.redraw(true); } this.component = component; } } clear() { this.component = null; } show(anchorToBottom) { if ([Composer.PositionEnum.MINIMIZED, Composer.PositionEnum.HIDDEN].indexOf(this.position()) !== -1) { this.position(Composer.PositionEnum.NORMAL); } this.render(anchorToBottom); } hide() { this.position(Composer.PositionEnum.HIDDEN); this.render(); } close() { if (!this.preventExit()) { this.hide(); } } minimize() { if (this.position() !== Composer.PositionEnum.HIDDEN) { this.position(Composer.PositionEnum.MINIMIZED); this.render(); } } fullScreen() { if (this.position() !== Composer.PositionEnum.HIDDEN) { this.position(Composer.PositionEnum.FULLSCREEN); this.render(); } } exitFullScreen() { if (this.position() === Composer.PositionEnum.FULLSCREEN) { this.position(Composer.PositionEnum.NORMAL); this.render(); } } control(props) { props.className = 'btn btn-icon btn-link'; return ActionButton.component(props); } controlItems() { var items = new ItemList(); if (this.position() === Composer.PositionEnum.FULLSCREEN) { items.add('exitFullScreen', this.control({ icon: 'compress', title: 'Exit Full Screen', onclick: this.exitFullScreen.bind(this) })); } else { if (this.position() !== Composer.PositionEnum.MINIMIZED) { items.add('minimize', this.control({ icon: 'minus minimize', title: 'Minimize', onclick: this.minimize.bind(this) })); items.add('fullScreen', this.control({ icon: 'expand', title: 'Full Screen', onclick: this.fullScreen.bind(this) })); } items.add('close', this.control({ icon: 'times', title: 'Close', onclick: this.close.bind(this) })); } return items; } } Composer.PositionEnum = { HIDDEN: 'hidden', NORMAL: 'normal', MINIMIZED: 'minimized', FULLSCREEN: 'fullScreen' }; export default Composer;