alchemy-widget
Version:
The widget plugin for the AlchemyMVC
516 lines (419 loc) • 9.45 kB
JavaScript
/**
* The base widget element
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.0
* @version 0.1.0
*/
let Base = Function.inherits('Alchemy.Element', 'Alchemy.Element.Widget', 'Base');
/**
* The stylesheet to load for this element
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.0
* @version 0.1.0
*/
Base.setStylesheetFile('alchemy_widgets');
/**
* Set the custom element prefix
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.0
* @version 0.2.0
*/
Base.setStatic('custom_element_prefix', 'al');
/**
* Don't register this as a custom element
* The `false` argument makes sure child classes don't also set this property
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.0
* @version 0.2.0
*/
Base.makeAbstractClass();
/**
* The Widget class instance belonging to this element
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.0
* @version 0.1.0
*
* @type {Alchemy.Widget.Widget}
*/
Base.setAssignedProperty('instance');
/**
* The container this widget is in
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.0
* @version 0.1.4
*/
Base.setProperty(function parent_container() {
let container,
current = this.parentElement;
while (current) {
if (current.is_container) {
container = current;
break;
}
current = current.parentElement;
}
return container;
});
/**
* The next container across container boundaries
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.4
* @version 0.1.4
*/
Base.setProperty(function next_container() {
return this.getSiblingContainer('next');
});
/**
* The previous container across container boundaries
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.4
* @version 0.1.4
*/
Base.setProperty(function previous_container() {
return this.getSiblingContainer('previous');
});
/**
* Is this the root element?
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.5
* @version 0.1.5
*/
Base.setProperty(function is_root_widget() {
return !this.parent_container;
});
/**
* Can this widget be saved?
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.5
* @version 0.1.5
*/
Base.setProperty(function can_be_saved() {
if (!this.is_root_widget) {
return false;
}
if (!this.record || !this.field) {
return false;
}
return true;
});
/**
* Can this widget be removed?
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.6
* @version 0.1.6
*
* @type {Boolean}
*/
Base.setProperty(function can_be_removed() {
// Widgets without a "parent_instance" are mostly
// hardcoded in some template (like in a Partial widget)
if (!this.instance?.parent_instance) {
return false;
}
return true;
});
/**
* Can this widget be moved?
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.6
* @version 0.1.6
*
* @type {Boolean}
*/
Base.setProperty(function can_be_moved() {
// Widgets without a "parent_instance" are mostly
// hardcoded in some template (like in a Partial widget)
if (!this.instance?.parent_instance) {
return false;
}
return true;
});
/**
* Get a sibling container
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.4
* @version 0.1.4
*/
Base.setMethod(function getSiblingContainer(type) {
let property;
if (type == 'next') {
property = 'nextElementSibling';
} else if (type == 'previous') {
property = 'previousElementSibling';
} else {
return false;
}
if (!this[property] && !this.parent_container) {
return;
}
let next = this[property];
if (next) {
if (next.is_container) {
return next;
}
if (next.tagName != 'AL-WIDGET-ADD-AREA') {
return false;
}
}
next = this.parent_container[property];
if (next && next.is_container) {
return next;
}
return false;
});
/**
* Look for a context variable
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.0
* @version 0.1.0
*/
Base.setMethod(function getContextVariable(name) {
let current = this,
result;
// First try looking through the parentElement chain
while (current) {
if (current.context_variables) {
result = current.context_variables[name];
if (result != null) {
break;
}
}
current = current.parentElement;
}
if (result != null) {
return result;
}
current = this;
// Now try through the parent_instance way
while (current) {
if (current.context_variables) {
result = current.context_variables[name];
if (result != null) {
break;
}
}
if (!current.instance) {
current = current.parentElement;
continue;
}
current = current.instance.parent_instance;
if (current) {
current = current.widget;
}
}
return result;
});
/**
* Rerender the contents
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.0
* @version 0.1.0
*/
Base.setMethod(function rerender() {
if (this.instance) {
return this.instance.rerender();
}
});
/**
* Added to the DOM for the first time
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.0
* @version 0.1.6
*/
Base.setMethod(function introduced() {
if (this.hasAttribute('editing')) {
this.startEditor();
}
this.addEventListener('click', e => {
let is_editing = this.instance?.editing;
if (is_editing) {
let anchor = e.target.closest('a');
if (anchor) {
e.preventDefault();
}
}
});
});
/**
* Get the configuration used to save data
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.5
* @version 0.1.6
*/
Base.setMethod(function gatherSaveData() {
if (!this.is_root_widget) {
return;
}
if (!this.record || !this.field) {
return;
}
let result = {
model : this.record.$model_name,
pk : this.record.$pk,
field : this.field,
value : this.value,
filter_target : this.filter_target,
filter_value : this.filter_value,
value_path : this.value_path,
field_languages : this.record.$hold.translated_fields,
};
return result;
});
/**
* Save the current configuration
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.1.5
* @version 0.1.5
*/
Base.setMethod(async function save() {
let data = this.gatherSaveData();
if (!data) {
return;
}
let config = {
href : alchemy.routeUrl('AlchemyWidgets#save'),
post : {
widgets: [data]
}
};
let result = await alchemy.fetch(config);
});
/**
* Copy this widget's configuration to the clipboard
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.0
* @version 0.2.5
*/
Base.setMethod(async function copyConfigToClipboard() {
let value = this.value;
if (!value) {
return;
}
value._altype = 'widget';
if (this.type) {
value.type = this.type;
}
let dried = JSON.dry(value, null, '\t');
try {
await navigator.clipboard.writeText(dried);
} catch (err) {
console.error('Failed to copy:', err);
}
localStorage._copied_widget_config = dried;
});
/**
* Get any current configuration from the clipboard
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.0
* @version 0.2.5
*/
Base.setMethod(async function getConfigFromClipboard() {
let result;
try {
result = await navigator.clipboard.readText();
} catch (err) {
if (!localStorage._copied_widget_config) {
return false;
} else {
result = localStorage._copied_widget_config;
}
}
if (result) {
result = result.trim();
}
if (!result || result[0] != '{') {
return false;
}
try {
result = JSON.undry(result);
} catch (err) {
return false;
}
if (result._altype != 'widget') {
return false;
}
return result;
});
/**
* Get configuration from the clipboard and return it if it's valid
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.5
* @version 0.2.5
*/
Base.setMethod(async function getReplaceableConfigFromClipboard() {
let result = await this.getConfigFromClipboard();
if (result.type == this.type) {
return result;
}
if (result.type == 'container') {
return false;
}
if (!this.can_be_removed) {
return false;
}
let parent_widget = this.parent_container;
if (!parent_widget || !parent_widget.editing || !parent_widget.addWidget) {
return false;
}
return result;
});
/**
* Read the configuration from the clipboard & apply
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.0
* @version 0.2.5
*/
Base.setMethod(async function pasteConfigFromClipboard() {
let result = await this.getReplaceableConfigFromClipboard();
if (result) {
this.replaceWithConfig(config);
}
});
/**
* Apply the given config
*
* @author Jelle De Loecker <jelle@elevenways.be>
* @since 0.2.0
* @version 0.2.5
*/
Base.setMethod(async function replaceWithConfig(config) {
if (!config?.type) {
throw new Error('Unable to replace, config type is empty');
}
if (this.type == config.type) {
this.value = config;
return true;
}
let parent_widget = this.parent_container;
// If this isn't in an editable widget, it can't be replaced.
// (Might be a hard-coded widget type)
if (!parent_widget || !parent_widget.editing || !parent_widget.addWidget) {
return false;
}
let new_widget = parent_widget.addWidget(config.type, config.config);
if (new_widget) {
this.replaceWith(new_widget);
}
});