- Added to assets-package

This commit is contained in:
Dave M. 2023-10-09 19:52:43 -04:00
commit fa69a8281b
9 changed files with 1230 additions and 0 deletions

14
asset/webcomponent/app.js Normal file
View File

@ -0,0 +1,14 @@
import { ElementObserver } from "./element-observer.js";
import { UiTabs } from "./module/ui-tabs.js";
window.customElements.define("ui-tabs", UiTabs);
import { UiPopup } from "./module/ui-popup.js";
window.customElements.define("ui-popup", UiPopup);
import { UiTextarea } from "./module/ui-textarea.js";
window.customElements.define("ui-textarea", UiTextarea);
import { UiSelect } from "./module/ui-select.js";
window.customElements.define("ui-select", UiSelect);

View File

@ -0,0 +1,159 @@
/*
* @author Dave Mc Nicoll
* @version 1.0.0
*
* Analysis elements entering or quitting the DOM, triggering events whenever
* a matching element is found.
*
**/
import { Hourglass } from "./hourglass.js";
const config = {
resize: {
timer: 200
}
};
const ACTION_ADDED = 0,
ACTION_REMOVED = 1,
ACTION_RESIZED = 2,
MutationObserver = window.MutationObserver || window.WebKitMutationObserver;
class ElementObserver {
constructor(options) {
this.observer = null;
this.listeners = [ [] , [] ];
}
added(selector, callback) {
this._observe(selector, callback, ACTION_ADDED);
}
removed(selector, callback) {
this._observe(selector, callback, ACTION_REMOVED);
}
/* a little hacky function, awaiting the ResizeObserver specs to be ready */
resize(element, callback) {
var width = 0;
if ( ! this.hourglass ) {
element.dataset.element_width = element.getBoundingClientRect().width;
this.hourglass = new Hourglass(function() {
this.resizing.forEach(function(item) {
width = item.element.getBoundingClientRect().width;
if ( item.element.dataset !== width ) {
item.element.dataset.element_width = width;
item.callback.call(item.element, width);
}
});
}.bind(this), {
start : false,
repeat : false,
timer : config.resize.timer
});
window.addEventListener('resize', function() {
this.hourglass.restart();
}.bind(this));
}
this.resizing = this.resizing || [];
this.resizing.push({
element: element,
callback: callback
});
}
_observe(selector, callback, action) {
this.listeners[action].push({
selector : selector,
callback : callback
});
this._register_observer();
}
_analyze_element(element, action) {
let split_selector = [];
for (let j = 0, l1 = this.listeners[action].length; j < l1; j++) {
split_selector = this.listeners[action][j].selector.split(' ');
if ( split_selector.length > 1 ) {
console.log("@todo!", split_selector);
}
else {
if ( $element.is( this.listeners[action][j].selector ) ) {
this.listeners[action][j].callback.call(element, element);
}
}
}
}
_analyze_added_element() {
return this._analyze_element(this, ACTION_ADDED);
}
_analyze_removed_element() {
return this._analyze_element(this, ACTION_REMOVED);
}
_observe_element(e) {
for(var i = 0; i < e.length; i++) {
if (e[i].type === 'childList') {
console.error("THIS PART IS NOT TRANSLATED INTO PURE JS AS OF NOW ... SOME THINKING NEED TO BE DONE FIRST !");
// $(e[i].addedNodes).each(this.analyze_added_element);
// $(e[i].removedNodes).each(this.analyze_removed_element);
}
}
}
_register_observer() {
if ( this.observer || ( this.observer = new MutationObserver(this._observe_element) ) ) {
this.observer.observe(document.documentElement, {
subtree: true,
childList: true
});
}
}
set observer(value) {
return this._observer = value;
}
get observer() {
return this._observer;
}
set listeners(value) {
return this._listeners = value;
}
get listeners() {
return this._listeners;
}
get hourglass() {
return this._hourglass;
}
set hourglass(value) {
return this._hourglass = value;
}
get resizing() {
return this._resizing;
}
set resizing(value) {
return this._resizing = value;
}
}
export { ElementObserver }

View File

@ -0,0 +1,107 @@
/*
* Really simple timer / timeout object
*
* @author Dave Mc Nicoll
* @version 2.0.0
*
*/
const config = {
timer: 1000,
start: false,
loop: true,
run: false, // If you need a defined number of code execution, set a number to run and loop to false.
complete: function(){} // This complete callback function is called if the run exec number is reached.
};
class Hourglass {
constructor(callback, options) {
this.object_id = null;
this.counter = null;
this.callback = callback !== undefined ? callback : function() {};
if ( config.start ) {
this.start();
}
}
start(timeout) {
let time = ( timeout !== undefined ? timeout : config.timer );
if ( config.loop ) {
if ( this.counter === null ) {
this.counter = config.loop ? 0 : null
}
this.object_id = window.setInterval( this.call_interval.bind(this), time);
}
else {
this.object_id = window.setTimeout( this.call_timeout.bind(this), time);
}
}
restart(timeout) {
this.clear();
this.start(timeout);
}
stop() {
if ( config.loop ) {
this.object_id && window.clearInterval(this.object_id);
}
else {
this.object_id && window.clearTimeout(this.object_id);
}
}
clear() {
this.stop();
this.counter = null;
this.object_id = null;
}
call_timeout() {
this.callback();
this.clear();
}
call_interval() {
this.callback();
if ( this.counter !== null && ++this.counter === config.run ) {
this.clear();
config.complete !== undefined && config.complete();
}
}
running() {
return !! this.object_id;
}
get callback() {
return this._callback;
}
set callback(value) {
return this._callback = value;
}
get counter() {
return this._counter;
}
set counter(value) {
return this._counter = value;
}
get object_id() {
return this._object_id;
}
set object_id(value) {
return this._object_id = value;
}
}
export { Hourglass }

View File

@ -0,0 +1,324 @@
import { Webcomponent } from "./webcomponent.js";
const config = {
item: ".tab-item",
content: ".tab-content"
};
class UiPopup extends Webcomponent {
constructor() {
super();
this.fetch_template();
this.message = this.querySelector('.message');
this.title = this.querySelector('.title');
this.replaceVarsPlaceholder().attach().render();
this.init();
this.handleOptions();
}
handleOptions() {
if ( this.options ) {
if ( ! this.options['skip-keys'] ) {
document.addEventListener('keyup', function(e) {
if ( this.visible() ) {
let trigger, ev;
e.preventDefault();
// There's no real value in checking if the popup is shown or not.
switch ( e.keyCode ) {
// RETURN
case 13:
trigger = this.querySelector('[action="confirm"]');
ev = new CustomEvent('click', { trigger: trigger } );
trigger.dispatchEvent(ev);
break;
// ESC
case 27:
trigger = this.querySelector('[action="close"]');
ev = new CustomEvent('click', { trigger: trigger } );
trigger.dispatchEvent(ev);
break;
}
}
}.bind(this));
}
}
}
init() {
document.addEventListener('keyup', function(e) {
if ( this.visible() ) {
let trigger, ev;
e.preventDefault();
// There's no real value in checking if the popup is shown or not.
switch ( e.keyCode ) {
case 13: // RETURN
trigger = this.querySelector('[action="confirm"]');
ev = new CustomEvent('click', { trigger: trigger } );
trigger.dispatchEvent(ev);
break;
case 8: // BACKSPACE
case 27: // ESC
trigger = this.querySelector('[action="close"]');
ev = new CustomEvent('click', { trigger: trigger } );
trigger.dispatchEvent(ev);
break;
}
}
}.bind(this));
}
attach() {
let triggers = document.querySelectorAll('[ui-popup]');
if ( triggers.length ) {
triggers.forEach(function(item) {
let trigger_args = this._parseTriggerArguments(item);
if ( trigger_args.name === this.getAttribute('name') ) {
item.addEventListener('click', this.triggerAction.bind(this, item, trigger_args));
}
this.options = trigger_args.options;
}.bind(this));
}
return this;
}
render() {
this.querySelectorAll('[action]').forEach(function(element) {
element.addEventListener("click", this.action.bind(this, element), false);
}.bind(this));
this.hide();
}
action(element, e) {
if ( ! element.attributes.action ) {
throw "A button was clicked onto which no action was bound.";
}
switch(element.attributes.action.value) {
case "confirm":
case "yes":
case "ok":
this.actionFollowLink(element) ||
this.actionClickButton(element) ||
this.actionSendForm(this.trigger) ||
this.actionSendForm(element);
case "cancel":
case "no":
case "close":
this.hide();
break;
}
this.dispatchEvent(new CustomEvent('action:' + element.attributes.action.value));
this.trigger.dispatchEvent(new CustomEvent('action:' + element.attributes.action.value));
if ( element.getAttribute("action") ) {
e.preventDefault();
}
return true;
}
hide() {
this.classList.remove('visible');
}
show() {
this.classList.add('visible');
}
visible() {
return this.classList.contains('visible');
}
actionFollowLink(element) {
//if ( element.getAttribute('follow-link') !== null ) {
// Automatically redirect to given location if it exists.
if ( this.trigger.tagName === "A" ) {
let href = this.trigger.getAttribute('href');
if ( href ) {
window.location = href;
return true;
}
}
//}
return false;
}
actionClickButton(element) {
//if ( element.getAttribute('button-click') !== null ) {
let tag = this.trigger.tagName.toLowerCase();
// Automatically redirect to given location if it exists.
if ( tag === "button" || ( ( tag === "input" ) && ( this.trigger.getAttribute("type").toLowerCase() === "submit") ) ) {
this.pause = true;
this.trigger.click();
this.pause = false;
return true;
}
//}
return false;
}
actionSendForm(element) {
//if ( element.getAttribute('send-form') !== null ) {
//let form = this.trigger.tagName === "FORM" ? this.trigger : this.trigger.closest('FORM');
let form = element.tagName === "FORM" ? element : element.closest('FORM');
if ( form ) {
form.submit();
return true;
}
else {
throw "A send-form action was set on a popup where the trigger as no parent form.";
}
//}
}
triggerAction(item, args, e) {
if (this.pause) {
return false;
}
e.preventDefault();
this.trigger = item;
args.vars && Object.keys(args.vars).map(function(objectKey, index) {
// let element = this.message.querySelectorAll(`.${objectKey}`);
let element = Array.prototype.slice.call( this.message.querySelectorAll(`.${objectKey}`) ).concat(Array.prototype.slice.call(this.title.querySelectorAll(`.${objectKey}`)));
element.length ? element.forEach(function(item) {
item.innerHTML = args.vars[objectKey];
}) : ( function() {
console.warn(`Variable '${objectKey}' could not be found within popup's message`);
} )();
}.bind(this));
if ( args.options ) {
args.options['input'] && Object.keys(args.options['input']).forEach(
key => this.querySelectorAll(`[name="${key}"]`).forEach(function(f) {
if (f.tagName === "TEXTAREA") {
f.value = args.options['input'][key];
}
else {
f.setAttribute("value", args.options['input'][key]);
}
f.dispatchEvent(new Event("change"));
})
);
}
this.show();
return true;
}
replaceVarsPlaceholder() {
let setVar = function(match, varname) {
let element = document.createElement('span');
element.classList.add('variable', varname);
return element.outerHTML;
};
this.message.innerHTML = this.message.innerHTML.replace(/{\$(.*?)}/g, setVar);
this.title.innerHTML = this.title.innerHTML.replace(/{\$(.*?)}/g, setVar);
return this;
}
get trigger() {
return this._trigger;
}
set trigger(value) {
return this._trigger = value;
}
get pause() {
return this._pause;
}
set pause(value) {
return this._pause = value;
}
get message() {
return this._message;
}
set message(value) {
return this._message = value;
}
get title() {
return this._title;
}
set title(value) {
return this._title = value;
}
get action_func() {
return this._action_func;
}
set action_func(value) {
return this._action_func = value;
}
_parseTriggerArguments(item) {
let attr = item.getAttribute('ui-popup').trim();
if ( attr.charAt(0) === '{' ) {
try {
return JSON.parse(attr);
}
catch(error) {
console.error("Popup trigger failed on JSON parsing of " + attr + " from object:", item);
}
}
else if ( typeof attr === "string" && attr.length ) {
return {
name: attr
};
}
else {
throw "A popup name must be given to attach this object.";
}
}
}
export { UiPopup }

View File

@ -0,0 +1,283 @@
import { Webcomponent } from './webcomponent.js';
const config = {
content: {
wrapper: ".wrapper",
list: ".item-list"
},
lang: {
create: "Create «{$name}»"
}
};
class UiSelect extends Webcomponent {
constructor() {
super();
this.fetch_template();
this.wrapper = this.shadowRoot.querySelector(config.content.wrapper);
this.list = this.shadowRoot.querySelector(config.content.list);
this.input = this.querySelector('input:not([type="hidden"])');
this.select = this.querySelector('select');
this.active = false;
this.init();
}
init() {
this.input.setAttribute('readonly', 'readonly');
if ( this.getAttribute("create") !== null ) {
this.item_create = this.add_item("__create__", config.lang.create, false, true);
}
Array.from(this.select.options).forEach(function(item) {
this.add_item(item.value, item.text, item.value === this.select.value);
}.bind(this));
[ "mouseover" , "mouseleave" ].forEach(function(ev) {
this.list.addEventListener(ev, function(e) {
this.active = ( ev === 'mouseover' ) && ( document.activeElement === this.input );
}.bind(this));
}.bind(this));
[ "focus" , "blur", "keyup", "click" ].forEach(function(ev) {
this.input.addEventListener(ev, function(e) {
switch(ev) {
case "focus":
case "blur":
setTimeout(function() {
this.has_focus = ev === "focus";
this[ (this.has_focus ? "show" : "hide" ) + "_list" ].call(this, false);
}.bind(this), 100);
break;
case "click":
if ( ! this.editing && this.has_focus ) {
this.editing = true;
this.input.removeAttribute('readonly');
this.show_list();
}
break;
case "keyup":
e.target.value && this.editing ? this.filter(e.target.value) : this.show_list(true);
break;
}
}.bind(this));
}.bind(this));
}
add_item(key, value, selselfected, hide) {
let elem = document.createElement('li');
if (this.list.querySelector(`[data-key="${key}"]`) !== null) {
console.error(`Item with key '${key}' is already existing.`);
return false;
}
elem.setAttribute('data-key', key);
if (! (elem.innerText = value) ) {
elem.innerHTML = "&nbsp;";
}
let self = this;
elem.addEventListener("click", function(e) {
self.set_value(this.getAttribute('data-key'), this.innerText);
let item = self.list.querySelector(".selected");
if ( item ) {
item.classList.remove("selected");
}
this.classList.add("selected");
self.hide_list(true);
});
if (selected) {
elem.click();
// this.input.key_up();
}
if (hide) {
elem.classList.add('hidden');
}
this.list.appendChild(
elem
);
return elem;
}
set_value(key, value) {
this.input.value = value.trim();
this.select.value = key;
}
hide_list(force) {
if ( ! this.active || force ) {
this.has_focus = this.editing = false;
this.list.classList.add('hidden');
this.input.setAttribute('readonly', 'readonly');
this.show_all_items();
this.item_create.classList.add('hidden');
}
}
show_list(show_all_items) {
this.list.classList.remove('hidden');
if ( show_all_items ) {
this.show_all_items();
}
else {
this.editing && this.input.value && this.filter(this.input.value);
}
if ((! this.querySelectorAll(":not(.hidden)").length || show_all_items ) && this.item_create ) {
this.item_create.classList.add('hidden');
}
}
flush() {
}
filter(org_query) {
let empty = true,
has_exact_result = false,
query = org_query.trim().normalize().toLowerCase();
Array.from( this.list.querySelectorAll('li:not([data-key="__create__"])') ).forEach(function(item) {
let text = item.innerText.normalize().toLowerCase(),
keep = false;
if ( text === query ) {
keep = true;
empty = false;
has_exact_result = true;
}
else {
query.split(this._advanced_search ? " " : "\n").filter(item => item.length > 1).forEach(function(query_item) {
if ( text.indexOf(query_item) !== -1 ) {
keep = true;
empty = false;
}
});
}
if ( keep ) {;
item.classList.remove('hidden');
}
else {
item.classList.add('hidden');
}
}.bind(this));
if ( this.item_create ) {
if ( empty && ! has_exact_result ) {
this.item_create.classList.remove('hidden');
this.item_create.innerText = this.replace_vars(config.lang.create, {
name: org_query
});
}
else {
this.item_create.classList.add('hidden');
}
}
}
replace_vars(content, vars) {
return content.replace(/{\$(.*?)}/g, function(match, varname) {
return vars[varname];
});
}
show_all_items() {
Array.from( this.list.querySelectorAll('li') ).forEach(item => item.classList.remove('hidden'));
}
get content() {
return this._content;
}
set content(value) {
return this._content = value;
}
get list() {
return this._list;
}
set list(value) {
return this._list = value;
}
get wrapper() {
return this._wrapper;
}
set wrapper(value) {
return this._wrapper = value;
}
get input() {
return this._input;
}
set input(value) {
return this._input = value;
}
get active() {
return this._active;
}
set active(value) {
return this._active = value;
}
get item_create() {
return this._item_create;
}
set item_create(value) {
return this._item_create = value;
}
get has_focus() {
return this._has_focus;
}
set has_focus(value) {
return this._has_focus = value;
}
get editing() {
return this._editing;
}
set editing(value) {
this.wrapper.classList[value ? "add" : "remove"]('is-editing');
return this._editing = value;
}
get advanced_search() {
return this._advanced_search;
}
set advanced_search(value) {
return this._advanced_search = value;
}
}
export { UiSelect }

View File

@ -0,0 +1,50 @@
import { Webcomponent } from "./webcomponent.js";
const config = {
item: ".tab-item",
content: ".tab-content",
activeClass: "ui-tab-active",
};
class UiTabs extends Webcomponent {
constructor(parameters) {
super();
this.init();
}
init() {
window.addEventListener("hashchange", this.hashChange.bind(this), false);
//this.querySelectorAll(config.item).forEach(ele => ele.addEventListener("click", this.clickTab.bind(this)));
if (window.location.hash) {
let eve = new Event("click");
eve.newURL = window.location.hash;
this.hashChange(eve);
}
}
hashChange(e) {
let anchor = e.newURL.split('#')[1];
document.querySelectorAll("#" + anchor + config.content).forEach(function(content) {
content.parentNode.querySelectorAll(config.content + "." + config.activeClass).forEach(ele => ele.classList.remove(config.activeClass));
content.classList.add(config.activeClass);
});
let element = this.querySelector("." + config.activeClass);
element ? element.classList.remove(config.activeClass) : null;
element = this.querySelector(config.item + `[href="#${anchor}"]`);
element ? element.closest('div').classList.add(config.activeClass) : null;
}
clickTab(e) {
e.preventDefault();
history.pushState(null, null, this.getAttribute('href'));
}
}
export { UiTabs }

View File

@ -0,0 +1,246 @@
import { Webcomponent } from "./webcomponent.js";
const baseRows = 1.6;
const domElements = {
input: ".input",
toolbar: ".toolbar",
wrapper: ".wrapper",
boilerplate: ".content"
};
const domReplaceTag = {
i: "em",
b: "strong",
pre: "div"
};
const copyCss = [
"font", "font-size", "color", "background", "outline"
];
class UiTextarea extends Webcomponent {
constructor() {
super();
this.fetch_template();
this.wrapper = this.shadowRoot.querySelector(domElements.wrapper);
this.content = this.shadowRoot.querySelector(domElements.boilerplate);
this.toolbar = this.shadowRoot.querySelector(domElements.toolbar);
if ( ! this.textareaInit() ) {
return;
}
this.toolbarInit();
this.render();
}
render() {
document.execCommand("DefaultParagraphSeparator", false, "p");
this.content.innerHTML = this.textarea.value.trim();
this.content.addEventListener("input", function(e, d) {
this.content.querySelectorAll('[style]').forEach(function(element) {
element.removeAttribute('style');
});
this.content.querySelectorAll('[class]').forEach(function(element) {
element.removeAttribute('class');
});
this.textarea.value = this.content.innerHTML;
this.content.dispatchEvent(new Event("change"));
}.bind(this));
}
actionAlign(value) {
switch(value) {
case "left" :
return document.execCommand('justifyLeft',false,null);
case "center" :
return document.execCommand('justifyCenter',false,null);
case "right" :
return document.execCommand('justifyRight',false,null);
case "justify" :
return document.execCommand('justifyFull',false,null);
}
}
actionBold(value, content) {
return document.execCommand('bold', false, null);
}
actionItalic(value, content) {
return document.execCommand('italic', false, null);
}
actionUnderline() {
return document.execCommand('underline',false, null);
}
actionStrike() {
return document.execCommand('strikeThrough',false, null);
}
actionUl() {
return document.execCommand('insertUnorderedList',false, null);
}
actionOl() {
return document.execCommand('insertOrderedList',false, null);
}
actionUndo() {
return document.execCommand('undo', false, null);
}
actionRedo() {
return document.execCommand('redo', false, null);
}
actionForecolor(color) {
return document.execCommand('forecolor', false, color);
}
actionLink() {
}
actionImage() {
}
actionDocument() {
}
actionTag(tag) {
return document.execCommand('formatBlock', false, '<'+tag+'>');
}
toolbarInit() {
let action = this.getAttribute('toolbar');
if ( action !== null ) {
this.toolbar.setAttribute('action', action);
}
this.toolbar.querySelectorAll('[data-action]:not(.font-color)').forEach(function(item){
item.addEventListener('click', function(e) {
e.preventDefault();
let action = item.getAttribute('data-action');
return this["action" + action[0].toUpperCase() + action.slice(1)].call(this, item.getAttribute('data-value'), this.content);
}.bind(this));
}.bind(this));
this.toolbar.querySelectorAll('.font-color').forEach(function(item) {
item.addEventListener('click', function(e) {
e.preventDefault();
this.toolbar.querySelector('label[for="colorpicker"]').dispatchEvent(new Event('click'));
}.bind(this));
}.bind(this));
this.toolbar.querySelector('input[type="color"]').addEventListener('change', function(e) {
return this.actionForecolor.call(this, e.target.value);
}.bind(this));
}
textareaInit() {
if ( ! (this.textarea = this.querySelector("textarea") ) ) {
// throw "You should have a <textarea> element within a container with slot='input'";
this.textarea = document.createElement("textarea");
this.textarea.innerHTML = this.innerHTML;
this.append(this.textarea);
}
if ( this.textarea.getAttribute('readonly') ) {
this.wrapper.classList.add("readonly");
this.content.removeAttribute('contenteditable');
}
let rows = this.textarea.getAttribute('rows') ;
if ( rows ) {
rows = parseInt(rows);
this.content.style.minHeight = ( rows * baseRows ) + "em";
}
this.textarea.webcomponent = this;
if ( this.classList.contains("lite") ) {
this.wrapper.classList.add("lite");
}
return true;
}
html(html) {
return html !== undefined ? this.content.innerHTML = html : this.content.innerHTML;
}
text(text) {
return text !== undefined ? this.content.innerText = text : this.content.innerText;
}
get content() {
return this._content;
}
set content(value) {
return this._content = value;
}
get toolbar() {
return this._toolbar;
}
set toolbar(value) {
return this._toolbar = value;
}
get textarea() {
return this._textarea;
}
set textarea(value) {
return this._textarea = value;
}
get element() {
return this._element;
}
set element(value) {
return this._element = value;
}
get wrapper() {
return this._wrapper;
}
set wrapper(value) {
return this._wrapper = value;
}
get value() {
return this.content.innerHTML;
}
set value(value) {
this.content.innerHTML = value;
}
}
export { UiTextarea }

View File

@ -0,0 +1,33 @@
class Webcomponent extends HTMLElement {
fetch_template() {
this.attachShadow({mode: 'open'}).appendChild(
document.querySelector("template#" + this.tagName.toLowerCase()).content.cloneNode(true)
);
}
get_name() {
return Object.getPrototypeOf(this).constructor.name;
}
get_slot(name) {
return this.querySelector('[slot="' + name + '"]');
}
empty_element(element) {
while (element.lastChild) {
element.removeChild(element.lastChild);
}
return element;
}
build_url(url, data) {
return url + ( Object.entries(data).length ? "?" + Object.keys(data).map(function(key) {
return [key, data[key]].map(encodeURIComponent).join("=");
}).join("&") : "" );
}
}
export { Webcomponent };

14
composer.json Normal file
View File

@ -0,0 +1,14 @@
{
"name": "assets-package/webcomponent",
"description": "Web component package",
"type": "library",
"license": "MIT",
"authors": [
{
"name": "Dave Mc Nicoll",
"email": "info@mcnd.ca"
}
],
"require": {},
"autoload": {}
}