Files
framework/js/forum/src/components/composer.js
2015-06-18 12:53:19 +09:30

294 lines
9.2 KiB
JavaScript

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;