@codegouvfr/react-dsfr
Version:
French State Design System React integration library
2,019 lines (1,670 loc) • 170 kB
JavaScript
/*! DSFR v1.12.1 | SPDX-License-Identifier: MIT | License-Filename: LICENSE.md | restricted use (see terms and conditions) */
class State {
constructor () {
this.modules = {};
}
create (ModuleClass) {
const module = new ModuleClass();
this.modules[module.type] = module;
}
getModule (type) {
return this.modules[type];
}
add (type, item) {
this.modules[type].add(item);
}
remove (type, item) {
this.modules[type].remove(item);
}
get isActive () {
return this._isActive;
}
set isActive (value) {
if (value === this._isActive) return;
this._isActive = value;
const values = Object.keys(this.modules).map((e) => {
return this.modules[e];
});
if (value) {
for (const module of values) {
module.activate();
}
} else {
for (const module of values) {
module.deactivate();
}
}
}
get isLegacy () {
return this._isLegacy;
}
set isLegacy (value) {
if (value === this._isLegacy) return;
this._isLegacy = value;
}
}
const state = new State();
const config = {
prefix: 'fr',
namespace: 'dsfr',
organisation: '@gouvfr',
version: '1.12.1'
};
class LogLevel {
constructor (level, light, dark, logger) {
this.level = level;
this.light = light;
this.dark = dark;
switch (logger) {
case 'warn':
this.logger = console.warn;
break;
case 'error':
this.logger = console.error;
break;
default:
this.logger = console.log;
}
}
log (...values) {
const message = new Message(config.namespace);
for (const value of values) message.add(value);
this.print(message);
}
print (message) {
message.setColor(this.color);
this.logger.apply(console, message.getMessage());
}
get color () {
return window.matchMedia('(prefers-color-scheme: dark)').matches ? this.dark : this.light;
}
}
class Message {
constructor (domain) {
this.inputs = ['%c'];
this.styles = ['font-family:Marianne', 'line-height: 1.5'];
this.objects = [];
if (domain) this.add(`${domain} :`);
}
add (value) {
switch (typeof value) {
case 'object':
case 'function':
this.inputs.push('%o ');
this.objects.push(value);
break;
default:
this.inputs.push(`${value} `);
}
}
setColor (color) {
this.styles.push(`color:${color}`);
}
getMessage () {
return [this.inputs.join(''), this.styles.join(';'), ...this.objects];
}
}
const LEVELS = {
log: new LogLevel(0, '#616161', '#989898'),
debug: new LogLevel(1, '#000091', '#8B8BFF'),
info: new LogLevel(2, '#007c3b', '#00ed70'),
warn: new LogLevel(3, '#ba4500', '#fa5c00', 'warn'),
error: new LogLevel(4, '#D80600', '#FF4641', 'error')
};
class Inspector {
constructor () {
this.level = 2;
for (const id in LEVELS) {
const level = LEVELS[id];
this[id] = (...msgs) => {
if (this.level <= level.level) level.log.apply(level, msgs);
};
this[id].print = level.print.bind(level);
}
}
state () {
const message = new Message();
message.add(state);
this.log.print(message);
}
tree () {
const stage = state.getModule('stage');
if (!stage) return;
const message = new Message();
this._branch(stage.root, 0, message);
this.log.print(message);
}
_branch (element, space, message) {
let branch = '';
if (space > 0) {
let indent = '';
for (let i = 0; i < space; i++) indent += ' ';
// branch += indent + '|\n';
branch += indent + '└─ ';
}
branch += `[${element.id}] ${element.html}`;
message.add(branch);
message.add({ '@': element });
message.add('\n');
for (const child of element.children) branch += this._branch(child, space + 1, message);
}
}
const inspector = new Inspector();
const startAtDomContentLoaded = (callback) => {
if (document.readyState !== 'loading') window.requestAnimationFrame(callback);
else document.addEventListener('DOMContentLoaded', callback);
};
const startAuto = (callback) => {
// detect
startAtDomContentLoaded(callback);
};
const Modes = {
AUTO: 'auto',
MANUAL: 'manual',
RUNTIME: 'runtime',
LOADED: 'loaded',
VUE: 'vue',
ANGULAR: 'angular',
REACT: 'react'
};
class Options {
constructor () {
this._mode = Modes.AUTO;
this.isStarted = false;
this.starting = this.start.bind(this);
this.preventManipulation = false;
}
configure (settings = {}, start, query) {
this.startCallback = start;
const isProduction = settings.production && (!query || query.production !== 'false');
switch (true) {
case query && !isNaN(query.level):
inspector.level = Number(query.level);
break;
case query && query.verbose && (query.verbose === 'true' || query.verbose === 1):
inspector.level = 0;
break;
case isProduction:
inspector.level = 999;
break;
case settings.verbose:
inspector.level = 0;
break;
}
inspector.info(`version ${config.version}`);
this.mode = settings.mode || Modes.AUTO;
}
set mode (value) {
switch (value) {
case Modes.AUTO:
this.preventManipulation = false;
startAuto(this.starting);
break;
case Modes.LOADED:
this.preventManipulation = false;
startAtDomContentLoaded(this.starting);
break;
case Modes.RUNTIME:
this.preventManipulation = false;
this.start();
break;
case Modes.MANUAL:
this.preventManipulation = false;
break;
case Modes.VUE:
this.preventManipulation = true;
break;
case Modes.ANGULAR:
this.preventManipulation = true;
break;
case Modes.REACT:
this.preventManipulation = true;
break;
default:
inspector.error('Illegal mode');
return;
}
this._mode = value;
inspector.info(`mode set to ${value}`);
}
get mode () {
return this._mode;
}
start () {
inspector.info('start');
this.startCallback();
}
}
const options = new Options();
class Collection {
constructor () {
this._collection = [];
}
forEach (callback) {
this._collection.forEach(callback);
}
map (callback) {
return this._collection.map(callback);
}
get length () {
return this._collection.length;
}
add (collectable) {
if (this._collection.indexOf(collectable) > -1) return false;
this._collection.push(collectable);
if (this.onAdd) this.onAdd();
if (this.onPopulate && this._collection.length === 1) this.onPopulate();
return true;
}
remove (collectable) {
const index = this._collection.indexOf(collectable);
if (index === -1) return false;
this._collection.splice(index, 1);
if (this.onRemove) this.onRemove();
if (this.onEmpty && this._collection.length === 0) this.onEmpty();
}
execute (...args) {
for (const collectable of this._collection) if (collectable) collectable.apply(null, args);
}
clear () {
this._collection.length = 0;
}
clone () {
const clone = new Collection();
clone._collection = this._collection.slice();
return clone;
}
get collection () {
return this._collection;
}
}
class Module extends Collection {
constructor (type) {
super();
this.type = type;
this.isActive = false;
}
activate () {}
deactivate () {}
}
const ns = name => `${config.prefix}-${name}`;
ns.selector = (name, notation) => {
if (notation === undefined) notation = '.';
return `${notation}${ns(name)}`;
};
ns.attr = (name) => `data-${ns(name)}`;
ns.attr.selector = (name, value) => {
let result = ns.attr(name);
if (value !== undefined) result += `="${value}"`;
return `[${result}]`;
};
ns.event = (type) => `${config.namespace}.${type}`;
ns.emission = (domain, type) => `emission:${domain}.${type}`;
const querySelectorAllArray = (element, selectors) => Array.prototype.slice.call(element.querySelectorAll(selectors));
const queryParentSelector = (element, selectors) => {
const parent = element.parentElement;
if (parent.matches(selectors)) return parent;
if (parent === document.documentElement) return null;
return queryParentSelector(parent, selectors);
};
class Registration {
constructor (selector, InstanceClass, creator) {
this.selector = selector;
this.InstanceClass = InstanceClass;
this.creator = creator;
this.instances = new Collection();
this.isIntroduced = false;
this._instanceClassName = this.InstanceClass.instanceClassName;
this._instanceClassNames = this.getInstanceClassNames(this.InstanceClass);
this._property = this._instanceClassName.substring(0, 1).toLowerCase() + this._instanceClassName.substring(1);
const dashed = this._instanceClassName
.replace(/[^a-zA-Z0-9]+/g, '-')
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1-$2')
.replace(/([a-z])([A-Z])/g, '$1-$2')
.replace(/([0-9])([^0-9])/g, '$1-$2')
.replace(/([^0-9])([0-9])/g, '$1-$2')
.toLowerCase();
this._attribute = ns.attr(`js-${dashed}`);
}
getInstanceClassNames (InstanceClass) {
const prototype = Object.getPrototypeOf(InstanceClass);
if (!prototype || prototype.instanceClassName === 'Instance') return [InstanceClass.instanceClassName];
return [...this.getInstanceClassNames(prototype), InstanceClass.instanceClassName];
}
hasInstanceClassName (instanceClassName) {
return this._instanceClassNames.indexOf(instanceClassName) > -1;
}
introduce () {
if (this.isIntroduced) return;
this.isIntroduced = true;
state.getModule('stage').parse(document.documentElement, this);
}
parse (node, nonRecursive) {
const nodes = [];
if (node.matches && node.matches(this.selector)) nodes.push(node);
// eslint-disable-next-line no-useless-call
if (!nonRecursive && node.querySelectorAll && node.querySelector(this.selector)) nodes.push.apply(nodes, querySelectorAllArray(node, this.selector));
return nodes;
}
create (element) {
if (!element.node.matches(this.selector)) return;
const instance = new this.InstanceClass();
this.instances.add(instance);
return instance;
}
remove (instance) {
this.instances.remove(instance);
}
dispose () {
const instances = this.instances.collection;
for (let i = instances.length - 1; i > -1; i--) instances[i]._dispose();
this.creator = null;
}
get instanceClassName () {
return this._instanceClassName;
}
get instanceClassNames () {
return this._instanceClassNames;
}
get property () {
return this._property;
}
get attribute () {
return this._attribute;
}
}
class Register extends Module {
constructor () {
super('register');
}
register (selector, InstanceClass, creator) {
const registration = new Registration(selector, InstanceClass, creator);
this.add(registration);
if (state.isActive) registration.introduce();
return registration;
}
activate () {
for (const registration of this.collection) registration.introduce();
}
remove (registration) {
registration.dispose();
super.remove(registration);
}
}
let count = 0;
class Element$1 {
constructor (node, id) {
if (!id) {
count++;
this.id = count;
} else this.id = id;
this.node = node;
this.attributeNames = [];
this.instances = [];
this._children = [];
this._parent = null;
this._projects = [];
}
get proxy () {
const scope = this;
if (!this._proxy) {
this._proxy = {
id: this.id,
get parent () {
return scope.parent ? scope.parent.proxy : null;
},
get children () {
return scope.children.map((child) => child.proxy);
}
};
for (const instance of this.instances) this._proxy[instance.registration.property] = instance.proxy;
}
return this._proxy;
}
get html () {
if (!this.node || !this.node.outerHTML) return '';
const end = this.node.outerHTML.indexOf('>');
return this.node.outerHTML.substring(0, end + 1);
}
project (registration) {
if (this._projects.indexOf(registration) === -1) this._projects.push(registration);
}
populate () {
const projects = this._projects.slice();
this._projects.length = 0;
for (const registration of projects) this.create(registration);
}
create (registration) {
if (this.hasInstance(registration.instanceClassName)) {
// inspector.debug(`failed creation, instance of ${registration.instanceClassName} already exists on element [${this.id}]`);
return;
}
inspector.debug(`create instance of ${registration.instanceClassName} on element [${this.id}]`);
const instance = registration.create(this);
this.instances.push(instance);
instance._config(this, registration);
if (this._proxy) this._proxy[registration.property] = instance.proxy;
}
remove (instance) {
const index = this.instances.indexOf(instance);
if (index > -1) this.instances.splice(index, 1);
if (this._proxy) delete this._proxy[instance.registration.property];
}
get parent () {
return this._parent;
}
get ascendants () {
return [this.parent, ...this.parent.ascendants];
}
get children () {
return this._children;
}
get descendants () {
const descendants = [...this._children];
this._children.forEach(child => descendants.push(...child.descendants));
return descendants;
}
// TODO : emit ascendant et descendant de changement ?
addChild (child, index) {
if (this._children.indexOf(child) > -1) return null;
child._parent = this;
if (!isNaN(index) && index > -1 && index < this._children.length) this._children.splice(index, 0, child);
else this._children.push(child);
return child;
}
removeChild (child) {
const index = this._children.indexOf(child);
if (index === -1) return null;
child._parent = null;
this._children.splice(index, 1);
}
emit (type, data) {
const elements = state.getModule('stage').collection;
const response = [];
for (const element of elements) response.push(...element._emit(type, data));
return response;
}
_emit (type, data) {
const response = [];
for (const instance of this.instances) response.push(...instance._emitter.emit(type, data));
return response;
}
ascend (type, data) {
if (this._parent) return this._parent._ascend(type, data);
return [];
}
_ascend (type, data) {
const response = [];
for (const instance of this.instances) response.push(...instance._ascent.emit(type, data));
if (this._parent) response.push(...this._parent._ascend(type, data));
return response;
}
descend (type, data) {
const response = [];
for (const child of this._children) response.push(...child._descend(type, data));
return response;
}
_descend (type, data) {
const response = [];
for (const instance of this.instances) response.push(...instance._descent.emit(type, data));
for (const child of this._children) response.push(...child._descend(type, data));
return response;
}
getInstance (instanceClassName) {
for (const instance of this.instances) if (instance.registration.hasInstanceClassName(instanceClassName)) return instance;
return null;
}
hasInstance (instanceClassName) {
return this.getInstance(instanceClassName) !== null;
}
getDescendantInstances (instanceClassName, stopAtInstanceClassName, stopAtFirstInstance) {
if (!instanceClassName) return [];
const instances = [];
for (const child of this._children) {
const instance = child.getInstance(instanceClassName);
if (instance) {
instances.push(instance);
if (stopAtFirstInstance) continue;
}
if ((!stopAtInstanceClassName || !child.hasInstance(stopAtInstanceClassName)) && child.children.length) instances.push.apply(instances, child.getDescendantInstances(instanceClassName, stopAtInstanceClassName, stopAtFirstInstance));
}
return instances;
}
getAscendantInstance (instanceClassName, stopAtInstanceClassName) {
if (!instanceClassName || !this._parent) return null;
const instance = this._parent.getInstance(instanceClassName);
if (instance) return instance;
if (stopAtInstanceClassName && this._parent.hasInstance(stopAtInstanceClassName)) return null;
return this._parent.getAscendantInstance(instanceClassName, stopAtInstanceClassName);
}
dispose () {
for (let i = this.instances.length - 1; i >= 0; i--) {
const instance = this.instances[i];
if (instance) instance._dispose();
}
this.instances.length = 0;
state.remove('stage', this);
this.parent.removeChild(this);
this._children.length = 0;
inspector.debug(`remove element [${this.id}] ${this.html}`);
}
prepare (attributeName) {
if (this.attributeNames.indexOf(attributeName) === -1) this.attributeNames.push(attributeName);
}
examine () {
const attributeNames = this.attributeNames.slice();
this.attributeNames.length = 0;
for (let i = this.instances.length - 1; i > -1; i--) this.instances[i].examine(attributeNames);
}
}
const RootEmission = {
CLICK: ns.emission('root', 'click'),
KEYDOWN: ns.emission('root', 'keydown'),
KEYUP: ns.emission('root', 'keyup')
};
const KeyCodes = {
TAB: {
id: 'tab',
value: 9
},
ESCAPE: {
id: 'escape',
value: 27
},
END: {
id: 'end',
value: 35
},
HOME: {
id: 'home',
value: 36
},
LEFT: {
id: 'left',
value: 37
},
UP: {
id: 'up',
value: 38
},
RIGHT: {
id: 'right',
value: 39
},
DOWN: {
id: 'down',
value: 40
}
};
const getKeyCode = (keyCode) => Object.values(KeyCodes).filter(entry => entry.value === keyCode)[0];
class Root extends Element$1 {
constructor () {
super(document.documentElement, 'root');
this.node.setAttribute(ns.attr('js'), true);
this.listen();
}
listen () {
// TODO v2 => listener au niveau des éléments qui redistribuent aux instances.
document.documentElement.addEventListener('click', this.click.bind(this), { capture: true });
document.documentElement.addEventListener('keydown', this.keydown.bind(this), { capture: true });
document.documentElement.addEventListener('keyup', this.keyup.bind(this), { capture: true });
}
click (e) {
this.emit(RootEmission.CLICK, e.target);
}
keydown (e) {
this.emit(RootEmission.KEYDOWN, getKeyCode(e.keyCode));
}
keyup (e) {
this.emit(RootEmission.KEYUP, getKeyCode(e.keyCode));
}
}
class Stage extends Module {
constructor () {
super('stage');
this.root = new Root();
super.add(this.root);
this.observer = new MutationObserver(this.mutate.bind(this));
this.modifications = [];
this.willModify = false;
this.modifying = this.modify.bind(this);
}
hasElement (node) {
for (const element of this.collection) if (element.node === node) return true;
return false;
}
getElement (node) {
for (const element of this.collection) if (element.node === node) return element;
const element = new Element$1(node);
this.add(element);
inspector.debug(`add element [${element.id}] ${element.html}`);
return element;
}
getProxy (node) {
if (!this.hasElement(node)) return null;
const element = this.getElement(node);
return element.proxy;
}
add (element) {
super.add(element);
this.put(element, this.root);
}
put (element, branch) {
let index = 0;
for (let i = branch.children.length - 1; i > -1; i--) {
const child = branch.children[i];
const position = element.node.compareDocumentPosition(child.node);
if (position & Node.DOCUMENT_POSITION_CONTAINS) {
this.put(element, child);
return;
} else if (position & Node.DOCUMENT_POSITION_CONTAINED_BY) {
branch.removeChild(child);
element.addChild(child, 0);
} else if (position & Node.DOCUMENT_POSITION_PRECEDING) {
index = i + 1;
break;
}
}
branch.addChild(element, index);
}
activate () {
this.observer.observe(document.documentElement, { childList: true, subtree: true, attributes: true });
}
deactivate () {
this.observer.disconnect();
}
mutate (mutations) {
const examinations = [];
mutations.forEach((mutation) => {
switch (mutation.type) {
case 'childList':
mutation.removedNodes.forEach((node) => this.dispose(node));
mutation.addedNodes.forEach((node) => this.parse(node));
break;
case 'attributes':
if (this.hasElement(mutation.target)) {
const element = this.getElement(mutation.target);
element.prepare(mutation.attributeName);
if (examinations.indexOf(element) === -1) examinations.push(element);
for (const descendant of element.descendants) if (examinations.indexOf(descendant) === -1) examinations.push(descendant);
}
if (this.modifications.indexOf(mutation.target) === -1) this.modifications.push(mutation.target);
break;
}
});
examinations.forEach(element => element.examine());
if (this.modifications.length && !this.willModify) {
this.willModify = true;
window.requestAnimationFrame(this.modifying);
}
}
modify () {
this.willModify = false;
const targets = this.modifications.slice();
this.modifications.length = 0;
for (const target of targets) if (document.documentElement.contains(target)) this.parse(target);
}
dispose (node) {
const disposables = [];
this.forEach((element) => {
if (node.contains(element.node)) disposables.push(element);
});
for (const disposable of disposables) {
disposable.dispose();
this.remove(disposable);
}
}
parse (node, registration, nonRecursive) {
const registrations = registration ? [registration] : state.getModule('register').collection;
const creations = [];
for (const registration of registrations) {
const nodes = registration.parse(node, nonRecursive);
for (const n of nodes) {
const element = this.getElement(n);
element.project(registration);
if (creations.indexOf(element) === -1) creations.push(element);
}
}
for (const element of creations) element.populate();
}
}
class Renderer extends Module {
constructor () {
super('render');
this.rendering = this.render.bind(this);
this.nexts = new Collection();
}
activate () {
window.requestAnimationFrame(this.rendering);
}
request (instance) {
this.nexts.add(instance);
}
render () {
if (!state.isActive) return;
window.requestAnimationFrame(this.rendering);
this.forEach((instance) => instance.render());
if (!this.nexts.length) return;
const nexts = this.nexts.clone();
this.nexts.clear();
nexts.forEach((instance) => instance.next());
}
}
class Resizer extends Module {
constructor () {
super('resize');
this.requireResize = false;
this.resizing = this.resize.bind(this);
const requesting = this.request.bind(this);
if (document.fonts) {
document.fonts.ready.then(requesting);
}
window.addEventListener('resize', requesting);
window.addEventListener('orientationchange', requesting);
}
activate () {
this.request();
}
request () {
if (this.requireResize) return;
this.requireResize = true;
window.requestAnimationFrame(this.resizing);
}
resize () {
if (!this.requireResize) return;
this.forEach((instance) => instance.resize());
this.requireResize = false;
}
}
class ScrollLocker extends Module {
constructor () {
super('lock');
this._isLocked = false;
this._scrollY = 0;
this.onPopulate = this.lock.bind(this);
this.onEmpty = this.unlock.bind(this);
}
get isLocked () {
return this._isLocked;
}
lock () {
if (!this._isLocked) {
this._isLocked = true;
this._scrollY = window.scrollY;
const scrollBarGap = window.innerWidth - document.documentElement.clientWidth;
document.documentElement.setAttribute(ns.attr('scrolling'), 'false');
document.body.style.top = `${-this._scrollY}px`;
this.behavior = getComputedStyle(document.documentElement).getPropertyValue('scroll-behavior');
if (this.behavior === 'smooth') document.documentElement.style.scrollBehavior = 'auto';
if (scrollBarGap > 0) {
document.documentElement.style.setProperty('--scrollbar-width', `${scrollBarGap}px`);
}
}
}
unlock () {
if (this._isLocked) {
this._isLocked = false;
document.documentElement.removeAttribute(ns.attr('scrolling'));
document.body.style.top = '';
window.scrollTo(0, this._scrollY);
if (this.behavior === 'smooth') document.documentElement.style.removeProperty('scroll-behavior');
document.documentElement.style.removeProperty('--scrollbar-width');
}
}
move (value) {
if (this._isLocked) {
this._scrollY += value;
document.body.style.top = `${-this._scrollY}px`;
} else {
window.scrollTo(0, window.scrollY + value);
}
}
}
class Load extends Module {
constructor () {
super('load');
this.loading = this.load.bind(this);
}
activate () {
window.addEventListener('load', this.loading);
}
load () {
this.forEach((instance) => instance.load());
}
}
const FONT_FAMILIES = ['Marianne', 'Spectral'];
class FontSwap extends Module {
constructor () {
super('font-swap');
this.swapping = this.swap.bind(this);
}
activate () {
if (document.fonts) {
document.fonts.addEventListener('loadingdone', this.swapping);
}
}
swap () {
const families = FONT_FAMILIES.filter(family => document.fonts.check(`16px ${family}`));
this.forEach((instance) => instance.swapFont(families));
}
}
class MouseMove extends Module {
constructor () {
super('mouse-move');
this.requireMove = false;
this._isMoving = false;
this.moving = this.move.bind(this);
this.requesting = this.request.bind(this);
this.onPopulate = this.listen.bind(this);
this.onEmpty = this.unlisten.bind(this);
}
listen () {
if (this._isMoving) return;
this._isMoving = true;
this.requireMove = false;
document.documentElement.addEventListener('mousemove', this.requesting);
}
unlisten () {
if (!this._isMoving) return;
this._isMoving = false;
this.requireMove = false;
document.documentElement.removeEventListener('mousemove', this.requesting);
}
request (e) {
if (!this._isMoving) return;
this.point = { x: e.clientX, y: e.clientY };
if (this.requireMove) return;
this.requireMove = true;
window.requestAnimationFrame(this.moving);
}
move () {
if (!this.requireMove) return;
this.forEach((instance) => instance.mouseMove(this.point));
this.requireMove = false;
}
}
class Hash extends Module {
constructor () {
super('hash');
this.handling = this.handle.bind(this);
this.getLocationHash();
}
activate () {
window.addEventListener('hashchange', this.handling);
}
deactivate () {
window.removeEventListener('hashchange', this.handling);
}
_sanitize (hash) {
if (hash.charAt(0) === '#') return hash.substring(1);
return hash;
}
set hash (value) {
const hash = this._sanitize(value);
if (this._hash !== hash) window.location.hash = hash;
}
get hash () {
return this._hash;
}
getLocationHash () {
const hash = window.location.hash;
this._hash = this._sanitize(hash);
}
handle (e) {
this.getLocationHash();
this.forEach((instance) => instance.handleHash(this._hash, e));
}
}
class Engine {
constructor () {
state.create(Register);
state.create(Stage);
state.create(Renderer);
state.create(Resizer);
state.create(ScrollLocker);
state.create(Load);
state.create(FontSwap);
state.create(MouseMove);
state.create(Hash);
const registerModule = state.getModule('register');
this.register = registerModule.register.bind(registerModule);
}
get isActive () {
return state.isActive;
}
start () {
inspector.debug('START');
state.isActive = true;
}
stop () {
inspector.debug('STOP');
state.isActive = false;
}
}
const engine = new Engine();
class Colors {
getColor (context, use, tint, options = {}) {
const option = getOption(options);
const decision = `--${context}-${use}-${tint}${option}`;
return getComputedStyle(document.documentElement).getPropertyValue(decision).trim() || null;
}
}
const getOption = (options) => {
switch (true) {
case options.hover:
return '-hover';
case options.active:
return '-active';
default:
return '';
}
};
const colors = new Colors();
const sanitize = (className) => className.charAt(0) === '.' ? className.substr(1) : className;
const getClassNames = (element) => {
switch (true) {
case !element.className:
return [];
case typeof element.className === 'string':
return element.className.split(' ');
case typeof element.className.baseVal === 'string':
return element.className.baseVal.split(' ');
}
return [];
};
const modifyClass = (element, className, remove) => {
className = sanitize(className);
const classNames = getClassNames(element);
const index = classNames.indexOf(className);
if (remove === true) {
if (index > -1) classNames.splice(index, 1);
} else if (index === -1) classNames.push(className);
element.className = classNames.join(' ');
};
const addClass = (element, className) => modifyClass(element, className);
const removeClass = (element, className) => modifyClass(element, className, true);
const hasClass = (element, className) => getClassNames(element).indexOf(sanitize(className)) > -1;
const ACTIONS = [
'[tabindex]:not([tabindex="-1"])',
'a[href]',
'button:not([disabled])',
'input:not([disabled])',
'select:not([disabled])',
'textarea:not([disabled])',
'audio[controls]',
'video[controls]',
'[contenteditable]:not([contenteditable="false"])',
'details>summary:first-of-type',
'details',
'iframe'
];
const ACTIONS_SELECTOR = ACTIONS.join();
const queryActions = (element) => {
return element.querySelectorAll(ACTIONS_SELECTOR);
};
let counter = 0;
const uniqueId = (id) => {
if (!document.getElementById(id)) return id;
let element = true;
const base = id;
while (element) {
counter++;
id = `${base}-${counter}`;
element = document.getElementById(id);
}
return id;
};
const dom = {
addClass: addClass,
hasClass: hasClass,
removeClass: removeClass,
queryParentSelector: queryParentSelector,
querySelectorAllArray: querySelectorAllArray,
queryActions: queryActions,
uniqueId: uniqueId
};
class DataURISVG {
constructor (width = 0, height = 0) {
this._width = width;
this._height = height;
this._content = '';
}
get width () {
return this._width;
}
set width (value) {
this._width = value;
}
get height () {
return this._height;
}
set height (value) {
this._height = value;
}
get content () {
return this._content;
}
set content (value) {
this._content = value;
}
getDataURI (isLegacy = false) {
let svg = `<svg xmlns='http://www.w3.org/2000/svg' viewbox='0 0 ${this._width} ${this._height}' width='${this._width}px' height='${this._height}px'>${this._content}</svg>`;
svg = svg.replace(/#/gi, '%23');
if (isLegacy) {
svg = svg.replace(/</gi, '%3C');
svg = svg.replace(/>/gi, '%3E');
svg = svg.replace(/"/gi, '\'');
svg = svg.replace(/{/gi, '%7B');
svg = svg.replace(/}/gi, '%7D');
}
return `data:image/svg+xml;charset=utf8,${svg}`;
}
}
const image = {
DataURISVG: DataURISVG
};
const supportLocalStorage = () => {
try {
return 'localStorage' in window && window.localStorage !== null;
} catch (e) {
return false;
}
};
const supportAspectRatio = () => {
if (!window.CSS) return false;
return CSS.supports('aspect-ratio: 16 / 9');
};
const support = {
supportLocalStorage: supportLocalStorage,
supportAspectRatio: supportAspectRatio
};
const TransitionSelector = {
NONE: ns.selector('transition-none')
};
const selector = {
TransitionSelector: TransitionSelector
};
/**
* Copy properties from multiple sources including accessors.
* source : https://developer.mozilla.org/fr/docs/Web/JavaScript/Reference/Global_Objects/Object/assign#copier_des_accesseurs
*
* @param {object} [target] - Target object to copy into
* @param {...objects} [sources] - Multiple objects
* @return {object} A new object
*
* @example
*
* const obj1 = {
* key: 'value'
* };
* const obj2 = {
* get function01 () {
* return a-value;
* }
* set function01 () {
* return a-value;
* }
* };
* completeAssign(obj1, obj2)
*/
const completeAssign = (target, ...sources) => {
sources.forEach(source => {
const descriptors = Object.keys(source).reduce((descriptors, key) => {
descriptors[key] = Object.getOwnPropertyDescriptor(source, key);
return descriptors;
}, {});
Object.getOwnPropertySymbols(source).forEach(sym => {
const descriptor = Object.getOwnPropertyDescriptor(source, sym);
if (descriptor.enumerable) {
descriptors[sym] = descriptor;
}
});
Object.defineProperties(target, descriptors);
});
return target;
};
const property = {
completeAssign: completeAssign
};
/**
* Return an object of query params or null
*
* @method
* @name searchParams
* @param {string} url - an url
* @returns {Object} object of query params or null
*/
const searchParams = (url) => {
if (url && url.search) {
const params = new URLSearchParams(window.location.search);
const entries = params.entries();
return Object.fromEntries(entries);
}
return null;
};
const internals = {};
const legacy = {};
Object.defineProperty(legacy, 'isLegacy', {
get: () => state.isLegacy
});
legacy.setLegacy = () => {
state.isLegacy = true;
};
internals.legacy = legacy;
internals.dom = dom;
internals.image = image;
internals.support = support;
internals.motion = selector;
internals.property = property;
internals.ns = ns;
internals.register = engine.register;
internals.state = state;
internals.query = searchParams(window.location);
Object.defineProperty(internals, 'preventManipulation', {
get: () => options.preventManipulation
});
Object.defineProperty(internals, 'stage', {
get: () => state.getModule('stage')
});
const api$1 = (node) => {
const stage = state.getModule('stage');
return stage.getProxy(node);
};
api$1.version = config.version;
api$1.prefix = config.prefix;
api$1.organisation = config.organisation;
api$1.Modes = Modes;
Object.defineProperty(api$1, 'mode', {
set: (value) => { options.mode = value; },
get: () => options.mode
});
api$1.internals = internals;
api$1.version = config.version;
api$1.start = engine.start;
api$1.stop = engine.stop;
api$1.inspector = inspector;
api$1.colors = colors;
const configuration = window[config.namespace];
api$1.internals.configuration = configuration;
options.configure(configuration, api$1.start, api$1.internals.query);
window[config.namespace] = api$1;
class Emitter {
constructor () {
this.emissions = {};
}
add (type, closure) {
if (typeof closure !== 'function') throw new Error('closure must be a function');
if (!this.emissions[type]) this.emissions[type] = [];
this.emissions[type].push(closure);
}
remove (type, closure) {
if (!this.emissions[type]) return;
if (!closure) delete this.emissions[type];
else {
const index = this.emissions[type].indexOf(closure);
if (index > -1) this.emissions[type].splice(index);
}
}
emit (type, data) {
if (!this.emissions[type]) return [];
const response = [];
for (const closure of this.emissions[type]) if (closure) response.push(closure(data));
return response;
}
dispose () {
this.emissions = null;
}
}
class Breakpoint {
constructor (id, minWidth) {
this.id = id;
this.minWidth = minWidth;
}
test () {
return window.matchMedia(`(min-width: ${this.minWidth}em)`).matches;
}
}
const Breakpoints = {
XS: new Breakpoint('xs', 0),
SM: new Breakpoint('sm', 36),
MD: new Breakpoint('md', 48),
LG: new Breakpoint('lg', 62),
XL: new Breakpoint('xl', 78)
};
class Instance {
constructor (jsAttribute = true) {
this.jsAttribute = jsAttribute;
this._isRendering = false;
this._isResizing = false;
this._isScrollLocked = false;
this._isLoading = false;
this._isSwappingFont = false;
this._isEnabled = true;
this._isDisposed = false;
this._listeners = {};
this._handlingClick = this.handleClick.bind(this);
this._hashes = [];
this._hash = '';
this._keyListenerTypes = [];
this._keys = [];
this.handlingKey = this.handleKey.bind(this);
this._emitter = new Emitter();
this._ascent = new Emitter();
this._descent = new Emitter();
this._registrations = [];
this._nexts = [];
}
static get instanceClassName () {
return 'Instance';
}
_config (element, registration) {
this.element = element;
this.registration = registration;
this.node = element.node;
this.id = element.node.id;
if (this.jsAttribute) this.setAttribute(registration.attribute, true);
this.init();
}
init () {}
get proxy () {
const scope = this;
const proxy = {
render: () => scope.render(),
resize: () => scope.resize()
};
const proxyAccessors = {
get node () {
return this.node;
},
get isEnabled () {
return scope.isEnabled;
},
set isEnabled (value) {
scope.isEnabled = value;
}
};
return completeAssign(proxy, proxyAccessors);
}
log (...values) {
values.unshift(`${this.registration.instanceClassName} #${this.id} - `);
inspector.log.apply(inspector, values);
}
debug (...values) {
values.unshift(`${this.registration.instanceClassName} #${this.id} - `);
inspector.debug.apply(inspector, values);
}
info (...values) {
values.unshift(`${this.registration.instanceClassName} #${this.id} - `);
inspector.info.apply(inspector, values);
}
warn (...values) {
values.unshift(`${this.registration.instanceClassName} #${this.id} - `);
inspector.warn.apply(inspector, values);
}
error (...values) {
values.unshift(`${this.registration.instanceClassName} #${this.id} - `);
inspector.error.apply(inspector, values);
}
register (selector, InstanceClass) {
const registration = state.getModule('register').register(selector, InstanceClass, this);
this._registrations.push(registration);
}
getRegisteredInstances (instanceClassName) {
for (const registration of this._registrations) if (registration.hasInstanceClassName(instanceClassName)) return registration.instances.collection;
return [];
}
dispatch (type, detail, bubbles, cancelable) {
const event = new CustomEvent(type, { detail: detail, bubble: bubbles === true, cancelable: cancelable === true });
this.node.dispatchEvent(event);
}
// TODO v2 => listener au niveau des éléments qui redistribuent aux instances.
listen (type, closure, options) {
if (!this._listeners[type]) this._listeners[type] = [];
const listeners = this._listeners[type];
// if (listeners.some(listener => listener.closure === closure)) return;
const listener = new Listener(this.node, type, closure, options);
listeners.push(listener);
listener.listen();
}
unlisten (type, closure, options) {
if (!type) {
for (const type in this._listeners) this.unlisten(type);
return;
}
const listeners = this._listeners[type];
if (!listeners) return;
if (!closure) {
listeners.forEach(listener => this.unlisten(type, listener.closure));
return;
}
const removal = listeners.filter(listener => listener.closure === closure && listener.matchOptions(options));
removal.forEach(listener => listener.unlisten());
this._listeners[type] = listeners.filter(listener => removal.indexOf(listener) === -1);
}
listenClick (options) {
this.listen('click', this._handlingClick, options);
}
unlistenClick (options) {
this.unlisten('click', this._handlingClick, options);
}
handleClick (e) {}
set hash (value) {
state.getModule('hash').hash = value;
}
get hash () {
return state.getModule('hash').hash;
}
listenHash (hash, add) {
if (!this._hashes) return;
if (this._hashes.length === 0) state.add('hash', this);
const action = new HashAction(hash, add);
this._hashes = this._hashes.filter(action => action.hash !== hash);
this._hashes.push(action);
}
unlistenHash (hash) {
if (!this._hashes) return;
this._hashes = this._hashes.filter(action => action.hash !== hash);
if (this._hashes.length === 0) state.remove('hash', this);
}
handleHash (hash, e) {
if (!this._hashes) return;
for (const action of this._hashes) action.handle(hash, e);
}
listenKey (keyCode, closure, preventDefault = false, stopPropagation = false, type = 'down') {
if (this._keyListenerTypes.indexOf(type) === -1) {
this.listen(`key${type}`, this.handlingKey);
this._keyListenerTypes.push(type);
}
this._keys.push(new KeyAction(type, keyCode, closure, preventDefault, stopPropagation));
}
unlistenKey (code, closure) {
this._keys = this._keys.filter((key) => key.code !== code || key.closure !== closure);
this._keyListenerTypes.forEach(type => {
if (!this._keys.some(key => key.type === type)) this.unlisten(`key${type}`, this.handlingKey);
});
}
handleKey (e) {
for (const key of this._keys) key.handle(e);
}
get isEnabled () { return this._isEnabled; }
set isEnabled (value) {
this._isEnabled = value;
}
get isRendering () { return this._isRendering; }
set isRendering (value) {
if (this._isRendering === value) return;
if (value) state.add('render', this);
else state.remove('render', this);
this._isRendering = value;
}
render () {}
request (closure) {
this._nexts.push(closure);
state.getModule('render').request(this);
}
next () {
const nexts = this._nexts.slice();
this._nexts.length = 0;
for (const closure of nexts) if (closure) closure();
}
get isResizing () { return this._isResizing; }
set isResizing (value) {
if (this._isResizing === value) return;
if (value) {
state.add('resize', this);
this.resize();
} else state.remove('resize', this);
this._isResizing = value;
}
resize () {}
isBreakpoint (breakpoint) {
switch (true) {
case typeof breakpoint === 'string':
return Breakpoints[breakpoint.toUpperCase()].test();
default:
return breakpoint.test();
}
}
get isScrollLocked () {
return this._isScrollLocked;
}
set isScrollLocked (value) {
if (this._isScrollLocked === value) return;
if (value) state.add('lock', this);
else state.remove('lock', this);
this._isScrollLocked = value;
}
get isLoading () {
return this._isLoading;
}
set isLoading (value) {
if (this._isLoading === value) return;
if (value) state.add('load', this);
else state.remove('load', this);
this._isLoading = value;
}
load () {}
get isSwappingFont () {
return this._isSwappingFont;
}
set isSwappingFont (value) {
if (this._isSwappingFont === value) return;
if (value) state.add('font-swap', this);
else state.remove('font-swap', this);
this._isSwappingFont = value;
}
swapFont () {}
get isMouseMoving () { return this._isMouseMoving; }
set isMouseMoving (value) {
if (this._isMouseMoving === value) return;
if (value) {
state.add('mouse-move', this);
} else {
state.remove('mouse-move', this);
}
this._isMouseMoving = value;
}
mouseMove (point) {}
examine (attributeNames) {
if (!this.node.matches(this.registration.selector)) {
this._dispose();
return;
}
this.mutate(attributeNames);
}
mutate (attributeNames) {}
retrieveNodeId (node, append) {
if (node.id) return node.id;
const id = uniqueId(`${this.id}-${append}`);
this.warn(`add id '${id}' to ${append}`);
node.setAttribute('id', id);
return id;
}
get isDisposed () {
return this._isDisposed;
}
_dispose () {
this.debug(`dispose instance of ${this.registration.instanceClassName} on element [${this.element.id}]`);
this.removeAttribute(this.registration.attribute);
this.unlisten();
state.remove('hash', this);
this._hashes = null;
this._keys = null;
this.isRendering = false;
this.isResizing = false;
this._nexts = null;
state.getModule('render').nexts.remove(this);
this.isScrollLocked = false;
this.isLoading = false;
this.isSwappingFont = false;
this.isMouseMoving = false;
this._emitter.dispose();
this._emitter = null;
this._ascent.dispose();
this._ascent = null;
this._descent.dispose();
this._descent = null;
this.element.remove(this);
for (const registration of this._registrations) state.remove('register', registration);
this._registrations = null;
this.registration.remove(this);
this._isDisposed = true;
this.dispose();
}
dispose () {}
emit (type, data) {
return this.element.emit(type, data);
}
addEmission (type, closure) {
this._emitter.add(type, closure);
}
removeEmission (type, closure) {
this._emitter.remove(type, closure);
}
ascend (type, data) {
return this.element.ascend(type, data);
}
addAscent (type, closure) {
this._ascent.add(type, closure);
}
removeAscent (type, closure) {
this._ascent.remove(type, closure);
}
descend (type, data) {
return this.element.descend(type, data);
}
addDescent (type, closure) {
this._descent.add(type, closure);
}
removeDescent (type, closure) {
this._descent.remove(type, closure);
}
get style () {
return this.node.style;
}
addClass (className) {
addClass(this.node, className);
}
removeClass (className) {
removeClass(this.node, className);
}
hasClass (className) {
return hasClass(this.node, className);
}
get classNames () {
return getClassNames(this.node);
}
remove () {
this.node.parentNode.removeChild(this.node);
}
setAttribute (attributeName, value) {
this.node.setAttribute(attributeName, value);
}
getAttribute (attributeName) {
return this.node.getAttribute(attributeName);
}
hasAttribute (attributeName) {
return this.node.hasAttribute(attributeName);
}
removeAttribute (attributeName) {
this.node.removeAttribute(attributeName);
}
setProperty (propertyName, value) {
this.node.style.setProperty(propertyName, value);
}
removeProperty (propertyName) {
this.node.style.removeProperty(propertyName);
}
focus () {
this.node.focus();
}
blur () {
this.node.blur();
}
focusClosest () {
const closest = this._focusClosest(this.node.parentNode);
if (closest) closest.focus();
}
_focusClosest (parent) {
if (!parent) return null;
const actions = [...queryActions(parent)];
if (actions.length <= 1) {
return this._focusClosest(parent.parentNode);
} else {
const index = actions.indexOf(this.node);
return actions[index + (index < actions.length - 1 ? 1 : -1)];
}
}
get hasFocus () {
return this.node === document.activeElement;
}
scrollIntoView () {
const rect = this.getRect();
const scroll = state.getModule('lock');
if (rect.top < 0) {
scroll.move(rect.top - 50);
}
if (rect.bottom > window.innerHeight) {
scroll.move(rect.bottom - window.innerHeight + 50);
}
}
matches (selectors) {
return this.node.matches(selectors);
}
querySelector (selectors) {
return this.node.querySelector(selectors);
}
querySelectorAll (selectors) {
return querySelectorAllArray(this.node, selectors);
}
queryParentSelector (selectors) {
return queryParentSelector(this.node, selectors);
}
getRect () {
const rect = this.node.getBoundingClientRect();
rect.center = rect.left + rect.width * 0.5;
rect.middle = rect.top + rect.height * 0.5;
return rect;
}
get isLegacy () {
return state.isLegacy;
}
}
class KeyAction {
constructor (type, keyCode, closure, preventDefault, stopPropagation) {
this.type = type;
this.eventType = `key${type}`;
this.keyCode = keyCode;
this.closure = closure;
this.preventDefault = preventDefault === true;
this.stopPropagation = stopPropagation === true;
}
handle (e) {
if (e.type !== this.eventType) return;
if (e.keyCode === this.keyCode.value) {
this.closure(e);
if (this.preventDefault) {
e.preventDefault();
}
if (this.stopPropagation) {
e.stopPropagation();
}
}
}
}
class Listener {
constructor (node, type, closure, options) {
this._node = node;
this._type = type;
this._closure = closure;
this._options = options;
}
get closure () {
return this._closure;
}
listen () {
this._node.addEventListener(this._type, this._closure, this._options);
}
matchOptions (options = null) {
switch (true) {
cas