mirror of
https://github.com/flarum/core.git
synced 2025-06-08 15:44:57 +02:00
300 lines
9.5 KiB
JavaScript
300 lines
9.5 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(false, 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;
|
||
|
||
if (this.position() !== Composer.PositionEnum.HIDDEN) {
|
||
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(true, anchorToBottom);
|
||
} 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(animate, anchorToBottom) {
|
||
var func = animate ? 'animate' : 'css';
|
||
var paddingBottom = this.position() !== Composer.PositionEnum.HIDDEN ? this.computedHeight() - parseInt($('#page').css('padding-bottom')) : 0;
|
||
$('#content')[func]({paddingBottom}, 'fast');
|
||
|
||
if (anchorToBottom) {
|
||
if (animate) {
|
||
$('html, body').stop(true).animate({scrollTop: $(document).height()}, 'fast');
|
||
} else {
|
||
$('html, body').scrollTop($(document).height());
|
||
}
|
||
}
|
||
}
|
||
|
||
// 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;
|