438 lines
11 KiB
JavaScript
438 lines
11 KiB
JavaScript
import {
|
|
WidgetClickHook,
|
|
WidgetDoubleClickHook,
|
|
WidgetClickOutsideHook,
|
|
WidgetKeyUpHook,
|
|
WidgetKeyDownHook,
|
|
WidgetMouseDownOutsideHook,
|
|
WidgetDragHook,
|
|
WidgetInputHook,
|
|
WidgetChangeHook,
|
|
WidgetMouseUpHook,
|
|
WidgetMouseDownHook,
|
|
WidgetMouseMoveHook
|
|
} from "discourse/widgets/hooks";
|
|
import { h } from "virtual-dom";
|
|
import DecoratorHelper from "discourse/widgets/decorator-helper";
|
|
import { Promise } from "rsvp";
|
|
import ENV from "discourse-common/config/environment";
|
|
import { get } from "@ember/object";
|
|
|
|
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");
|
|
|
|
this.init(this.attrs);
|
|
|
|
// Helps debug widgets
|
|
if (Discourse.Environment === "development" || ENV.environment === "test") {
|
|
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 {};
|
|
}
|
|
|
|
init() {}
|
|
|
|
destroy() {}
|
|
|
|
get(propertyPath) {
|
|
return get(this, propertyPath);
|
|
}
|
|
|
|
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 = Promise.resolve();
|
|
} else {
|
|
const target = view.get("target") || view;
|
|
promise = method.call(target, param);
|
|
if (!promise || !promise.then) {
|
|
promise = Promise.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.doubleClick) {
|
|
properties["widget-double-click"] = new WidgetDoubleClickHook(this);
|
|
}
|
|
|
|
if (this.mouseDownOutside) {
|
|
properties["widget-mouse-down-outside"] = new WidgetMouseDownOutsideHook(
|
|
this
|
|
);
|
|
}
|
|
|
|
if (this.drag) {
|
|
properties["widget-drag"] = new WidgetDragHook(this);
|
|
}
|
|
|
|
if (this.input) {
|
|
properties["widget-input"] = new WidgetInputHook(this);
|
|
}
|
|
|
|
if (this.change) {
|
|
properties["widget-change"] = new WidgetChangeHook(this);
|
|
}
|
|
|
|
if (this.mouseDown) {
|
|
properties["widget-mouse-down"] = new WidgetMouseDownHook(this);
|
|
}
|
|
|
|
if (this.mouseUp) {
|
|
properties["widget-mouse-up"] = new WidgetMouseUpHook(this);
|
|
}
|
|
|
|
if (this.mouseMove) {
|
|
properties["widget-mouse-move"] = new WidgetMouseMoveHook(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";
|