Newer
Older
(function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
protected_dirs: ["assets", "style", "views", "standard"],
default_meta_keys: ["title", "description", "image", "open_graph", "json_ld"],
},
};
images_url: `/assets/images`,
data_url: `/assets/data`,
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
/**
* A tiny library to handle text translation.
*/
/**
* init parameter object type
* @typedef TranslatorParam
* @property {String[]} supported_languages - DEFAULT: ["en"] - An array of ISO 639 lowercase language codes
* @property {String} locale ISO 639 lowercase language code - DEFAULT: "en"
*
* @property {Boolean} use_url_locale_fragment - DEFAULT: true -
* If true, the 1st fragment of the window.location url will be parsed as a language code
* Example:
* window.location
* > https://example.com/en/some-page...
* then the /en/ part of the url will be taken in priority and locale will be set to "en".
* window.location
* > https://example.com/some-page...
* No locale fragment will be found so locale will not be set from url fragment.
* window.location
* > https://example.com/some-page/en
* Doesn't work, locale fragment must be the first framgment
*
* @property {String} local_storage_key - DEFAULT: "translator-prefered-language"
* The key used to saved the current locale into local storage
*
* @property {String} translations_url - REQUIRED - the url of the directory containing the static json files for translations
* Translations files are expected to be named with their corresponding locale code.
* Example:
* if supported_languages is set to ["en", "fr", "it"]
* and translations_url is set to "https://example.com/translations/""
* Then the expected translations files are
* https://example.com/translations/en.json
* https://example.com/translations/fr.json
* https://example.com/translations/it.json
* The json resources must simple key value maps, value being the translated text.
*/
module.exports = {
locale: "en", // ISO 639 lowercase language code
supported_languages: ["en"],
translations: {},
translations_url: "",
use_url_locale_fragment: true,
local_storage_key: "translator-prefered-language",
/**
* Initialize the lib with params
* @param {TranslatorParam} params
* @returns {Promise}
*/
init(params) {
Object.entries(params).forEach(k_v => {
const [key, value] = k_v;
if ([
"supported_languages",
"use_url_locale_fragment",
"local_storage_key",
"translations_url"
].includes(key)) {
this[key] = value;
}
});
this.translations_url = this.format_translations_url(this.translations_url);
this.supported_languages = this.format_supported_languages(this.supported_languages);
return new Promise((resolve, reject) => {
const loc =
(() => {// Locale from url priority 1
if (this.use_url_locale_fragment) {
const first_url_fragment = window.location.pathname.substring(1).split("/")[0];
const fragment_is_locale = this.supported_languages.includes(first_url_fragment);
return fragment_is_locale ? first_url_fragment : "";
} else {
return "";
}
})()
|| localStorage.getItem(this.local_storage_key) // Locale from storage priority 2
|| (() => { // locale from navigator priority 3
const navigator_locale = navigator.language.split("-")[0].toLocaleLowerCase();
return this.supported_languages.includes(navigator_locale)
? navigator_locale
: this.supported_languages[0] // Default if navigator locale is not supported
})();
fetch(`${this.translations_url}${loc}.json`)
.then(response => response.json())
.then(response => {
this.locale = loc;
this.translations = response;
resolve();
})
.catch(err => {
this.locale = "en";
reject(err);
});
});
},
/**
* Return a lowercase string without dash.
* If given locale is en-EN, then "en" will be returned.
* @param {String} locale
* @returns A lowercase string
*/
format_locale(locale) {
return locale.split("-")[0].toLowerCase();
},
/**
* Appends a slash at the end of the given string if missing, and returns the url.
* @param {String} url
* @returns {String}
*/
format_translations_url(url) {
if (url.charAt(url.length - 1) !== "/") {
url += "/"
}
return url;
},
/**
* Return the array of language codes formatted as lowsercase language code.
* if ["en-EN", "it-IT"]is given, ["en", "it"] will b returned.
* @param {String[]} languages_codes
* @returns {String[]}
*/
format_supported_languages(languages_codes) {
return languages_codes.map(lc => this.format_locale(lc));
},
/**
* Fetches a new set of translations in case the wanted language changes
* @param {String} locale A lowercase language code
* @returns {Promise}
*/
update_translations(locale) {
locale = this.format_locale(locale);
return new Promise((resolve, reject) => {
fetch(`${this.translations_url}${locale}.json`)
.then(response => response.json())
.then(response => {
this.translations = response;
this.locale = locale;
localStorage.setItem(this.local_storage_key, locale);
const split_path = window.location.pathname.substring(1).split("/");
const first_url_fragment = split_path[0];
const fragment_is_locale = this.supported_languages.includes(first_url_fragment);
if (fragment_is_locale) {
split_path.splice(0, 1, locale);
const updated_path = split_path.join("/");
window.history.replaceState(null, "", "/" + updated_path);
}
resolve();
})
.catch(err => {
reject(err);
});
});
},
/**
* Tries to get the translation of the source string, or return the string as it.
* @param {String} text The source text to translate
* @param {Object} params Some dynamic element to insert in the text
* Params can be used if the translated text provide placeholder like {%some_word%}
* Example:
* translator.trad("Some trad key", {some_word: "a dynamic parameter"})
* -> translation for "Some trad key": "A translated text with {%some_word%}"
* -> will be return as : "A translated text with a dynamic parameter"
* @returns {String}
*/
trad: function (text, params = {}) {
text = this.translations[text] || text;
Object.keys(params).forEach(k => {
text = text.replace(`{%${k}%}`, params[k]);
});
return text;
}
};
},{}],5:[function(require,module,exports){
"use strict";
register_key: "objectToHtmlRender",
/**
* Register "this" as a window scope accessible variable named by the given key, or default.
* @param {String} key
*/
register(key) {
const register_key = key || this.register_key;
window[register_key] = this;
},
/**
* This must be called before any other method in order to initialize the lib.
* It provides the root of the rendering cycle as a Javascript object.
* @param {Object} renderCycleRoot A JS component with a render method.
*/
setRenderCycleRoot(renderCycleRoot) {
this.renderCycleRoot = renderCycleRoot;
},
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
event_name: "objtohtml-render-cycle",
/**
* Set a custom event name for the event that is trigger on render cycle.
* Default is "objtohtml-render-cycle".
* @param {String} evt_name
*/
setEventName(evt_name) {
this.event_name = evt_name;
},
/**
* This is the core agorithm that read an javascript Object and convert it into an HTML element.
* @param {Object} obj The object representing the html element must be formatted like:
* {
* tag: String // The name of the html tag, Any valid html tag should work. div, section, br, ul, li...
* xmlns: String // This can replace the tag key if the element is an element with a namespace URI, for example an <svg> tag.
* See https://developer.mozilla.org/en-US/docs/Web/API/Document/createElementNS for more information
* style_rules: Object // a object providing css attributes. The attributes names must be in JS syntax,
* like maxHeight: "500px", backgrouncColor: "#ff2d56", margin: 0, etc.
* contents: Array or String // This reprensents the contents that will be nested in the created html element.
* <div>{contents}</div>
* The contents can be an array of other objects reprenting elements (with tag, contents, etc)
* or it can be a simple string.
* // All other attributes will be parsed as html attributes. They can be anything like onclick, href, onchange, title...
* // or they can also define custom html5 attributes, like data, my_custom_attr or anything.
* }
* @returns {HTMLElement} The output html node.
*/
objectToHtml(obj) {
if (!obj) return document.createElement("span"); // in case of invalid input, don't block the whole process.
const objectToHtml = this.objectToHtml.bind(this);
const { tag, xmlns } = obj;
const node = xmlns !== undefined ? document.createElementNS(xmlns, tag) : document.createElement(tag);
const excludeKeys = ["tag", "contents", "style_rules", "state", "xmlns"];
Object.keys(obj)
.filter(attr => !excludeKeys.includes(attr))
.forEach(attr => {
switch (attr) {
case "class":
node.classList.add(...obj[attr].split(" ").filter(s => s !== ""));
break;
case "on_render":
if (!obj.id) {
node.id = `${btoa(JSON.stringify(obj).slice(0, 127)).replace(/\=/g, '')}${window.performance.now()}`;
}
if (typeof obj.on_render !== "function") {
console.error("The on_render attribute must be a function")
} else {
this.attach_on_render_callback(node, obj.on_render);
}
break;
default:
if (xmlns !== undefined) {
node.setAttributeNS(null, attr, obj[attr])
} else {
node[attr] = obj[attr];
}
}
});
if (obj.contents && typeof obj.contents === "string") {
node.innerHTML = obj.contents;
} else {
obj.contents &&
obj.contents.length > 0 &&
obj.contents.forEach(el => {
switch (typeof el) {
case "string":
node.innerHTML = el;
break;
case "object":
if (xmlns !== undefined) {
el = Object.assign(el, { xmlns })
}
node.appendChild(objectToHtml(el));
break;
}
});
}
if (obj.style_rules) {
Object.keys(obj.style_rules).forEach(rule => {
node.style[rule] = obj.style_rules[rule];
});
}
return node;
},
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
on_render_callbacks: [],
/**
* This is called if the on_render attribute of a component is set.
* @param {HTMLElement} node The created html element
* @param {Function} callback The callback defined in the js component to render
*/
attach_on_render_callback(node, callback) {
const callback_handler = {
callback: e => {
if (e.detail.outputNode === node || e.detail.outputNode.querySelector(`#${node.id}`)) {
callback(node);
const handler_index = this.on_render_callbacks.indexOf((this.on_render_callbacks.find(cb => cb.node === node)));
if (handler_index === -1) {
console.warn("A callback was registered for node with id " + node.id + " but callbacck handler is undefined.")
} else {
window.removeEventListener(this.event_name, this.on_render_callbacks[handler_index].callback)
this.on_render_callbacks.splice(handler_index, 1);
}
}
},
node,
};
const len = this.on_render_callbacks.push(callback_handler);
window.addEventListener(this.event_name, this.on_render_callbacks[len - 1].callback);
},
/**
* If a main element exists in the html document, it will be used as rendering root.
* If not, it will be created and inserted.
*/
const main_elmt = document.getElementsByTagName("main")[0] || (function () {
const created_main = document.createElement("main");
document.body.appendChild(created_main);
return created_main;
})();
this.subRender(this.renderCycleRoot.render(), main_elmt, { mode: "replace" });
/**
* This method behaves like the renderCycle() method, but rather that starting the rendering cycle from the root component,
* it can start from any component of the tree. The root component must be given as the first argument, the second argument be
* be a valid html element in the dom and will be used as the insertion target.
* @param {Object} object An object providing a render method returning an object representation of the html to insert
* @param {HTMLElement} htmlNode The htlm element to update
* @param {Object} options can be used the define the insertion mode, default is set to "append" and can be set to "override",
* "insert-before" (must be defined along with an insertIndex key (integer)),
* "adjacent" (must be defined along with an insertLocation key (String)), "replace" or "remove".
* In case of "remove", the first argument "object" is not used and can be set to null, undefined or {}...
*/
subRender(object, htmlNode, options = { mode: "append" }) {
let outputNode = null;
const get_insert = () => {
outputNode = this.objectToHtml(object);
return outputNode;
};
break;
case "override":
htmlNode.innerHTML = "";
htmlNode.insertBefore(get_insert(), htmlNode.childNodes[options.insertIndex]);
break;
case "adjacent":
/**
* options.insertLocation must be one of:
*
* afterbegin
* afterend
* beforebegin
* beforeend
*/
htmlNode.insertAdjacentHTML(options.insertLocation, get_insert());
htmlNode.parentNode.replaceChild(get_insert(), htmlNode);
break;
case "remove":
htmlNode.remove();
break;
}
const evt_name = this.event_name;
const event = new CustomEvent(evt_name, {
detail: {
inputObject: object,
outputNode,
insertOptions: options,
targetNode: htmlNode,
}
});
window.dispatchEvent(event);
const { fetch_json_or_error_text } = require("./fetch");
function getArticleBody(text) {
return text.replaceAll("\n", "<br/>");
}
function getArticleDate(date) {
return `${date.getDate()}-${date.getMonth() + 1}-${date.getFullYear()}`;
}
function loadArticles(category, locale) {
return fetch_json_or_error_text(`/articles/${category}/${locale}`);
}
module.exports = {
loadArticles,
getArticleBody,
getArticleDate,
};
},{"./fetch":7}],7:[function(require,module,exports){
"use strict";
function fetchjson(url) {
return new Promise((resolve, reject) => {
fetch(url)
.then(r => r.json())
.then(r => resolve(r))
.catch(e => reject(e));
});
}
function fetchtext(url) {
return new Promise((resolve, reject) => {
fetch(url)
.then(r => r.text())
.then(r => resolve(r))
.catch(e => reject(e));
});
}
async function fetch_json_or_error_text(url, options = {}) {
return new Promise((resolve, reject) => {
fetch(url, options).then(async res => {
if (res.status >= 400 && res.status < 600) {
reject(await res.text());
} else {
resolve(await res.json());
}
})
})
}
module.exports = {
fetchjson,
fetchtext,
const translator = require("ks-cheap-translator");
const { translations_url } = require("../../constants");
class WebPage {
constructor(args) {
Object.assign(this, args);
if (!this.id) {
this.id = "webpage-" + performance.now();
}
translator.init({
translations_url,
supported_languages: ["fr", "en"],
}).then(this.refresh_all.bind(this));
}
refresh() {
obj2htm.subRender(this.render(), document.getElementById(this.id), { mode: "replace" })
}
refresh_all() {
obj2htm.renderCycle()
},{"../../constants":3,"ks-cheap-translator":4}],9:[function(require,module,exports){
const { images_url } = require("../../../../../admin-frontend/src/constants");
const { getArticleBody } = require("../../../lib/article-utils");
class SoftwareArticle {
constructor(props) {
this.props = props;
}
render() {
const { title, body, subtitle, images, details = [] } = this.props;
return {
tag: "article",
class: "software-article",
typeof: "SoftwareApplication",
additionalType: "Article",
contents: [
{
tag: "h2",
class: "software-title",
contents: title,
property: "name",
},
{
tag: "div", class: "software-image",
contents: [
{
}
]
},
{
tag: "h3",
class: "software-subtitle",
contents: subtitle,
property: "alternativeHeadline",
},
{
tag: "div",
class: "software-description",
contents: getArticleBody(body),
property: "description",
},
contents: [
{
tag: "h2",
contents: "Details",
},
{
tag: "ul",
contents: details.map(detail => {
return {
tag: "li",
class: "detail",
contents: [
{ tag: "label", contents: detail.label },
},
],
},
],
};
}
}
},{"../../../../../admin-frontend/src/constants":1,"../../../lib/article-utils":6}],10:[function(require,module,exports){
"use strict";
const { loadArticles } = require("../../../lib/article-utils");
const SoftwareArticle = require("./software-article");
const translator = require("ks-cheap-translator");
class SoftwareArticles {
constructor(props) {
this.props = props;
this.state = {
articles: [],
};
this.loadArticles();
}
loadArticles() {
loadArticles("software", translator.locale).then(articles => {
this.state.articles = articles;
this.refresh();
this.fixScroll();
}).catch(e => console.log(e))
}
renderPlaceholder() {
return {
tag: "article",
class: "placeholder",
contents: [
{ tag: "div", class: "title" },
{ tag: "div", class: "body" },
{ tag: "div", class: "details" },
],
};
}
refresh() {
obj2htm.subRender(this.render(), document.getElementById(this.id), {
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
mode: "replace",
});
}
fixScroll() {
if (window.location.href.includes("#")) {
window.scrollTo(
0,
document.getElementById(window.location.href.match(/#.+/)[0].replace("#", ""))
.offsetTop
);
}
}
render() {
const { articles } = this.state;
return {
tag: "section",
class: "software-articles page-contents-center",
id: this.id,
contents:
articles.length > 0
? articles.map(article => new SoftwareArticle({ ...article }).render())
: [this.renderPlaceholder()],
};
}
}
module.exports = SoftwareArticles;
},{"../../../lib/article-utils":6,"./software-article":9,"ks-cheap-translator":4}],11:[function(require,module,exports){
"use strict";
const { images_url } = require("../../../constants");
const WebPage = require("../../lib/web-page");
const SoftwareArticles = require("./components/software-articles");
const translator = require("ks-cheap-translator");
const t = translator.trad.bind(translator);
class SoftwareDevelopment extends WebPage {
render() {
return {
tag: "div",
id: "software-page",
contents: [
{
tag: "div",
class: "page-header logo-left",
contents: [
{
tag: "div",
class: "page-contents-center grid-wrapper",
contents: [
{
tag: "div",
class: "logo",
contents: [
{
tag: "img",
alt: `image mechanic electronic`,
},
],
},
{ tag: "h1", contents: "Software" },
{
tag: "p",
},
],
},
],
},
new SoftwareArticles().render(),
],
};
}
}
module.exports = SoftwareDevelopment;
},{"../../../constants":3,"../../lib/web-page":8,"./components/software-articles":10,"ks-cheap-translator":4}],12:[function(require,module,exports){
"use strict";
"use strict";
const runPage = require("../../run-page");
const SoftwareDevelopment = require("./software-development");
runPage(SoftwareDevelopment);
},{"../../run-page":13,"./software-development":11}],13:[function(require,module,exports){
const Template = require("./template/template");
module.exports = function runPage(PageComponent) {
const template = new Template({ page: new PageComponent() });
renderer.register("obj2htm")
obj2htm.setRenderCycleRoot(template);
obj2htm.renderCycle();
},{"./template/template":15,"object-to-html-renderer":5}],14:[function(require,module,exports){
"use strict";
const { images_url } = require("../../../constants");
const translator = require("ks-cheap-translator");
const t = translator.trad.bind(translator);
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
const NAV_MENU_ITEMS = [
{ url: "/games/", text: "Jeux" },
{
url: "/education/",
text: "Pédagogie",
},
{ url: "/software-development/", text: "Software" }
];
class NavBar {
constructor() {
this.initEventHandlers();
}
handleBurgerClick() {
document.getElementById("nav-menu-list").classList.toggle("responsive-show");
}
initEventHandlers() {
window.addEventListener("click", event => {
if (
event.target.id !== "nav-menu-list" &&
!event.target.classList.contains("burger") &&
!event.target.parentNode.classList.contains("burger")
) {
document.getElementById("nav-menu-list").classList.remove("responsive-show");
}
});
}
handle_chang_lang(lang) {
translator.update_translations(lang).then(() => {
obj2htm.renderCycle();
}).catch(err => console.log(err));
}
renderHome() {
return {
tag: "div",
class: "home",
contents: [
{
tag: "a",
href: "/",
contents: [
{
tag: "img",
alt: "Logo Kuadrado",
src: `${images_url}/logo_kuadrado.svg`,
},
{
tag: "img",
alt: "Kuadrado Software",
class: "logo-text",
src: `${images_url}/logo_kuadrado_txt.svg`,
},
],
},
],
};
}
renderMenu(menuItemsArray, isSubmenu = false, parentUrl = "") {
return {
tag: "ul",
id: "nav-menu-list",
class: isSubmenu ? "submenu" : "",
contents: menuItemsArray.map(item => {
const { url, text, submenu } = item;
const href = `${parentUrl}${url}`;
return {
tag: "li",
class: !isSubmenu && window.location.pathname === href ? "active" : "",
contents: [
{
tag: "a",
href,
},
].concat(submenu ? [this.renderMenu(submenu, true, url)] : []),
};
}).concat({
tag: "li",
class: "lang-flags",
contents: ["fr", "en"].map(lang => {
return {
tag: "img", src: `${images_url}/flag-${lang}.svg`,
class: translator.locale === lang ? "selected" : "",
onclick: this.handle_chang_lang.bind(this, lang)
}
})
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
}),
};
}
renderResponsiveBurger() {
return {
tag: "div",
class: "burger",
onclick: this.handleBurgerClick.bind(this),
contents: [{ tag: "span", contents: "···" }],
};
}
render() {
return {
tag: "nav",
contents: [
this.renderHome(),
this.renderResponsiveBurger(),
this.renderMenu(NAV_MENU_ITEMS),
],
};
}
}
module.exports = NavBar;
},{"../../../constants":3,"ks-cheap-translator":4}],15:[function(require,module,exports){
"use strict";
const { in_construction } = require("../../config");
const { images_url } = require("../../constants");
const NavBar = require("./components/navbar");
const translator = require("ks-cheap-translator");
const t = translator.trad.bind(translator)
class Template {
constructor(props) {
this.props = props;
}
render() {
return {
tag: "main",
contents: [
{
tag: "header",
contents: [new NavBar().render()],
},
in_construction && {
tag: "section",
class: "warning-banner",
contents: [
{
tag: "strong",
class: "page-contents-center",
},
],
},
{
tag: "section",
id: "page-container",
contents: [this.props.page.render()],
},
{
tag: "footer",
contents: [
{
tag: "div",
class: "logo",
contents: [
{
tag: "img",
alt: `logo Kuadrado`,
src: `${images_url}/logo_kuadrado.svg`,
},
{
tag: "img",
class: "text-logo",
alt: "Kuadrado Software",
src: `${images_url}/logo_kuadrado_txt.svg`,
},
],
},
{
tag: "span",
contents: "32 rue Simon Vialet, 07240 Vernoux en Vivarais. Ardèche, France",
},
{
tag: "div",
contents: [
{ tag: "strong", contents: "<blue>Contact : </blue>" },
{
tag: "a",
href: "mailto:contact@kuadrado-software.fr",
contents: "contact@kuadrado-software.fr",
},
],
},
{
tag: "div",
class: "social",
contents: [
{
tag: "strong",
contents: `<blue>${t("Sur les réseaux")} : </blue>`,
},
{
tag: "a",
href: "https://www.linkedin.com/company/kuadrado-software",
target: "_blank",
contents: "in",
title: "Linkedin",
},
{
tag: "a",
href: "https://mastodon.gamedev.place/@kuadrado_software",
target: "_blank",
contents: "m",
title: "Mastodon",
}
{ tag: "span", contents: t("Ce site web est") + " " },
{
tag: "a", target: "_blank",
style_rules: { fontWeight: "bold" },
href: "https://gitlab.com/kuadrado-software/kuadrado-website/-/blob/master/README.md",
contents: "OPEN SOURCE"
}
]
}
],
},
],
};
}
}
module.exports = Template;
},{"../../config":2,"../../constants":3,"./components/navbar":14,"ks-cheap-translator":4}]},{},[12]);