This repository has been archived on 2023-03-18. You can view files and clone it, but cannot push or open issues or pull requests.
osr-discourse-src/app/assets/javascripts/discourse/widgets/widget.js.es6
Robin Ward f5d391a48a
REFACTOR: Move app-events:main to service:app-events (#8152)
AppEvents was always a service object in disguise, so we should move it
to the correct place in the application. Doing this allows other service
objects to inject it easily without container access.

In the future we should also deprecate `this.appEvents` without an
explicit injection too.
2019-10-04 10:06:08 -04:00

398 lines
9.9 KiB
JavaScript

import {
WidgetClickHook,
WidgetClickOutsideHook,
WidgetKeyUpHook,
WidgetKeyDownHook,
WidgetMouseDownOutsideHook,
WidgetDragHook
} from "discourse/widgets/hooks";
import { h } from "virtual-dom";
import DecoratorHelper from "discourse/widgets/decorator-helper";
const _registry = {};
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 [];
}
export function resetDecorators() {
Object.keys(_decorators).forEach(key => delete _decorators[key]);
}
const _customSettings = {};
export function changeSetting(widgetName, settingName, newValue) {
_customSettings[widgetName] = _customSettings[widgetName] || {};
_customSettings[widgetName][settingName] = newValue;
}
export function createWidgetFrom(base, name, opts) {
const result = class CustomWidget extends base {};
if (name) {
_registry[name] = result;
}
opts.name = name;
if (opts.template) {
opts.html = opts.template;
}
Object.keys(opts).forEach(k => (result.prototype[k] = opts[k]));
return result;
}
export function createWidget(name, opts) {
return createWidgetFrom(Widget, name, opts);
}
export function reopenWidget(name, opts) {
let existing = _registry[name];
if (!existing) {
// eslint-disable-next-line no-console
console.error(`Could not find widget ${name} in registry`);
return;
}
if (opts.template) {
opts.html = opts.template;
}
Object.keys(opts).forEach(k => {
let old = existing.prototype[k];
if (old instanceof Function) {
// Add support for `this._super()` to reopened widgets if the prototype exists in the
// base object
existing.prototype[k] = function(...args) {
let ctx = Object.create(this);
ctx._super = (...superArgs) => old.apply(this, superArgs);
return opts[k].apply(ctx, args);
};
} else {
existing.prototype[k] = opts[k];
}
});
return existing;
}
export default class Widget {
constructor(attrs, register, opts) {
opts = opts || {};
this.attrs = attrs || {};
this.mergeState = opts.state;
this.model = opts.model;
this.register = register;
this.dirtyKeys = opts.dirtyKeys;
register.deprecateContainer(this);
this.key = this.buildKey ? this.buildKey(attrs) : null;
this.site = register.lookup("site:main");
this.siteSettings = register.lookup("site-settings:main");
this.currentUser = register.lookup("current-user:main");
this.capabilities = register.lookup("capabilities:main");
this.store = register.lookup("service:store");
this.appEvents = register.lookup("service:app-events");
this.keyValueStore = register.lookup("key-value-store:main");
// Helps debug widgets
if (Discourse.Environment === "development" || Ember.testing) {
const ds = this.defaultState(attrs);
if (typeof ds !== "object") {
throw new Error(`defaultState must return an object`);
} else if (Object.keys(ds).length > 0 && !this.key) {
throw new Error(`you need a key when using state in ${this.name}`);
}
}
if (this.name) {
const custom = _customSettings[this.name];
if (custom) {
Object.keys(custom).forEach(k => (this.settings[k] = custom[k]));
}
}
}
transform() {
return {};
}
defaultState() {
return {};
}
destroy() {}
render(prev) {
const { dirtyKeys } = this;
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 = dirtyKeys.optionsFor(prev.key);
if (prev.shadowTree) {
this.shadowTree = true;
if (!dirtyOpts.dirty && !dirtyKeys.allDirty()) {
return prev.vnode;
}
}
if (prev.key) {
dirtyKeys.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;
}
}
lookupWidgetClass(widgetName) {
let WidgetClass = _registry[widgetName];
if (WidgetClass) {
return WidgetClass;
}
if (!this.register) {
// eslint-disable-next-line no-console
console.error("couldn't find register");
return null;
}
WidgetClass = this.register.lookupFactory(`widget:${widgetName}`);
if (WidgetClass && WidgetClass.class) {
return WidgetClass.class;
}
return null;
}
attach(widgetName, attrs, opts, otherOpts = {}) {
let WidgetClass = this.lookupWidgetClass(widgetName);
if (!WidgetClass && otherOpts.fallbackWidgetName) {
WidgetClass = this.lookupWidgetClass(otherOpts.fallbackWidgetName);
}
if (WidgetClass) {
const result = new WidgetClass(attrs, this.register, opts);
result.parentWidget = this;
result.dirtyKeys = this.dirtyKeys;
return result;
} else {
throw new Error(
`Couldn't find ${widgetName} or fallback ${otherOpts.fallbackWidgetName}`
);
}
}
scheduleRerender() {
let widget = this;
while (widget) {
if (widget.shadowTree) {
this.dirtyKeys.keyDirty(widget.key);
}
const rerenderable = widget._rerenderable;
if (rerenderable) {
return rerenderable.queueRerender();
}
widget = widget.parentWidget;
}
}
_sendComponentAction(name, param) {
let promise;
const view = this._findView();
if (view) {
const method = view.get(name);
if (!method) {
// eslint-disable-next-line no-console
console.warn(`${name} not found`);
return;
}
if (typeof method === "string") {
view[method](param);
promise = Ember.RSVP.resolve();
} else {
const target = view.get("target") || view;
promise = method.call(target, param);
if (!promise || !promise.then) {
promise = Ember.RSVP.resolve(promise);
}
}
}
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, attrs) {
const methodName = `${name}Event`;
return this.rerenderResult(() => {
const widget = this._findAncestorWithProperty(methodName);
if (widget) {
return widget[methodName](attrs);
}
});
}
sendWidgetAction(name, param) {
return this.rerenderResult(() => {
const widget = this._findAncestorWithProperty(name);
if (widget) {
return widget[name].call(widget, param);
}
return this._sendComponentAction(name, param || this.findAncestorModel());
});
}
html() {}
draw(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.mouseDownOutside) {
properties["widget-mouse-down-outside"] = new WidgetMouseDownOutsideHook(
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);
}
}
this.transformed = this.transform(this.attrs, this.state);
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);
}
}
Widget.prototype.type = "Thunk";