alchemy-widget
Version:
The widget plugin for the AlchemyMVC
1,056 lines (820 loc) • 21.6 kB
JavaScript
/**
* The Base Widget class
*
* @constructor
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.0
* @version 0.2.1
*
* @param {Object} config
*/
const Widget = Function.inherits('Alchemy.Base', 'Alchemy.Widget', function Widget(config) {
// The configuration of this widget
if (config) {
this.config = config;
}
this.originalconfig = this.config;
// Are we currently editing?
this.editing = false;
// The parent instance
this.parent_instance = null;
});
/**
* Make this an abstract class
*/
Widget.makeAbstractClass();
/**
* This class starts a new group
*/
Widget.startNewGroup('widgets');
/**
* Return the class-wide schema
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.0
* @version 0.1.0
*
* @type {Schema}
*/
Widget.setProperty(function schema() {
return this.constructor.schema;
});
/**
* Get the actual widget
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.0
* @version 0.1.0
*
* @type {HTMLElement}
*/
Widget.enforceProperty(function widget(new_value, old_value) {
return new_value;
});
/**
* A reference to the element
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.0
* @version 0.1.0
*
* @type {HTMLElement}
*/
Widget.setProperty(function element() {
return this.widget;
}, function setElement(element) {
return this.widget = element;
});
/**
* The config object
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.1
* @version 0.2.1
*
* @type {Object}
*/
Widget.enforceProperty(function config(new_value) {
if (!new_value) {
new_value = {};
}
return new_value;
});
/**
* A reference to the renderer
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.0
* @version 0.1.6
*
* @type {Hawkejs.Renderer}
*/
Widget.enforceProperty(function hawkejs_renderer(new_value) {
if (!new_value) {
if (this.parent_instance && this.parent_instance.hawkejs_renderer) {
new_value = this.parent_instance.hawkejs_renderer;
} else if (this.widget && this.widget.hawkejs_renderer) {
new_value = this.widget.hawkejs_renderer;
} else if (Blast.isBrowser) {
new_value = hawkejs.scene.general_renderer;
}
}
return new_value;
});
/**
* Visibility of the widget
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.1
* @version 0.2.1
*
* @type {Boolean}
*/
Widget.enforceProperty(function is_hidden(new_value) {
if (new_value == null) {
new_value = this.config.hidden;
if (new_value == null) {
new_value = false;
}
} else {
this.config.hidden = new_value;
this.queueVisibilityUpdate();
}
return new_value;
});
/**
* Prepare the schema & actions
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.0
* @version 0.2.5
*/
Widget.constitute(function prepareSchema() {
// Create the actions
this.actions = new Deck();
// Create the schema
this.schema = this.createSchema();
// Extra classnames for the wrapper
this.schema.addField('wrapper_class_names', 'String', {
title : 'Wrapper CSS classes',
description : 'Configure extra CSS classes to the wrapper `al-widget` element',
array: true,
widget_config_editable: true,
});
// Classnames for the inserted element (if any)
this.schema.addField('main_class_names', 'String', {
title : 'Main CSS classes',
description : 'Configure extra CSS classes for the main inserted element',
array: true,
});
// Should this widget be hidden?
this.schema.addField('hidden', 'Boolean', {
title : 'Should this widget be hidden?',
description : 'Hidden widgets are only visible during editing',
default : false,
});
this.schema.addField('language', 'String', {
title : 'Language override',
description : 'If the content of this widget is in a different language, set it here',
widget_config_editable : true,
});
// Add the "copy to clipboard" action
let copy = this.createAction('copy', 'Copy to clipboard');
copy.setHandler(function copyAction(widget_el, handle) {
return widget_el.copyConfigToClipboard();
});
copy.setTester(function copyAction(widget_el, handle) {
return true;
});
copy.setIcon('copy');
copy.close_actionbar = true;
// Add the "replace from clipboard" action
let replace = this.createAction('replace', 'Replace from clipboard');
replace.setHandler(async function replaceAction(widget_el, handle) {
let config = await widget_el.getReplaceableConfigFromClipboard();
if (!config) {
return false;
}
widget_el.replaceWithConfig(config);
});
replace.setTester(function replaceActionTester(widget_el, handle) {
return widget_el.getReplaceableConfigFromClipboard();
});
replace.setIcon('repeat');
replace.close_actionbar = true;
// Add the "paste from clipboard" action
let paste = this.createAction('paste', 'Paste from clipboard');
paste.setHandler(async function pasteAction(widget_el, handle) {
let config = await widget_el.getConfigFromClipboard();
if (!config) {
return false;
}
widget_el.addWidget(config.type, config.config);
});
paste.setTester(async function pasteActionTester(widget_el, handle) {
if (!widget_el.addWidget) {
return false;
}
let config = await widget_el.getConfigFromClipboard();
if (!config || config.type == 'container') {
return false;
}
return true;
});
paste.setIcon('paste');
paste.close_actionbar = true;
// Add the "save" action
let save = this.createAction('save', 'Save');
save.setHandler(function removeAction(widget_el, handle) {
return widget_el.save();
});
save.setTester(function saveAction(widget_el, handle) {
return widget_el.can_be_saved;
});
save.setIcon('floppy-disk');
// Add the hide action
let hide = this.createAction('hide', 'Hide');
hide.close_actionbar = true;
hide.setHandler(function hideAction(widget_el, handle) {
widget_el.instance.is_hidden = true;
});
hide.setTester(function hideTester(widget_el, handle) {
return !widget_el.instance.is_hidden;
});
hide.setIcon('eye-slash');
// Add the show action
let show = this.createAction('show', 'Show');
show.close_actionbar = true;
show.setHandler(function showAction(widget_el, handle) {
widget_el.instance.is_hidden = false;
});
show.setTester(function showTester(widget_el, handle) {
return widget_el.instance.is_hidden;
});
show.setIcon('eye');
// Add the remove action
let remove = this.createAction('remove', 'Remove');
remove.close_actionbar = true;
remove.setHandler(function removeAction(widget_el, handle) {
handle.remove();
});
remove.setTester(function removeTester(widget_el, handle) {
return widget_el.can_be_removed;
});
remove.setIcon('trash');
// Add the config action
let config = this.createAction('config', 'Config');
config.close_actionbar = true;
config.setIcon('gears');
config.setHandler(function configAction(widget_el, handle) {
widget_el.instance.showConfig();
});
// The move-left action
let move_left = this.createAction('move-left', 'Move left');
move_left.close_actionbar = true;
move_left.setHandler(function moveLeftAction(widget_el, handle) {
// Hawkejs custom element method!
handle.moveBeforeElement(handle.previousElementSibling);
});
move_left.setTester(function moveLeftTest(widget_el, handle) {
if (!widget_el.can_be_moved) {
return false;
}
return !!handle.previousElementSibling;
});
move_left.setIcon('arrow-left');
let move_right = this.createAction('move-right', 'Move right');
move_right.close_actionbar = true;
move_right.setHandler(function moveRightAction(widget_el, handle) {
// Hawkejs custom element method!
handle.moveAfterElement(handle.nextElementSibling);
});
move_right.setTester(function moveRightTest(widget_el, handle) {
if (!widget_el.can_be_moved) {
return false;
}
let next = handle.nextElementSibling;
if (!next || next.tagName == 'AL-WIDGET-ADD-AREA') {
return false;
}
return true;
});
move_right.setIcon('arrow-right');
// The move-in-left action
let move_in_left = this.createAction('move-in-left', 'Move in left');
move_in_left.close_actionbar = true;
move_in_left.setHandler(function moveLeftAction(widget_el, handle) {
// Hawkejs custom element method!
let container = handle.previous_container;
if (container) {
container.append(handle);
}
});
move_in_left.setTester(function moveLeftTest(widget_el, handle) {
if (!widget_el.can_be_moved) {
return false;
}
return !!handle.previous_container;
});
move_in_left.setIcon('arrow-left');
// The move-in-right action
let move_in_right = this.createAction('move-in-right', 'Move in right');
move_in_right.close_actionbar = true;
move_in_right.setHandler(function moveRightAction(widget_el, handle) {
// Hawkejs custom element method!
let container = handle.next_container;
if (container) {
container.prepend(handle);
}
});
move_in_right.setTester(function moveRightTest(widget_el, handle) {
if (!widget_el.can_be_moved) {
return false;
}
return !!handle.next_container;
});
move_in_right.setIcon('arrow-right');
let css_class = this.createAction('css-class', 'CSS Class');
css_class.setHandler(function setCssClass(widget_el, handle) {
widget_el.instance.showConfig(['main_class_names']);
});
css_class.setIcon('tags');
});
/**
* Create a schema
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.0
* @version 0.1.0
*/
Widget.setStatic(function createSchema() {
let schema;
if (Blast.isNode) {
// Create the schema
schema = new Classes.Alchemy.Schema();
} else {
schema = new Classes.Alchemy.Client.Schema();
}
return schema;
});
/**
* Set an action
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.0
* @version 0.1.4
*/
Widget.setStatic(function createAction(name, title) {
let action = new Classes.Alchemy.Widget.Action(name, title || name.titleize());
this.actions.set(name, action);
return action;
});
/**
* Add a check to see if the widget can be added to the current location
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.6
* @version 0.1.6
*
* @param {Boolean|Function} checker
*/
Widget.setStatic(function setAddChecker(checker) {
this.add_checker = checker;
});
/**
* Actually perform the add-check
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.6
* @version 0.1.6
*
* @param {Alchemy.Element.Widget.Base} widget_element
*/
Widget.setStatic(function canBeAdded(widget_element) {
if (this.add_checker != null) {
const type = typeof this.add_checker;
if (type == 'function') {
return this.add_checker(widget_element);
} else if (type == 'boolean') {
return this.add_checker;
}
}
return true;
});
/**
* unDry an object
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.0
* @version 0.1.0
*
* @param {Object} obj
*
* @return {Alchemy.Widget.Widget}
*/
Widget.setStatic(function unDry(obj, custom_method, whenDone) {
let widget = new this(obj.config);
whenDone(() => {
widget.widget = obj.element;
widget.parent_instance = obj.parent;
});
return widget;
});
/**
* Create a child widget instance
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.0
* @version 0.2.6
*
* @param {String} type The typeof widget to create
* @param {Object} config The optional config object
*
* @return {Widget}
*/
Widget.setMethod(function createChildWidget(type, config) {
let WidgetClass = Widget.getMember(type);
if (!WidgetClass) {
throw new Error('Unable to find Widget of type "' + type + '"');
}
// Create the instance
let instance = new WidgetClass(config);
if (this.conduit) {
instance.conduit = this.conduit;
}
// Set the parent instance!
instance.parent_instance = this;
// Create the actual element
let el = instance._createWidgetElement();
// And attach it
instance.element = el;
return instance;
});
/**
* Return an object for json-drying this widget
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.0
* @version 0.1.0
*
* @return {Object}
*/
Widget.setMethod(function toDry() {
return {
value: {
config : this.config,
element : this.widget,
parent : this.parent_instance,
}
};
});
/**
* Get an array of actionbar actions
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.0
* @version 0.2.6
*
* @return {Array}
*/
Widget.setMethod(async function getActionbarActions() {
if (!this.constructor.actions) {
return [];
}
let sorted = this.constructor.actions.getSorted(),
result = [],
action;
for (action of sorted) {
let tester = action.test(this.widget);
if (Pledge.isThenable(tester)) {
tester = await tester;
}
if (tester) {
result.push(action);
}
}
return result;
});
/**
* Create an HTML element of the wanted type
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.0
* @version 0.1.0
*
* @param {String} tag_name
*
* @return {HTMLElement}
*/
Widget.setMethod(function createElement(tag_name) {
let element;
if (this.widget) {
// Use the widget to create an element,
// it might contain a hawkejs_renderer
element = this.widget.createElement(tag_name);
} else if (this.hawkejs_renderer) {
element = this.hawkejs_renderer.createElement(tag_name);
} else if (this.parent_instance) {
return this.parent_instance.createElement(tag_name);
} else {
element = Classes.Hawkejs.Hawkejs.createElement(tag_name);
}
return element;
});
/**
* Create an instance of the HTML element representing this widget
* This will probably be just <al-widget>
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.0
* @version 0.2.0
*
* @return {HTMLElement}
*/
Widget.setMethod(function _createWidgetElement() {
let element = this.createElement('al-widget');
// Set the type of the widget to our type
element.type = this.constructor.type_name;
// Attach this instance
element.instance = this;
return element;
});
/**
* Get the widget HTML element with assigned data
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.0
* @version 0.2.2
*
* @return {HTMLElement}
*/
Widget.setMethod(function _createPopulatedWidgetElement() {
// Create the wrapper widget elemend
let element = this._createWidgetElement();
// Attach this instance
element.instance = this;
this.widget = element;
this.loadWidget();
return element;
});
/**
* Load the widget element: populate it & finalize it
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.2
* @version 0.2.2
*
* @return {Promise|null}
*/
Widget.setMethod(function loadWidget() {
let populate = this.populateWidget();
if (Pledge.isThenable(populate)) {
let pledge = new Pledge();
Pledge.done(populate, (err, result) => {
if (err) {
pledge.reject(err);
}
pledge.resolve(this.finalizePopulatedWidget());
});
return pledge;
} else {
return this.finalizePopulatedWidget();
}
});
/**
* Populate the actual widget
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.1
* @version 0.2.1
*/
Widget.setMethod(function populateWidget() {
// Does nothing on its own
});
/**
* Populate the contents of the widget
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.0
* @version 0.2.1
*/
Widget.setMethod(function finalizePopulatedWidget() {
const config = this.config;
if (config.wrapper_class_names) {
let name,
i;
let class_names = Array.cast(config.wrapper_class_names);
for (i = 0; i < class_names.length; i++) {
name = class_names[i];
this.widget.classList.add(name);
}
}
if (config.language) {
this.widget.setAttribute('lang', config.language);
} else {
this.widget.removeAttribute('lang');
}
let child_classes = this.widget.child_class;
if (child_classes) {
let children = this.widget.children,
i;
for (i = 0; i < children.length; i++) {
Hawkejs.addClasses(children[i], child_classes);
}
}
let child_roles = this.widget.child_role;
if (child_roles) {
let children = this.widget.children,
child,
i;
for (i = 0; i < children.length; i++) {
child = children[i];
if (!child.hasAttribute('role')) {
child.setAttribute('role', child_roles);
}
}
}
this.checkVisibility();
});
/**
* Queue a widget element visibility update
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.1
* @version 0.2.1
*/
Widget.setMethod(function queueVisibilityUpdate() {
if (this._visibility_update_queue) {
clearTimeout(this._visibility_update_queue);
}
this._visibility_update_queue = setTimeout(() => {
this.checkVisibility();
this._visibility_update_queue = null;
}, 50);
});
/**
* Check the widget element visibility
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.1
* @version 0.2.7
*/
Widget.setMethod(function checkVisibility() {
let should_be_hidden = this.is_hidden;
if (this.editing) {
this.widget.hideForEveryone(false);
} else {
this.widget.hideForEveryone(should_be_hidden);
}
if (should_be_hidden) {
this.widget.classList.add('aw-hidden');
} else {
this.widget.classList.remove('aw-hidden');
}
});
/**
* Start the editor
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.0
* @version 0.2.1
*/
Widget.setMethod(async function startEditor() {
// Show this is being edited
this.editing = true;
// Make sure the icon font is loaded
if (this.hawkejs_renderer?.helpers?.Media) {
this.hawkejs_renderer.helpers.Media.loadIconFont();
}
// Add the appropriate class to the current widget wrapper
this.widget.classList.add('aw-editing');
await this.widget.waitForTasks();
if (typeof this._startEditor == 'function') {
this._startEditor();
}
this.checkVisibility();
});
/**
* Stop the editor
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.0
* @version 0.2.1
*/
Widget.setMethod(function stopEditor() {
// Show this is not being edited anymore
this.editing = false;
// Remove the editing class
this.widget.classList.remove('aw-editing');
this.syncConfig();
if (typeof this._stopEditor == 'function') {
this._stopEditor();
// Remove the editing class again
// (some editors will try to restore the original classes)
this.widget.classList.remove('aw-editing');
}
this.checkVisibility();
});
/**
* Rerender this widget
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.0
* @version 0.2.2
*/
Widget.setMethod(async function rerender() {
Hawkejs.removeChildren(this.widget);
await this.loadWidget();
if (this.editing) {
this.startEditor();
}
});
/**
* Get the config of this widget
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.0
* @version 0.1.0
*
* @return {Object}
*/
Widget.setMethod(function syncConfig() {
return this.config;
});
/**
* Show the config
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.0
* @version 0.2.0
*/
Widget.setMethod(async function showConfig(fields) {
let field;
if (!fields) {
fields = [];
for (field of this.schema) {
if (!field.options.widget_config_editable) {
continue;
}
fields.push(field);
}
}
fields = fields.slice(0);
let i;
for (i = 0; i < fields.length; i++) {
field = fields[i];
if (typeof field == 'string') {
fields[i] = this.schema.get(field);
}
}
let widget_settings = Object.assign({}, this.syncConfig());
let variables = {
title : this.constructor.title,
schema : this.schema,
widget_settings,
fields : fields
};
await hawkejs.scene.render('widget/widget_config', variables);
let dialog_contents = document.querySelector('he-dialog [data-he-template="widget/widget_config"]');
if (!dialog_contents) {
return;
}
let dialog = dialog_contents.queryParents('he-dialog'),
button = dialog_contents.querySelector('.btn-apply');
dialog_contents.classList.add('default-form-editor');
hawkejs.scene.enableStyle('chimera/chimera');
button.addEventListener('click', e => {
e.preventDefault();
let form = dialog.querySelector('al-form');
Object.assign(this.config, form.value);
this.rerender();
dialog.remove();
});
});
/**
* Get the handle element of this widget
* (which is the widget itself by default)
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.0
* @version 0.1.0
*
* @return {HTMLElement}
*/
Widget.setMethod(function getHandle() {
let element = this.widget;
if (element.parent_container && typeof element.parent_container.getWidgetHandle == 'function') {
element = element.parent_container.getWidgetHandle(element);
}
return element;
});
/**
* See if the given value is considered not-empty for this widget
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.6
* @version 0.2.6
*
* @return {Boolean}
*/
Widget.setMethod(function valueHasContent(value) {
if (!value || typeof value != 'object') {
return false;
}
let entry,
key;
for (key in value) {
entry = value[key];
if (entry) {
if (Array.isArray(entry) && entry.length) {
return true;
}
if (entry === '') {
continue;
}
return true;
}
}
return false;
});