discourse/app/assets/javascripts/discourse/widgets/widget.js.es6
2016-07-05 11:25:08 -04:00

325 lines
8.2 KiB
JavaScript

import { WidgetClickHook, WidgetClickOutsideHook, WidgetKeyUpHook, WidgetKeyDownHook, WidgetDragHook } from 'discourse/widgets/hooks';
import { h } from 'virtual-dom';
import DecoratorHelper from 'discourse/widgets/decorator-helper';
function emptyContent() { }
const _registry = {};
let _dirty = {};
export function keyDirty(key, options) {
_dirty[key] = options || {};
}
export function renderedKey(key) {
delete _dirty[key];
}
export function queryRegistry(name) {
return _registry[name];
}
const _decorators = {};
export function decorateWidget(widgetName, cb) {
_decorators[widgetName] = _decorators[widgetName] || [];
_decorators[widgetName].push(cb);
}
export function applyDecorators(widget, type, attrs, state) {
const decorators = _decorators[`${widget.name}:${type}`] || [];
if (decorators.length) {
const helper = new DecoratorHelper(widget, attrs, state);
return decorators.map(d => d(helper));
}
return [];
}
const _customSettings = {};
export function changeSetting(widgetName, settingName, newValue) {
_customSettings[widgetName] = _customSettings[widgetName] || {};
_customSettings[widgetName][settingName] = newValue;
}
function drawWidget(builder, attrs, state) {
const properties = {};
if (this.buildClasses) {
let classes = this.buildClasses(attrs, state) || [];
if (!Array.isArray(classes)) { classes = [classes]; }
const customClasses = applyDecorators(this, 'classNames', attrs, state);
if (customClasses && customClasses.length) {
classes = classes.concat(customClasses);
}
if (classes.length) {
properties.className = classes.join(' ');
}
}
if (this.buildId) {
properties.id = this.buildId(attrs);
}
if (this.buildAttributes) {
properties.attributes = this.buildAttributes(attrs);
}
if (this.keyUp) {
properties['widget-key-up'] = new WidgetKeyUpHook(this);
}
if (this.keyDown) {
properties['widget-key-down'] = new WidgetKeyDownHook(this);
}
if (this.clickOutside) {
properties['widget-click-outside'] = new WidgetClickOutsideHook(this);
}
if (this.click) {
properties['widget-click'] = new WidgetClickHook(this);
}
if (this.drag) {
properties['widget-drag'] = new WidgetDragHook(this);
}
const attributes = properties['attributes'] || {};
properties.attributes = attributes;
if (this.title) {
if (typeof this.title === 'function') {
attributes.title = this.title(attrs, state);
} else {
attributes.title = I18n.t(this.title);
}
}
let contents = this.html(attrs, state);
if (this.name) {
const beforeContents = applyDecorators(this, 'before', attrs, state) || [];
const afterContents = applyDecorators(this, 'after', attrs, state) || [];
contents = beforeContents.concat(contents).concat(afterContents);
}
return h(this.tagName || 'div', properties, contents);
}
export function createWidget(name, opts) {
const result = class CustomWidget extends Widget {};
if (name) {
_registry[name] = result;
}
opts.name = name;
opts.html = opts.html || emptyContent;
opts.draw = drawWidget;
Object.keys(opts).forEach(k => result.prototype[k] = opts[k]);
return result;
}
export default class Widget {
constructor(attrs, container, opts) {
opts = opts || {};
this.attrs = attrs || {};
this.mergeState = opts.state;
this.container = container;
this.model = opts.model;
this.key = this.buildKey ? this.buildKey(attrs) : null;
// Helps debug widgets
if (Ember.testing) {
const ds = this.defaultState(attrs);
if (typeof ds !== "object") {
Ember.warn(`defaultState must return an object`);
} else if (Object.keys(ds).length > 0 && !this.key) {
Ember.warn(`you need a key when using state ${this.name}`);
}
}
this.site = container.lookup('site:main');
this.siteSettings = container.lookup('site-settings:main');
this.currentUser = container.lookup('current-user:main');
this.capabilities = container.lookup('capabilities:main');
this.store = container.lookup('store:main');
this.appEvents = container.lookup('app-events:main');
this.keyValueStore = container.lookup('key-value-store:main');
if (this.name) {
const custom = _customSettings[this.name];
if (custom) {
Object.keys(custom).forEach(k => this.settings[k] = custom[k]);
}
}
}
defaultState() {
return {};
}
destroy() {
console.log('destroy called');
}
render(prev) {
if (prev && prev.key && prev.key === this.key) {
this.state = prev.state;
} else {
this.state = this.defaultState(this.attrs, this.state);
}
// Sometimes we pass state down from the parent
if (this.mergeState) {
this.state = _.merge(this.state, this.mergeState);
}
if (prev) {
const dirtyOpts = _dirty[prev.key] || {};
if (prev.shadowTree) {
this.shadowTree = true;
if (!dirtyOpts && !_dirty['*']) {
return prev.vnode;
}
}
renderedKey(prev.key);
const refreshAction = dirtyOpts.onRefresh;
if (refreshAction) {
this.sendWidgetAction(refreshAction, dirtyOpts.refreshArg);
}
}
return this.draw(h, this.attrs, this.state);
}
_findAncestorWithProperty(property) {
let widget = this;
while (widget) {
const value = widget[property];
if (value) {
return widget;
}
widget = widget.parentWidget;
}
}
_findView() {
const widget = this._findAncestorWithProperty('_emberView');
if (widget) {
return widget._emberView;
}
}
attach(widgetName, attrs, opts) {
let WidgetClass = _registry[widgetName];
if (!WidgetClass) {
if (!this.container) {
console.error("couldn't find container");
return;
}
WidgetClass = this.container.lookupFactory(`widget:${widgetName}`);
}
if (WidgetClass) {
const result = new WidgetClass(attrs, this.container, opts);
result.parentWidget = this;
return result;
} else {
throw `Couldn't find ${widgetName} factory`;
}
}
scheduleRerender() {
let widget = this;
while (widget) {
if (widget.shadowTree) {
keyDirty(widget.key);
}
const emberView = widget._emberView;
if (emberView) {
return emberView.queueRerender();
}
widget = widget.parentWidget;
}
}
_sendComponentAction(name, param) {
const view = this._findAncestorWithProperty('_emberView');
let promise;
if (view) {
// Peek into ember internals to allow us to return promises from actions
const ev = view._emberView;
const target = ev.get('targetObject');
const actionName = ev.get(name);
if (!actionName) {
Ember.warn(`${name} not found`);
return;
}
if (target) {
// TODO: Use ember closure actions
const actions = target._actions || target.actionHooks || {};
const method = actions[actionName];
if (method) {
promise = method.call(target, param);
if (!promise || !promise.then) {
promise = Ember.RSVP.resolve(promise);
}
} else {
return ev.sendAction(name, param);
}
}
}
return this.rerenderResult(() => promise);
}
findAncestorModel() {
const modelWidget = this._findAncestorWithProperty('model');
if (modelWidget) {
return modelWidget.model;
}
}
rerenderResult(fn) {
this.scheduleRerender();
const result = fn();
// re-render after any promises complete, too!
if (result && result.then) {
return result.then(() => this.scheduleRerender());
}
return result;
}
sendWidgetEvent(name) {
const methodName = `${name}Event`;
return this.rerenderResult(() => {
const widget = this._findAncestorWithProperty(methodName);
if (widget) {
return widget[methodName]();
}
});
}
sendWidgetAction(name, param) {
return this.rerenderResult(() => {
const widget = this._findAncestorWithProperty(name);
if (widget) {
return widget[name](param);
}
return this._sendComponentAction(name, param || this.findAncestorModel());
});
}
}
Widget.prototype.type = 'Thunk';