From 998e04e392a2665e8dbf88762a23be93f51fc341 Mon Sep 17 00:00:00 2001 From: Lenin Compres Date: Wed, 30 Oct 2024 16:29:27 -0400 Subject: [PATCH] Better interval animations --- DOM.js | 177 +++++++++++++++------------ index.js | 51 ++++---- src/Copy.js | 306 +++++++++++++++++++++++++++++----------------- src/Pager.js | 111 +++++++++++++---- src/animations.js | 36 ------ src/pages.js | 79 ++++++------ src/projects.js | 42 +++---- src/social.js | 0 src/style.js | 28 +++++ 9 files changed, 488 insertions(+), 342 deletions(-) delete mode 100755 src/animations.js mode change 100755 => 100644 src/social.js diff --git a/DOM.js b/DOM.js index 06dfdb8..ab6d036 100644 --- a/DOM.js +++ b/DOM.js @@ -1,7 +1,7 @@ /** * Creates DOM structures from a JS object (structure) * @author Lenin Compres - * @version 1.2.4 + * @version 1.2.6 * @repository https://github.com/lenincompres/DOM.js */ @@ -9,7 +9,7 @@ * Gets the value of an element's property. * @param {station} string - The style|attribute|(element)tag|innerText/innetHTML|on(event)|name of an element. */ - Element.prototype.get = function (station) { +Element.prototype.get = function (station) { let output; if (!station && this.tagName.toLocaleLowerCase() === "input") output = this.value; else if (!station || ["content", "inner", "innerhtml", "html"].includes(station)) output = this.innerHTML; @@ -69,7 +69,7 @@ Element.prototype.set = function (model, ...args) { const IS_PRIMITIVE = modelType.isPrimitive; let station = argsType.string; // original style|attr|tag|inner…|on…|name const CLEAR = !station && IS_PRIMITIVE || station === "content"; - if ([undefined, "create", "assign", "model", "inner", "set"].includes(station)) station = "content"; + if ([undefined, "inner", "create", "assign", "model", "set"].includes(station)) station = "content"; const STATION = station; station = station.toLowerCase(); // station lowercase // SELECT and input exception @@ -105,9 +105,14 @@ Element.prototype.set = function (model, ...args) { else this[STATION] = e => model(e, this); return this; } + // deletes related transition/animation intervals + if (IS_CONTENT && argsType.boolean === true) { + while (this.firstChild) this.removeChild(parentElement.firstChild); + this.innerHTML = ""; + } if (argsType.boolean === true && this.intervals && this.intervals[STATION]) { + clearInterval(this.intervals[station]); DOM.transition(this, `${DOM.unCamelize(STATION)} 0s`); - clearInterval(this.intervals[STATION]); } if ((model.interval || model.delay) && (model.to || model.through || model.loop)) { model.interval = parseInt(model.interval); @@ -120,20 +125,22 @@ Element.prototype.set = function (model, ...args) { } if (model.delay === undefined) model.delay = 0; if (model.delay !== undefined && model.interval === undefined) model.interval = model.delay; - if (model.transition) DOM.transition(this, `${DOM.unCamelize(STATION)} ${model.interval}ms ${model.transition}`); if (model.repeat === undefined) model.repeat = -1; - if (!model.while) model.while = typeof model.repeat === "function" ? model.repeat : () => model.repeat; - if (model.loop) setTimeout(() => { - this.set(model.loop[0], STATION); + if (!model.while) model.while = (typeof model.repeat === "function") ? model.repeat : (() => model.repeat > 0); + DOM.transition(this, `${DOM.unCamelize(STATION)} 0s`); + this.set(model.loop[0], STATION); + setTimeout(() => { let i = 1; + if (model.transition) DOM.transition(this, `${DOM.unCamelize(STATION)} ${model.interval}ms ${model.transition}`); DOM.interval(this, () => { this.set(model.loop[i], STATION); i += 1; - if (i >= model.loop.length) { - i = 0; - if (!isNaN(model.repeat)) model.repeat -= 1 - } - }, model.interval, model.while, STATION); + if (i >= model.loop.length) i = 0; + if (i === 0 && !isNaN(model.repeat)) model.repeat -= 1; + }, model.interval, model.while, STATION, () => { + this.set(model.loop[model.loop.length - 1], STATION); + if(model.callBack) model.callBack(); + }); }, model.delay); return this; } @@ -287,14 +294,25 @@ Element.prototype.set = function (model, ...args) { } if (DOM.typify(model.content).object) model.content = DOM.css(model.content); } + if (IS_HEAD && !IS_PRIMITIVE) { + if (station === "image") { + return this.set({ + "og:image": model.src, + "og:image:alt": model.alt, + "twitter:image": model.src, + "twitter:card": model.card, + }) + } + } if (IS_PRIMITIVE) { if (IS_HEAD) { const type = DOM.getDocType(model); - if (station === "title") this.innerHTML += `${model}`; + if (station === "description") this.innerHTML += ``; + if (station === "title") this.innerHTML += `${model}`; else if (station === "icon") this.innerHTML += ``; else if (station === "image") this.innerHTML += ``; else if (station === "charset") this.innerHTML += ``; - else if (station.startsWith("og:")) this.innerHTML += ``; + else if (station.includes(":")) this.innerHTML += ``; else if (DOM.metaNames.includes(station)) this.innerHTML += ``; else if (DOM.htmlEquivs.includes(STATION)) this.innerHTML += ``; else if (station === "font") DOM.set({ @@ -327,7 +345,7 @@ Element.prototype.set = function (model, ...args) { elt = p5Elem ? elem.elt : elem; if (cls.length) elt.classList.add(...cls); if (id) elt.setAttribute("id", id); - if (argsType.boolean === undefined) this.append(elt); + argsType.boolean === false ? this.prepend(elt) : this.append(elt); ["ready", "onready", "done", "ondone"].forEach(f => { if (!model[f]) return this; model[f](elem); @@ -335,7 +353,7 @@ Element.prototype.set = function (model, ...args) { ["timeout"].forEach(f => { if (!model[f]) return this; let [func, t] = Array.isArray(model[f]) ? model[f] : [model[f], 1]; - setTimeout(() => func(elem), t); + DOM.interval(this, func, t, 1); }); ["interval"].forEach(f => { if (!model[f]) return this; @@ -382,19 +400,21 @@ Element.prototype.css = function (style) { * Updates stations of element when its value changes. Can also update other binders. */ class Binder { + #value; + /** * creates a new instance of a Binder. * @param {val} - Initial value for the Binder to hold. */ constructor(val) { - this._value = val; + this.#value = val; this._bonds = []; this._listeners = {}; this._listenerCount = 0; this.onvalue = v => v; this.update = bond => { if (!bond.target) return; - let theirValue = bond.as(this._value); + let theirValue = bond.as(this.#value); if (bond.target.tagName) { if (!bond.type) return bond.target.set(theirValue, bond.station); return bond.target.set({ @@ -411,7 +431,7 @@ class Binder { * Keeps track of other Binders or binds that may be tracking value changes. * @param {func} - */ - addListener(func) { + onChange(func) { if (typeof func !== "function") return; this._listeners[this._listenerCount] = func; return this._listenerCount++; @@ -474,7 +494,7 @@ class Binder { let as = argsType.function ? argsType.function : val => val; if (values && values.length) as = this.getAs(values, as); else if (map && map !== target) as = this.getAs(map, as); - if (!target) return DOM.bind([this], as, listener); // binding in a model + if (!target) return DOM.bind([this], as, listener); // binding in a model if (listener) this.removeListener(listener); // if in a model, removes the listener let bond = { binder: this, @@ -541,7 +561,7 @@ class Binder { * @param {val} - value to hold. */ set value(val) { - this._value = val; + this.#value = val; this._bonds.forEach(bond => { if (bond.target === this.setter) return; this.update(bond); @@ -554,7 +574,7 @@ class Binder { * Gets the value in the binder. */ get value() { - return this._value; + return this.#value; } /** * Creates a bind, given one of more binders. @@ -602,25 +622,32 @@ Element.prototype.bind = function (...args) { * @param {value} string - initial value in the binder */ Object.prototype.binderSet = function (name, value) { - const BS = new BinderSet(name, value); - BS.names.forEach(key =>{ - const _key = '_' + key; - Object.defineProperty(this, _key, { + if (typeof name == 'string') { + const _name = '_' + name; + const _ = new Binder(value); + Object.defineProperty(this, _name, { get() { - return BS[_key]; + return _; + }, + set(val) { + console.error(`Error: This (${_name}) is a read-only binder and cannot be reassigned. Use: ${name}, or: ${_name}.value. to change its value.`); }, configurable: false, // Prevents deletion of the property enumerable: true, }); - Object.defineProperty(this, key, { + Object.defineProperty(this, name, { get() { - return BS[key]; + return this[_name].value; }, set(val) { - BS[key] = val; + this[_name].value = val; }, }); - }); + return; + } + for (const [key, value] of Object.entries(name)) { + this.binderSet(key, value); + } } /** @@ -630,38 +657,8 @@ class BinderSet { /** * Creates a BinderSet instance */ - names = []; constructor(...args) { - this.addBinder(...args); - } - addBinder(name, value) { - if (typeof name == 'string') { - const _name = '_' + name; - const _ = new Binder(value); - Object.defineProperty(this, _name, { - get() { - return _; - }, - set(val) { - console.error(`Error: This (${_name}) is a read-only binder and cannot be reassigned. Use: ${name}, or: ${_name}.value. to change its value.`); - }, - configurable: false, // Prevents deletion of the property - enumerable: true, - }); - Object.defineProperty(this, name, { - get() { - return this[_name].value; - }, - set(val) { - this[_name].value = val; - }, - }); - this.names.push(name); - return; - } - for (const [key, value] of Object.entries(name)) { - this.addBinder(key, value); - } + this.binderSet(...args); } /** * Creates a binding object inside a model to be set() applying one of multiple binders in the set. Expected to be called followed by a "as()" call: myBinderSet.with("binderName").as(func). @@ -669,15 +666,7 @@ class BinderSet { */ with(...args) { if (Array.isArray(args[0])) return this.with(...args[0]).as(args[1]); - return DOM.bind(...this.validate(args)); - } - /** - * Given a string name returnd the appropriate binder in the set. - * @param {args} array - binders to be returned. - */ - validate(...args) { - if (Array.isArray(args[0])) return this.validate(...args[0]); - return args.map(a => typeof a === 'string' ? this["_" + a] : a); + return DOM.bind(...BinderSet.validate(args)); } /** * Binds binders in the set to an element's property. @@ -694,16 +683,24 @@ class BinderSet { if (argsType.element) return { with: (...binders) => ({ as: func => argsType.element.set({ - [argsType.string]: DOM.bind(this.validate(binders), func), + [argsType.string]: DOM.bind(BinderSet.validate(binders), func), }) }) } if (args[0].target) return args[0].target.set({ - [args[0].station]: DOM.bind(this.validate(args[0].with), args[0].as), + [args[0].station]: DOM.bind(BinderSet.validate(args[0].with), args[0].as), }); if (args[0].with) return this.with(args[0].with, args[0].as); return this.with(...args); } + /** + * Given a string name returnd the appropriate binder in the set. + * @param {args} array - binders to be returned. + */ + static validate(...args) { + if (Array.isArray(args[0])) return BinderSet.validate(...args[0]); + return args.map(a => typeof a === 'string' ? this["_" + a] : a); + } } /** @@ -785,7 +782,12 @@ class DOM { * @param {model} object - model to be set to create the structure and properties of the element. * @param {tag} string - tag. */ - static element = (model, tag = "section") => { + static element = (model, tag) => { + if (tag === undefined && typeof model === "string") { + tag = model; + model = {}; + } + if (tag === undefined) tag = "section"; if (model && model.tag) { tag = model.tag; delete model.tag; @@ -910,6 +912,22 @@ class DOM { } return qs.split("/"); } + /** + * @param {array} - model of items in the list + * @return {object} - Model of ul with li + */ + static list = (...items) => Array.isArray(items[0]) ? DOM.list(...items[0]) : ({ + ul: { + li: items, + } + }); + /** + * @param {array} - model of links in the menu list + * @return {object} - Model of ul with li with links in it + */ + static linkMenu = (...links) => Array.isArray(links[0]) ? DOM.linkMenu(...links[0]) : DOM.list(links.map(link => ({ + a: link, + }))); /** * Sets and interval tied to an element and station * @param {elem} element - element related to the interval. @@ -925,6 +943,7 @@ class DOM { let go = typeof end === "function" ? end() : end || end === undefined; if (!go) { callback(); + delete elem.intervals[station]; return clearInterval(iId); } func(elem); @@ -940,10 +959,10 @@ class DOM { static transition(elem, trn) { // let prop = trn.split(' ')[0].trim(); let trns = elem.get("transition"); - if (trns) trns = trns.split(",").map(t => t.trim()).filter(t => t !== "NaN") - .map(t => t.startsWith(prop) ? trn : t); - else trns = [trn]; - elem.set(trns.join(", "), "transition"); + if (trns) trns = trns.split(",").map(t => t.trim()).filter(t => t !== "NaN").map(t => t.startsWith(prop) ? trn : t).join(", "); + else trns = trn; + if (!trns.includes(trn)) trns += ", " + trn; + elem.set(trns, "transition"); } /** * Ads a new global variable for elements with an id diff --git a/index.js b/index.js index bb50fab..00218ec 100755 --- a/index.js +++ b/index.js @@ -1,11 +1,10 @@ import * as STYLE from "./src/style.js"; -import pager from "./src/pages.js"; -import SOCIAL_LINKS from "./src/social.js"; -import queueDown from "./src/animations.js"; +import Pager from "./src/Pager.js"; import Copy from "./src/Copy.js"; +import "./src/pages.js"; +import SOCIAL_LINKS from "./src/social.js"; const _hoverPage = new Binder(); - const _isMobile = new Binder(); const _isWide = new Binder(); @@ -14,6 +13,7 @@ function resized(e) { _isWide.value = window.innerWidth > 1050; }; + Copy.add({ storyteller: { en: "storyteller", @@ -91,7 +91,7 @@ DOM.set({ } }, header: { - model: [STYLE.PAGE, STYLE.FLEX], + model: [STYLE.PAGE, STYLE.FLEX, STYLE.SLIDE(0)], alignItems: "center", placeContent: "center", padding: "0.5em", @@ -118,7 +118,7 @@ DOM.set({ fontFamily: "title, Georgia, \"Times New Roman\", Times, serif", text: "Lenino", }, - click: e => pager.key = pager.default, + click: e => Pager.key = Pager.default, } }, tagline: { @@ -129,10 +129,11 @@ DOM.set({ display: _isMobile.as("block", "none"), a: SOCIAL_LINKS, }, - onready: queueDown, + //onready: queueDown, }, nav: { style: STYLE.FLEX, + model: STYLE.SLIDE(1), flexDirection: "column", color: "rgb(245, 220, 154)", zIndex: 5, @@ -147,7 +148,7 @@ DOM.set({ borderRadius: _isMobile.as("0.5em", 0), margin: _isMobile.as("2em 0 0 1.1em", 0), position: _isMobile.as("absolute", "relative"), - a: { + content: Pager.getLinkMenu(name => ({ color: STYLE.COLOR.PAGE, width: "5em", padding: ".25em", @@ -159,24 +160,20 @@ DOM.set({ whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", - content: pager.keys.map(name => ({ - backgroundColor: pager._key.as({ - [name]: STYLE.COLOR.LINK_DARK, - default: STYLE.COLOR.LINK, - }), - display: _isMobile.as(val => (val || name !== pager.keys.slice(-1)[0]) && name !== pager.keys[0], "none", "block"), - boxShadow: { - bind: [_hoverPage, pager._key], - as: (over, current) => current === name ? STYLE.SHADOW.INSET : over === name ? STYLE.SHADOW.HIGHLIGHT : STYLE.SHADOW.NORMAL, - }, - href: "#" + name, - text: Copy.get(name), - click: e => pager.key = name, - mouseover: e => _hoverPage.value = name, - mouseout: e => _hoverPage.value = false, - })) - }, - onready: queueDown + backgroundColor: Pager._key.as({ + [name]: STYLE.COLOR.LINK_DARK, + default: STYLE.COLOR.LINK, + }), + display: _isMobile.as(val => (val || name !== Pager.keys.slice(-1)[0]) && name !== Pager.keys[0], "none", "block"), + boxShadow: { + bind: [_hoverPage, Pager._key], + as: (over, current) => current === name ? STYLE.SHADOW.INSET : over === name ? STYLE.SHADOW.HIGHLIGHT : STYLE.SHADOW.NORMAL, + }, + text: Copy.get(name), + mouseover: e => _hoverPage.value = name, + mouseout: e => _hoverPage.value = false, + })), + //onready: queueDown }, article: { model: STYLE.FLEX, @@ -185,7 +182,7 @@ DOM.set({ height: "fit-content", width: _isMobile.as("47em", "100%"), margin: _isMobile.as("6em 0 1.5em 9em", "0 0 1em 0"), - content: pager._content, + content: Pager._content, }, footer: { css: { diff --git a/src/Copy.js b/src/Copy.js index 404728d..dcdcea6 100644 --- a/src/Copy.js +++ b/src/Copy.js @@ -1,110 +1,162 @@ -/* - This class return text from the map content given to it. - There are several way copy can be indexed. - - map = { - key0: Text in only language, - key1: { - en: "Text in English", - es: "Texto en español", - }, - key2: [{ - en: "Text in English", - es: "Texto en español", - }, { - en: "Next text in English", - es: "Texto siguiente en español", - }] - } -*/ - -class Copy { - +/** + * Class that stores the copy text and retrieves the appropriate language copy (text) from a map given a key. + * @author Lenin Compres + * @version 1.0.1 + * @repository https://github.com/lenincompres/DOM.js + */ + + class Copy { + #map = {}; + #key; + #indexMap = {}; + #KEY = {}; + + /** + * Creates an instance of a copy. + * @param {object} map - { + key0: Text in only language, + key1: { + en: "Text in English", + es: "Texto en español", + }, + key2: [{ + en: "Text in English", + es: "Texto en español", + }, { + en: "Next text in English", + es: "Texto siguiente en español", + }] + } + * */ constructor(map = {}) { - this.map = map; - this.lang = Copy.lang; - this.key = Object.keys(this.map)[0]; - this.counter = {}; - this.KEY = {}; - } - + this.#map = map; + this.#key = this.keys[0]; + } + + /** + * @return {array} - All the keys in the copy. + */ + get keys() { + return Object.keys(this.#map); + } + + /** + * @return {array} - All the languages in the copy. + */ + get langs() { + let langs = []; + let values = []; + Object.values(this.#map).forEach(entry => Array.isArray(entry) ? entry.forEach(e => values.push(e)) : values.push(entry)); + values = values.filter(entry => typeof entry !== "string") + values.forEach(entry => Object.keys(entry).forEach(code => !langs.includes(code) ? langs.push(code) : null)); + return langs.map(code => Copy.LANG[code]); + } + + /** + * Adds an entry to the copy map + * @param {string} key - Key to access this text. + * @param {string} val - Text. + * @returns {string} - Text added, in the appropriate language, if any. + */ add(key, val) { - if (typeof key !== "string") { - return Object.entries(key).forEach(([k, v]) => this.add(k, v)); - } - if (this.map[key]) { - console.error(`Key "${key}" already exists in copy.`); - return "sd"; - } - this.map[key] = val; - this.KEY[key] = key; + if (typeof key !== "string") return Object.entries(key).forEach(([k, v]) => this.add(k, v)); + if (this.#map[key]) return console.error(`Key "${key}" already exists in copy.`); + this.#map[key] = val; + this.#KEY[key] = key; return this.get(key); } - // returns the text based on key, array index and language - get(key, i) { - if (key === undefined) { - console.error("No key was passed to the copy."); - }; - let val = this.map[key]; - if (val === undefined) { - console.error(`Key "${key}" not found in copy.`); - return ""; - } - this.key = key; - let lang = this.lang; + /** + * Gets the text based on key, array index and language. + * @param {string} key - Key of text to retrieve. + * @param {number} i - Index to retrieve, if that key has an array. + * @return {string} - Text copy in the appropriate key and index. + */ + get(key, i = 0) { + if (key === undefined) return console.error("No key was passed to the copy."); + let val = this.#map[key]; + if (val === undefined) return console.error(`Key "${key}" not found in copy.`); + this.#key = key; + let lang = Copy.lang; if (val[lang]) return Copy.treat(val[lang]); if (typeof val === "string") return Copy.treat(val); if (!Array.isArray(val)) { console.error(`Language "${lang}" not found in copy at "${key}".`); return Copy.treat(val[Object.keys(val)[0]]); } - if (i === undefined) i = 0; val = val[i]; - if (val === undefined) return ""; - this.counter[key] = i; + if (val === undefined) return console.error(`No copy text found at: ${key}[${i}].`); + this.#indexMap[key] = i; if (val[lang]) return Copy.treat(val[lang]); if (typeof val === "string") return Copy.treat(val); - console.error(`Language "${lang}" not found in copy at "${key}[${i}]".`); + console.error(`Language "${lang}" not found in copy at: ${key}[${i}].`); return Copy.treat(val[Object.keys(val)[0]]); } - static treat(s) { - if (!s) return s; - if (Array.isArray(s)) return s.map(i => Copy.treat(i)); - //if(s.includes("\n")) return Copy.treat(s.split("\n")); - return s.replaceAll("—", '--'); - } - - next() { - let i = this.counter[this.key]; - i = i === undefined ? 0 : i + 1; - return this.get(this.key, i); - } - - static copy = new Copy(); - - static add(...args) { - return Copy.copy.add(...args); - } - - static get(...args) { - return Copy.copy.get(...args); - } - - static next() { - return Copy.copy.next(); - } - - static text(map){ - return map[Copy.lang]; - } - - static set lang(val) { - localStorage.setItem("copy-lang", val); - location.reload(); - } - + /** + * @return {Object} - The next text of the last key retrieved, if it has an array. + */ + next = () => this.get(this.#key, this.#indexMap[this.#key] + 1); + + /** + * @return {Object} - JS model of links to toggle the page's language. + */ + getToggleLink = () => this.langs.map(lang => ({ + display: Copy.lang !== lang.code ? "block" : "none", + text: lang.name, + onclick: () => Copy.lang = lang.code, + })); + + /** + * @return {Object} - JS model with a ul of li's with links to toggle the page's language. + */ + getLinkMenu = () => DOM.linkMenu(this.langs.map(lang => ({ + class: { + selected: Copy.lang === lang.code, + }, + text: lang.name, + onclick: () => Copy.lang = lang.code, + }))); + + /** + * @return {Object} - JS model to select with options to change the page's language. + */ + getSelect = () => ({ + option: this.langs.map(lang => ({ + selected: Copy.lang === lang.code ? true : undefined, + value: lang.code, + text: lang.name, + })), + onchange: e => Copy.lang = e.target.value, + }); + + /** + * Treats the text as to add any fixes. So + * @param {string} str - Text to be treated. + * @param {array} str - Texts to be treated. + * @return {str} - Treated text. + * @return {array} - Array of treated texts. + */ + static treat(str) { + if (!str) return str; + if (Array.isArray(str)) return str.map(i => Copy.treat(i)); + return str.replaceAll("—", '--'); + } + + /** + * Selects the text from the given map in the appropriate language. + * @param {object} map - A map the copy text from which to select the one in the appropriate language. + */ + static text = (map) => map[Copy.lang]; + + /** + * Name to be used to saves chosen language in the local storage. + */ + static storageKey = "DOM-copy-lang"; + + /** + * Access to languages codes and proper keys. + */ static LANG = { es: { code: "es", @@ -116,37 +168,61 @@ class Copy { }, } - static get KEY(){ - return Copy.copy.KEY; - } - - static addKey(...keys) { - keys.forEach(key => { - if(Copy.KEY[key]) console.error(`Copy KEY ${key} already exists.`); - else Copy.copy.KEY[key] = key; - }); + /** + * Sets the language to be used when retrieving copy, and saves it in the local storage. + */ + static set lang(val) { + localStorage.setItem(Copy.storageKey, val); + location.reload(); } + /** + * Gets the current language code from the logal storage ot navigator. + */ static get lang() { - let lang = "en"; //Copy.LANG.EN.code; - if (navigator && navigator.language) { - lang = navigator.language.split("-")[0]; - } - let savedLang = localStorage.getItem("copy-lang");; - if (savedLang) { - lang = savedLang; - } + let lang = "en"; //Copy.LANG.en.code; + if (navigator && navigator.language) lang = navigator.language.split("-")[0]; + let savedLang = localStorage.getItem(Copy.storageKey);; + if (savedLang) lang = savedLang; return lang; } - static getToggleLink(...langs) { - return langs.map(lang => ({ - display: Copy.lang !== lang.code ? "block" : "none", - text: lang.name, - click: () => Copy.lang = lang.code, - })) - } - + /** + * This is a static instance of Copy, that can be used if you site only need one copy instance to hodl it's text. + */ + static copy = new Copy(); + static get keys(){ + return Copy.copy.keys; + } + static get KEY() { + return Copy.copy.#KEY; + } + static add = (...args) => Copy.copy.add(...args); + static get = (...args) => Copy.copy.get(...args); + static next = () => Copy.copy.next(); + static getToggleLink = () => Copy.copy.getToggleLink(); + static getLinkMenu = () => Copy.copy.getLinkMenu(); + static getSelect = () => Copy.copy.getSelect(); } +/** + * Browser readers don't handle em-dashes(—) correctly. This css, together with the treat method, allows writting copy with double dashes (--) and displays them as em-dashes, having readers read appropriately. + */ +DOM.css({ + "em.em-dash": { + display: "inline-block", + width: "0.7em", + height: "1em", + overflow: "hidden", + before: { + content: "—", + }, + }, +}); + +/** + * Sets the page's html property lang to the one stored + */ +document.documentElement.let("lang", Copy.lang); + export default Copy; \ No newline at end of file diff --git a/src/Pager.js b/src/Pager.js index fd50619..37931b7 100644 --- a/src/Pager.js +++ b/src/Pager.js @@ -1,31 +1,40 @@ /** - * It's like a router based on DOM.js (structure) + * It's a router based on DOM.js (structure) * @author Lenin Compres - * @version 1.0.0 + * @version 1.0.1 * @repository https://github.com/lenincompres/DOM.js */ -class Pager { + class Pager { + /** + * Creates a Pager instance. + * @param {object} map - Indexed pages models. + * @param {object} hashed - Links the static pager to the window hash. + */ + #hashed = false; map = {}; default; - - constructor(map = {}, linkHash = false) { + constructor(map = {}) { this.binderSet("key"); this.add(map); - if (linkHash) window.addEventListener("hashchange", (() => { - let hash = location.hash.substr(1); - let key = hash.split("-")[0]; - if (!key) key = this.default; - if (this.key === key) return; - if (!this.map[key]) return; - this.key = key; - if (key !== hash) setTimeout(location.href = `#${hash}`, 500); - })()); + this.#hashed = !Pager.pager; + if (this.#hashed) { + this._key.onChange(val => location.href = `#${val}`); + const hash = () => this.key = location.hash.substr(1).split("-")[0]; + window.addEventListener("hashchange", hash); + hash(); + } + this._key.onChange(val => !this.hasKey(val) ? this.key = this.default : null); } - + /** + * Adds an entry of page model to the pager. + * @param {string} key - Key index of this page. + * @param {object} content - JS model of thepage. + * @returns + */ add(key, content) { if (typeof key === "string") { - if (!this.default) this.key = this.default = key; + if (!this.default) this.default = key; this.map[key] = content; return; } @@ -33,25 +42,81 @@ class Pager { this.add(k, value); } } - + /** + * Gets the key of the current page. + */ get keys() { return Object.keys(this.map); } - - /* Methods and getters that return binder models */ + /** + * Gets the JS model of the current page. + */ + get content(){ + return this.map[this.key]; + } + /** + * Gets the binder for the JS model of the current page. + */ get _content() { return this._key.as(key => this.map[key]); } - - hasKey(key) { - return this.keys.includes(key); + /** + * Gets the links to change the pager. + * @param {function} aFunc - Function to be assigned to the links model. + * @returns {object} - The JS model of a list of links (ul:li:a). + */ + getLinkMenu(aFunc = (key, i) => {}) { + return DOM.linkMenu(this.keys.map((key, i) => Object.assign({ + class: { + active: this._key.as(val => val === key), + }, + text: key, + onclick: () => this.key = key, + }, aFunc(key, i)))); } + /** + * Checks if a key exists in the pager. + * @param {string} key - Key to check. + * @returns {boolean} - Does the key exist? + */ + hasKey = key => this.keys.includes(key); /* Not sure what this does */ _map(key, func = val => val) { return this._key.as(key => func(this.map[key])); } - + /** + * Static instace of a pager linked to the hash, meant to be the main pager of a website. + */ + static pager = new Pager({}, true); + static get map() { + return Pager.pager.map; + } + static get key() { + return Pager.pager.key; + } + static set key(val) { + Pager.pager.key = val; + } + static get _key() { + return Pager.pager._key; + } + static get default() { + return Pager.pager.default; + } + static get keys() { + return Pager.pager.keys; + } + static get content() { + return Pager.pager.content; + } + static get _content() { + return Pager.pager._content; + } + static add = (...args) => Pager.pager.add(...args); + static hasKey = (key) => Pager.pager.hasKey(key); + static _map = (...args) => Pager.pager._map(...args); + static getLinkMenu = (...args) => Pager.pager.getLinkMenu(...args); } export default Pager; \ No newline at end of file diff --git a/src/animations.js b/src/animations.js deleted file mode 100755 index 58d33e3..0000000 --- a/src/animations.js +++ /dev/null @@ -1,36 +0,0 @@ -const SPEED = 300; - -const queue = []; - -const trigger = (s = SPEED) => setTimeout(_ => { - let entry = queue.shift(); - entry.elem.set(entry.model); - if (!queue.length) return; - trigger(s); -}, 0.3 * s); - -export const queueDown = (elem, model = { - top: ['-30px', '10px', 0], - opacity: [0, 1], -}, s = SPEED, transition = 'ease') => { - - if (!['fixed', 'absolute'].includes(elem.style.position)) elem.set('relative', 'position'); - - let properModel = {}; - Object.entries(model).forEach(([key, val]) => properModel[key] = { - through: val, - interval: s / Math.sqrt(val.length), - transition: transition, - }); - - Object.entries(properModel).forEach(([key, val]) => val.through ? elem.set(val.through[0], key) : null); - - queue.push({ - elem: elem, - model: properModel - }); - if (queue.length === 1) trigger(s); - -} - -export default queueDown; \ No newline at end of file diff --git a/src/pages.js b/src/pages.js index 70a6932..0038d87 100755 --- a/src/pages.js +++ b/src/pages.js @@ -1,4 +1,3 @@ -import queueDown from "./animations.js" import allProjects from "./projects.js" import * as STYLE from "./style.js"; import SOCIAL_LINKS from "./social.js"; @@ -12,6 +11,8 @@ const NONE = "∅"; const _activeProject = new Binder(); const _allTags = new Binder([]); const _activeTag = new Binder(NONE); +Pager._key.onChange(val => _activeTag.value = NONE); + const showTag = (tag = NONE) => _activeTag.value = tag; const addTag = (tag) => !_allTags.value.includes(tag) ? (_allTags.value = [..._allTags.value, tag].sort(sortWords)) : null; const sortWords = (a, b) => a < b ? -1 : 1; @@ -42,13 +43,14 @@ window.addEventListener('hashchange', () => { newsScroll.start() }); -export const pager = new Pager({ +Pager.add({ [Copy.KEY.home]: { section: newsScroll, }, [Copy.KEY.bio]: { section: { style: STYLE.PAGE, + model: STYLE.SLIDE(0, "left"), lineHeight: "1.5em", content: { p: Copy.text({ @@ -56,47 +58,47 @@ export const pager = new Pager({ en: `Lenin Comprés (also known as Prof. Lenino) is a professional in interactive media, educational sciences, and performing arts. He serves as a professor and creative technologist at New York University's TISCH School of the Arts and holds a degree from Columbia University's Teachers College. Lenin is the co-writer of the award-winning script for A State of Madness, which represented the Dominican Republic at the 2021 Oscars. He is also the creator of Lenino’s JACK RABBITS, the first original Dominican-authored board game featuring an instructional booklet written entirely in verse and rhyme. As an artist, he performs as a quirky musical storyteller under his Lenino moniker.` }), }, - onready: elt => queueDown(elt, { - left: ['-30px', '10px', 0], - opacity: [0, 1], - }), } }, [Copy.KEY.projects]: { menu: { - maxWidth: "50em", - margin: "0 0.5em 0.5em", - justifyContent: "center", display: "flex", - flexWrap: "wrap", - content: _allTags.as(val => ({ - a: [NONE, ...val].map(tag => ({ - backgroundColor: _activeTag.as({ - [tag]: STYLE.COLOR.LINK_DARK, - default: STYLE.COLOR.PALE - }), - color: _activeTag.as(val => val === tag ? STYLE.COLOR.PAGE : STYLE.COLOR.LINK), - boxShadow: STYLE.SHADOW.NORMAL, - borderRadius: "0.25em", - padding: "0.2em 0.68em", - margin: "0.3em 0.3em 0 0", - display: "inline-block", - text: tag, - onclick: e => showTag(tag), - })) + ul: _allTags.as(val => ({ + maxWidth: "50em", + margin: "0 0.5em 0.5em", + justifyContent: "center", + display: "flex", + flexWrap: "wrap", + li: [ + [NONE, ...val].map((tag, i) => ({ + a: { + model: STYLE.SLIDE("left", i), + backgroundColor: _activeTag.as({ + [tag]: STYLE.COLOR.LINK_DARK, + default: STYLE.COLOR.PALE + }), + color: _activeTag.as(val => val === tag ? STYLE.COLOR.PAGE : STYLE.COLOR.LINK), + boxShadow: STYLE.SHADOW.NORMAL, + borderRadius: "0.25em", + padding: "0.2em 0.68em", + margin: "0.3em 0.3em 0 0", + display: "inline-block", + text: tag, + onclick: e => showTag(tag), + }, + })), + { + color: STYLE.COLOR.PALE, + textShadow: STYLE.SHADOW.TEXT, + text: _activeTag.as(val => val !== NONE ? projects.filter(p => p.tags.includes(val)).length : projects.length), + } + ], })), - span: { - color: STYLE.COLOR.PALE, - textShadow: STYLE.SHADOW.TEXT, - text: _activeTag.as(val => val !== NONE ? projects.filter(p => p.tags.includes(val)).length : projects.length), - }, - onready: elt => queueDown(elt, { - left: ['-30px', '10px', 0], - opacity: [0, 1], - }), }, section: projects.map((project, i) => ({ - model: STYLE.PAGE, + style: STYLE.PAGE, + model: STYLE.SLIDE(), + position: "relative", fontSize: "1em", width: "23em", cursor: "pointer", @@ -148,7 +150,6 @@ export const pager = new Pager({ let link = project.link ? project.link : project.folder window.open(link, "_blank") }, - onready: queueDown })), onready: () => { showTag(); @@ -161,11 +162,9 @@ export const pager = new Pager({ marginTop: "1em", textAlign: "center", a: SOCIAL_LINKS.map(a => Object.assign({ + model: STYLE.SLIDE(), margin: "0.25em", - onready: queueDown }, a)) } }, -}, true); - -export default pager; \ No newline at end of file +}); \ No newline at end of file diff --git a/src/projects.js b/src/projects.js index 39973fb..f8484bf 100755 --- a/src/projects.js +++ b/src/projects.js @@ -1,97 +1,95 @@ import Copy from "./Copy.js"; -Copy.addKey("hermanastra", "jackrabbits", "DOMjs", "locos", "pre", "album", "animacules", "chimes", "eloping", "pieces", "lave", "book", "code", "film", "game", "music", "performance", "theatre", "tool", "writing"); - Copy.add({ - [Copy.KEY.book]: { + book: { en: "book", es: "libro", }, - [Copy.KEY.code]: { + code: { en: "code", es: "código", }, - [Copy.KEY.film]: { + film: { en: "film", es: "cine", }, - [Copy.KEY.game]: { + game: { en: "game", es: "juego", }, - [Copy.KEY.music]: { + music: { en: "music", es: "música", }, - [Copy.KEY.performance]: "performance", - [Copy.KEY.theatre]: { + performance: "performance", + theatre: { en: "theatre", es: "teatro", }, - [Copy.KEY.tool]: { + tool: { en: "tool", es: "herramienta", }, - [Copy.KEY.writing]: { + writing: { en: "writing", es: "escritura", }, - [Copy.KEY.hermanastra]: [{ + hermanastra: [{ en: "La Hermanastra", es: "The Stepsister", }, { en: "A cabaret style musical monologue that reveals the stepsister's fate after Cinderella steals her prince.", es: "Monólogo músical que revela lo que pasó con la hermanastra de la Cenicienta tras perder al príncipe.", }], - [Copy.KEY.jackrabbits]: ["Jack Rabbits", { + jackrabbits: ["Jack Rabbits", { en: "The board game for playing cards on a land of wooden shards", es: "El juego de tablero en el que viajas al reino de conejos y barajas.", }], - [Copy.KEY.DOMjs]: ["DOM.js (DOM.set)", { + DOMjs: ["DOM.js (DOM.set)", { en: "JavaScript library that creates and handles DOM structures using a JS structural objects as a model", es: "Librería de JavaScript que crea y maneja estructuras de DOM utilizando objetos estructurales de JS como modelo.", }], - [Copy.KEY.locos]: [{ + locos: [{ en: "Mis 500 Locos", es: "A State of Madness", }, { en: "Feature film written by Lenin Compres & Waddys Jaquez, directed by Leticia Tonos, based on book by Antonio Zaglul", es: "Película de largometraje escrita por Lenin Comprés y Waddys Jáquez, dirigida por Leticia Tonos, basada en un libro de Antonio Zaglul", }], - [Copy.KEY.pre]: ["3DPsyche", { + pre: ["3DPsyche", { en: "Psychometric tool to visualize the physical, rational and emotional spectrum—interests, focus, personality types.", es: "Herramienta psicométrica que visualiza la espectro físico, racional y emocional del interes, el enfoque y la personalidad.", }], - [Copy.KEY.album]: ["Illusions of Duality", { + album: ["Illusions of Duality", { en: "Musical album: a storyteller's journey through conflict and empathy", es: "Album musical: la travesía del cuentacuentos a través del conflicto y la empatía", }], - [Copy.KEY.animacules]: [{ + animacules: [{ en: "Animalcules", es: "Animalucos", }, { en: "A digital story of life. Use movement and voice to survive as an unicellular organism.", es: "Una historia digital de la vida. Utiliza tu movimiento y voz para sobrevivir como un organismo unicelular", }], - [Copy.KEY.chimes]: [{ + chimes: [{ en: "Photonic Chimes", es: "Fotones Musicales", }, { en: "A refreshing and relaxing piece of creative code.", es: "Una pieza de código creativo y relajante.", }], - [Copy.KEY.eloping]: ["Eloping is Fun", { + eloping: ["Eloping is Fun", { en: "Lenino may officiate the wedding of your wildest dreams stress-free, easy and fun.", es: "Lenino podría oficiar la boda de tus fantasías sin estrés, con facilidad y diversión.", }], - [Copy.KEY.pieces]: [{ + pieces: [{ en: "12 Short Pieces", es: "Las 12 Piezas Cortas", }, { en: "Primeras obras de teatro, dirigidas a la formación de talentos teatrales. Incluye Desayuno en Rojo Chino.", es: "First theatre plays by Lenino, aimed at training theater talents. Includes Desayuno en Rojo Chino.", }], - [Copy.KEY.lave]: ["La Ve", { + lave: ["La Ve", { en: "Cover of Regina Spektor's “That Time” to celebrate international theatre month at La 37 por las Tablas, Santiago R.D.", es: "Cover de “That Time” de Regina Spektor para celebrar el mes internacional del teatro en La 37 por las Tablas, Santiago R.D.", }], diff --git a/src/social.js b/src/social.js old mode 100755 new mode 100644 diff --git a/src/style.js b/src/style.js index ce26c9c..ca73094 100755 --- a/src/style.js +++ b/src/style.js @@ -31,4 +31,32 @@ export const FLEX = { flexWrap: 'wrap', alignItems: 'flex-start', alignContent: 'flex-start', +}; + +let slideDelay = 0; + +export const SLIDE = (from = "top", delay, interval = 200) => { + if (typeof from === "number") { + delay = top; + from = "top" + }; + if (delay === undefined) delay = slideDelay++; + return { + pointerEvents: "default", + position: "relative", + maxHeight: "100em", + [from]: { + through: ["-2em", "1em", 0], + interval: interval, + delay: delay * interval / 3, + transition: "ease", + callBack: () => slideDelay--, + }, + opacity: { + through: [0, 1], + interval: interval, + delay: delay * interval / 3, + transition: "ease", + }, + } }; \ No newline at end of file