mirror of
https://github.com/flarum/framework.git
synced 2025-05-22 22:59:57 +08:00
294 lines
9.2 KiB
JavaScript
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;
|