Some quick example text to build on the card title and make up the bulk of the card's content.
Hanako TS
is a lighweight Typescript framework to manipulate DOM. It also offers a simple way of organizing website code around "components". Its behaviour ($ shortcut, chaining, events, etc.) is heavily based on the greate jQuery.
Its easy to use Boostrap 5
and Three.js
inside of your project.
You are free to use it under MIT License.
For a complete informations on Class, Methods, etc. please read documentation on /docs/
folder.
Compontents are the core of your website. the Component
class allows you to organize your code in a simple way. Components can be directyl initialized in your main .ts
file. In the example below, component folder is not inside TS folder as components may contain other files (assets, scss, etc.).
my-website-project
├── index.html
├── compontents
| ├── MyFirstCompontent
| | └── index.ts
| └── ...
└── ts
└── index.ts
First of all you need to install Hanako framework
npm init
npm install hanako-ts --save-dev
Files in hanako-ts/dist
are compiled in es2020
. If you need compatibility with es2015
, please use hanako-ts/dist-legacy
import { Component } from 'hanako-ts/dist/Component';
export class MyFirstCompontent extends Component {
//you can customize constructor parameters if needed
constructor() {
// use super('Compontent name', true) if you want your compontent to be initialized after image are loaded
super('Compontent name', false);
// to some stuff
}
public async init(): Promise {
await super.init();
// to some stuff
this.success();
}
// add methods here
}
import { MyFirstCompontent } from '../components/MyFirstCompontent';
(new MyFirstCompontent()).init();
A Collection
is a set of HTMLElement
, Nodes
, etc. It offers a lot of function for iteration, traversing, manipulation, css, events, etc.
Jump to Selector, Manipulation, Traversing, HTML, CSS, Dimensions, Events
Collection
accept the following elements as selector:
HTMLCollection
, NodeList
, GenericElement
, GenericElement[]
Collection
(dupliacate a Collection
)Collection
)const element1: Collection = new Collection('any-valid-css-selector'); // default method
const element2: Collection = $('any-valid-css-selector'); // $ shortcut
const element3: Collection = $(anOtherHTMLElement);
Of course functions can be chained. They always return the Collection
item (except for value getter function).
$('any-valid-css-selector').prev().css('color', 'red').on('click', () => { ... });
<p id="selector-demo">I'm a paragraph with an some <code id="selector-demo"></code></p>
I'm a paragraph with an id="selector-demo"
Please open browser console and look at "Selector demo" group for detailed console.log()
A Collection
is a list of Elem
(whatever HTMLElement ot HTMLxxxElement) than can be manipulated in various ways.
$('selector').length; // items count
$('selector').forEach((item: Elem) => { ... }); // looping collection's item as Elem
$('selector').each((item: Collection) => { ... }); // looping collection's item as Collection
$('selector').get(index); // get a single Elem
$('selector').eq(index); // get a single element as Collection
$('selector').add(item); // add a new Elem
$('selector').search('selector'); // search of element matching selector in collection
<p id="manipulation-demo">I'm a paragraph with an some <em>italic</em>, <strong>Bold</strong> and <span>span items</span></p>
I'm a paragraph with an some italic, Bold and span items
Please open browser console and look at "Manipulation demo" group for detailed console.log()
Collection
offers various functions to traverse DOM nodes
$('selector').prev(); // get previous element of first element in collection
$('selector').next(); // get next element of each elements in collection
$('selector').nextPrev(); // get all previous elements of each elements in collection
$('selector').nextAll(); // get all next elements of each elements in collection
$('selector').parent(); // get parent element of each elements in collection
$('selector').first(); // get the first element in collection
$('selector').last(); // get lest element of in collection
$('selector').parents('selector'); // get parents matching selector of first element in collection
$('selector').find('selector'); // get descendents marching selector of first element in collection
$('selector').children('selector'); // get direct descendents marching selector of first element in collection
$('selector').index(); // get the position of the first element within the Collection relative to its sibling elements
<p id="traversing-demo">
<span class="first">
<em>First</em>
</span>
<span class="second">
<em>Second</em>
</span>
<span class="third">Third</span>
</p>
First Second Third
Please open browser console and look at "Traversing demo" group for detailed console.log()
Collection
offers various functions to manipulate HTML nodes
$('selector').clone(); // clone all elements in collection
$('selector').remove(); // detach all elements in collection from DOM
$('selector').empty(); // empty all elements in collection
$('selector').prepend(item); // prepend a element to the first element in collection
$('selector').append(item); // append a element to the first element in collection
$('selector').after(item); // Add an element before specified element
$('selector').before(item); // Add an element after specified element
$('selector').wrap(item); // wrap all elements in collection with a the specified element
$('selector').html(); // get the html content of the first element in collection
$('selector').html('content'); // set the html content of all element in collection
$('selector').text(); // get the text content of the first element in collection
$('selector').text('content'); // set the text content of all element in collection
$('selector').attr('name'); // get the named attribue value of the first element in collection
$('selector').attr('name', 'value'); // set the named attribute value of the first element in collection
$('selector').removeAttr('name'); // remove the named attribute of all elements in collection
$('selector').data('name'); // get the named data value of the first element in collection
$('selector').data('name', 'value'); // set the data attribute value of the first element in collection
<p id="html-demo">
<strong class="to-prepend">prepend</strong>
<strong class="to-append">append</strong>
<div class="wrapper text-warning"></div>
<div>1: nothing <span>Test</span></div>
<div class="remove" data-id="1">2: remove</div>
<div class="remove" data-id="2">2: remove</div>
<div class="empty">3: empty</div>
<div class="prepend">4: prepend</div>
<div class="append">5: append</div>
<div class="before">6: before</div>
<div class="after">7: after</div>
<div class="wrap">8: wrap</div>
<div class="html">9: html</div>
<div class="text">10: text</div>
<div class="atr" title="title attribute" alt="attribute to remove">11: attr</div>
<div class="data" data-test="test dateset value">12: data</div>
</p>
Please open browser console and look at "HTML demo" group for detailed console.log()
Collection
offers various functions to manipulate CSS classes and style attribute.
$('.has-class').hasClass(className) // return true if any element in collection has specified class.
$('.add-class').addClass(className) // Add className to all elements in collection
$('.remove-class').removeClass(className) // Remove className on all elements in collection
$('.toggle-class').toggleClass(className) // Toggle className on all elements in collection
$('.css-get').css('key') // Return the CSS value of specified key of the first element in collection
$('.css-set').css('key', 'value') // Set the specified value of specified key css key on all elements in collection
$('.css-set').css({'key': 'value', ...}) // Set the specified key/value css on all elements in collection
<p id="css-demo">
<div class="has-class text-warning">Has class text-warning?</div>
<div class="has-class">Has class text-warning?</div>
<div class="add-class">Add class text-warning</div>
<div class="remove-class">Remove class text-warning</div>
<div class="toggle-class text-warning">Toggle class text-warning</div>
<div class="toggle-class">Toggle class text-warning</div>
<div class="css-get text-warning">Get CSS</div>
<div class="css-set">Set CSS</div>
</p>
Please open browser console and look at "CSS demo" group for detailed console.log()
Collection
offers various functions to get/set element position and dimensions.
$('#dimensions-test').width() // return the width of the first element in collection
$('#dimensions-test').width('calc(100% - 175px)') // set the width of all elements in collection
$('#dimensions-test').height() // return the height of the first element in collection
$('#dimensions-test').height(350) // et the height of all elements in collection
$('#dimensions-test').position() // return position {x, y} of a element relative to document
$('#dimensions-test').position('#dimensions-reference') // return position {x, y} of a element relative to a reference Element
$('#dimensions-test').viewportPosition() // return position {x, y} of a element relative to viewport
<div id="dimension-demo" class="d-flex">
<div id="dimensions-test">Original width/height is 175px</div>
<div id="dimensions-reference">Reference element</div>
</div>
Please open browser console and look at "Dimensions demo" group for detailed console.log()
Collection
offers various functions to manage events.
$(window).on('resize', () => {
console.log($(window).width(), $(window).height());
});
$('.button-click').on('click', (event: MouseEvent, item: Collection) => {
console.log((new Date()).getTime(), event, item);
});
$('.button-click-once').on('click', (event: MouseEvent, item: Collection) => {
console.log((new Date()).getTime(), event, item);
item.off('click');
});
$(document).on('click', '.button-delegate-click', (event: MouseEvent, item: Collection) => {
console.log((new Date()).getTime(), event, item);
});
$('.button-trigger').on('click', (event: MouseEvent, item: Collection) => {
console.log((new Date()).getTime(), event, item);
$('.button-click').trigger('click');
});
<div class="btn-group mb-3">
<button type="button" class="button-click btn btn-outline-primary">Click 1</button>
<button type="button" class="button-click-once btn btn-outline-primary">Click only once</button>
<button type="button" class="button-delegate-click btn btn-outline-primary">Delegate click</button>
<button type="button" class="button-delegate-click btn btn-outline-primary">Trigger "click 1"</button>
</div>
Window resize
Click
Click once
Click delegate
Trigger click
With $.httpRequest
you can query HTML, json or text. It uses async/await so you dont have to mess with callbacks. It support GET and POST.
let text: string = await $.httpRequest({
url: 'ajax.txt',
dataType: 'text',
});
console.log(text);
$('#ajax-target').text(text);
let json: object = await $.httpRequest({
url: 'ajax.json',
dataType: 'json',
});
console.log(json);
$('#ajax-target').html('');
let html: Collection = await $.httpRequest({
url: 'ajax.html',
dataType: 'html',
});
$('#ajax-target').append(html);
Helpers offers some handy functions like:
Await for document ready state
// some stuff to do before document is loaded
await $.ready();
// some stuff to do after document is loaded
Await for document images or set of images to be loaded
// some stuff to do before document images are loaded
await $.imagesLoaded();
// some stuff to do after document images are loaded
await $.imagesLoaded($('img.ajax-images'));
// some stuff to do after a specific set of images is loaded
// useful for ajax loaded content, ie. an ajax Carousel with some dimensions computations ;–)
Scroll document to a specified position
$.scrollTo(0);
Return a Collection
from an HTML string.
let p: Collection = $.parseHTML('<p>This is a <strong>bold</strong> text.</p>');
$('#parsehtml-target').append(p);
Jump to Menu, Scrollspy, Filtering, Boostrap Carsouel, Audio Player, Graphic Engine,
Compontent used in this page to toggle the fake menu (top right burger).
import { $ } from '../../../src/Framework';
import { Collection } from '../../../src/Collection';
import { Component } from '../../../src/Component';
export class Menu extends Component {
private triggerElementName: string;
private menuElementName: string;
private triggerElement: Collection;
private menuElement: Collection;
constructor(triggerElementName: string, menuElementName: string) {
super('Menu', false);
this.triggerElementName = triggerElementName;
this.menuElementName = menuElementName;
}
public async init(): Promise {
await super.init();
this.triggerElement = $(this.triggerElementName)
this.menuElement = $(this.menuElementName);
if (this.triggerElement.length == 0|| this.menuElement.length == 0) {
this.warning('missing `triggerElement` or `menuElement`');
} else {
this.triggerElement.on('click', (event: Event) => {
event.preventDefault();
this.triggerElement.toggleClass('active');
this.menuElement.toggleClass('active');
});
this.success();
}
}
}
import { Menu } from '../components/Menu';
(new Menu('#buttonToggleSideMenu', '#sideMenu')).init();
Compontent used in this page to handle scrollSpy menu.
import { $ } from '../../../src/Framework';
import { Collection } from '../../../src/Collection';
import { Elem } from '../../../src/Collection/Types';
import { Component } from '../../../src/Component';
export class ScrollSpy extends Component {
private menuElementName: string;
private sectionClass: string;
private link: Collection;
private sections: Collection;
private headerHeight: number;
constructor(menuElementName: string, sectionClass: string, headerHeight: number) {
super('ScrollSpyComponent', false);
this.menuElementName = menuElementName;
this.sectionClass = sectionClass;
this.headerHeight = headerHeight;
}
public async init(): Promise {
await super.init();
this.link = $(this.menuElementName + ' a');
this.sections = $(this.sectionClass);
if (this.link.length == 0 || this.sections.length == 0) {
this.warning('missing `link` or `sections`');
} else {
$(window).on('scroll', () => {
this.spy();
});
this.spy();
this.success();
}
this.link.on('click', (event: Event) => {
event.preventDefault();
var target: Collection = $($(event.target).attr('href'));
var top: number = (target.get(0) === this.sections.get(0)) ? 0 : target.position().y - this.headerHeight + 1;
$.scrollTo(top);
});
}
public spy(): void {
var currentID = '';
this.sections.forEach((section: Elem) => {
if (section.getBoundingClientRect().y <= this.headerHeight) currentID = $(section).attr('id');
});
if (currentID) {
this.link.removeClass('active').search('[href="#' + currentID + '"]').addClass('active');
}
}
}
import { ScrollSpy } from '../components/ScrollSpy';
(new ScrollSpy('#menu', '.section', 55)).init();
Some basic item filtering user dataset
import { $ } from '../../../src/Framework';
import { Collection } from '../../../src/Collection';
import { Component } from "../../../src/Component";
export class Filters extends Component {
private filtersElementName: string;
private contentElementName: string;
private filtersElements: Collection;
private allowMultiSelection: boolean;
private elements: Collection;
private idsToFilter: Array = [];
constructor(filtersElementName: string, contentElementName: string, allowMultiSelection: boolean) {
super('Filters', false);
this.filtersElementName = filtersElementName;
this.contentElementName = contentElementName;
this.allowMultiSelection = allowMultiSelection;
}
public async init(): Promise {
await super.init();
this.filtersElements = $(this.filtersElementName + ' a');
this.elements = $(this.contentElementName + ' .element');
if (this.filtersElements.length == 0 || this.elements.length == 0) {
this.warning('missing `filtersElement` or `contentElement`');
} else {
this.filtersElements.on('click', (event: Event) => {
event.preventDefault();
this.filterClickHandler($(event.target));
});
this.success();
}
}
private filterClickHandler(filterElement: Collection) {
const elementID: number = +filterElement.data('id');
/* all clicked => restore */
if (elementID == 0) {
this.idsToFilter = [];
this.elements.addClass('active');
return;
}
/* deactivate filter if active */
if (filterElement.hasClass('active')) {
filterElement.removeClass('active');
const index = this.idsToFilter.indexOf(elementID, 0);
if (index > -1) {
this.idsToFilter.splice(index, 1);
}
if (this.idsToFilter.length == 0) this.filtersElements.search('[data-id="0"]').addClass('active')
} else {
/* deactivate all filter */
if (!this.allowMultiSelection) {
this.idsToFilter = [];
this.filtersElements.removeClass('active');
}
/* activate the clicked one */
filterElement.addClass('active');
this.idsToFilter.push(elementID);
}
this.filterElements();
}
private filterElements(): void {
/* show all element */
if (this.idsToFilter.length == 0) {
this.elements.addClass('active');
return;
}
/* hide all element */
this.elements.removeClass('active');
/* ID provided, filters elements */
this.idsToFilter.forEach(id => {
$(this.contentElementName + ' .element[data-id="' + id + '"]').addClass('active')
});
}
}
import { Filters } from '../components/Filters';
(new Filters('.filters', '.list', false)).init();
You can use boostrap javascript in your code (npm install bootstrap
, npm install @types/bootstrap
).
import { $ } from '../../../src/Framework';
import { Elem } from '../../../src/Collection/Types';
import { Component } from '../../../src/Component';
import BS_Carousel from 'bootstrap/js/dist/carousel';
export class Carousel extends Component {
constructor() {
super('Carousel', true);
}
public async init(): Promise {
await super.init();
$('.carousel').forEach((carousel: Elem) => {
new BS_Carousel(carousel);
// here you can add events or whatever
});
this.success();
}
}
import { Carousel } from '../components/Carousel';
(new Carousel()).init();
Audio player component (no need to modify its source code) offers a HTML/CSS customizable audio player.
Music sample: Dupe Dodging by Speck (c) copyright 2021 Licensed under a Creative Commons Attribution Noncommercial (3.0) license. http://dig.ccmixter.org/files/speck/63463 Ft: Martijn de Boer
import { AudioPlayers } from '../components/AudioPlayers';
(new AudioPlayers()).init();
Every <audio>
tag with class .ht-audio-player
is automatically initized in the component.
<audio class="ht-audio-player" src="some.mp3" ></audio>
<div class="ab-audio-player-container">
<audio src="http://ccmixter.org/content/speck/speck_-_Dupe_Dodging.mp3" preload="metadata" class="ht-audio-player"></audio>
<button class="button-play-pause" data-status="paused">Play</button>
<input type="range" value="0" class="slider-seek">
<div class="indicators">
<span class="label-current-time">0:00</span>
<span class="label-duration">0:00</span>
</div>
<button class="button-mute" data-status="unmuted">Mute</button>
<input type="range" value="100" min="0" max="100" class="slider-volume">
</div>
Create a blind player that can be programmatical contoled
let audioPlayer: AudioPlayer = new AudioPlayer('some.mp3');
$('#play-audio').on('click', () => {
audioPlayer.play();
});
$('#pause-audio').on('click', () => {
audioPlayer.pause();
});
GraphicEngine
offers a way to create canvas animation or work with Three.js.
It offers methos such as setup()
, animate()
, draw()
to place your code.
<canvas id="canvas-2d"></canvas>
import { Selector, Dimensions } from '../../../src/Collection/Types';
import { GraphicEngine, GraphicEngineOptions} from "../../../src/GraphicEngine";
import { Component } from '../../../src/Component';
class Animation extends GraphicEngine {
private context: CanvasRenderingContext2D;
private bufferDimensions: Dimensions;
private imageData: ImageData;
private buffer: Uint32Array;
private alphaLimit: number;
private waveCounter: number;
constructor(selector: Selector, options?: GraphicEngineOptions) {
super(selector, options);
if (this.length == 0) return;
this.context = this.get(0).getContext('2d', {
alpha: this.options.alpha
});
this.setup();
}
public resize(dimensions: Dimensions) {
super.resize(dimensions);
this.bufferDimensions = {
width: this.options.width * this.options.pixelRatio,
height: this.options.height * this.options.pixelRatio
}
}
public setup() {
this.imageData = this.context.createImageData(this.bufferDimensions.width, this.bufferDimensions.height);
this.buffer = new Uint32Array(this.imageData.data.buffer);
this.alphaLimit = .5;
this.waveCounter = 0;
const increase = Math.PI * 2 / 100;
setInterval(() => {
this.alphaLimit = Math.sin(this.waveCounter) / 2 + .5;
this.waveCounter += increase;
}, 40);
setInterval(() => {
let len: number = this.buffer.length - 1;
while (len--) this.buffer[len] = Math.random() < this.alphaLimit ? 0 : 0xffffffff;
}, 40);
}
public clear () {
if (this.options.clear && this.context) this.context.clearRect(0, 0, this.bufferDimensions.width, this.bufferDimensions.height);
}
public draw() {
this.context.putImageData(this.imageData, 0, 0);
}
}
export class Animation2D extends Component {
constructor() {
super('Animation2D', true);
}
public async init(): Promise {
await super.init();
const canvas2D = new Animation('#canvas-2d', { width: 640, height: 480, pixelRatio: 1 });
this.success();
}
}
import { Animation2D } from '../components/Animation2D';
(new Animation2D()).init();
Exemple with Three.js. npm install three
, npm install @type/three
If you are working with Webpack and because Three.js is heavy, I recommand to create a separate entry point for this component.
<canvas id="canvas-3d"></canvas>
import { GraphicEngine, GraphicEngineOptions } from "../../../src/GraphicEngine";
import { Selector, Dimensions} from '../../../src/Collection/Types';
import { WebGLRenderer } from 'three/src/renderers/WebGLRenderer';
import { Scene } from 'three/src/scenes/Scene';
import { PerspectiveCamera } from 'three/src/cameras/PerspectiveCamera';
import { BoxGeometry } from 'three/src/geometries/BoxGeometry';
import { MeshBasicMaterial } from 'three/src/materials/MeshBasicMaterial';
import { Mesh } from 'three/src/objects/Mesh';
import { OrbitControls } from 'three/examples/jsm/controls/OrbitControls'
import { Component } from '../../../src/Component';
import { Color } from "three";
class Animation extends GraphicEngine {
private scene: Scene;
private renderer: WebGLRenderer;
private camera: PerspectiveCamera;
private cube: Mesh;
private controls: OrbitControls;
constructor(selector: Selector, options?: GraphicEngineOptions) {
super(selector, options);
if (this.length == 0) return;
this.scene = new Scene();
this.renderer = new WebGLRenderer({
canvas: this.get(0),
antialias: this.options.antialias,
alpha: this.options.alpha
});
this.camera = new PerspectiveCamera(75, this.options.width / this.options.height, 0.1, 1000);
this.renderer.setSize(this.options.width, this.options.height);
this.setup();
}
public resize(dimensions: Dimensions) {
super.resize(dimensions);
if (this.camera) {
this.camera.aspect = this.options.width / this.options.height
this.camera.updateProjectionMatrix()
this.renderer.setSize(this.options.width, this.options.height);
}
}
public setup() {
this.controls = new OrbitControls(this.camera, this.renderer.domElement);
const geometry: BoxGeometry = new BoxGeometry();
const material: THREE.MeshBasicMaterial = new MeshBasicMaterial({ color: 0x00ff00, wireframe: true })
this.cube = new Mesh(geometry, material)
this.scene.add(this.cube);
this.camera.position.z = 5;
this.renderer.setClearColor(new Color(0x000000), 0)
}
public animate() {
this.cube.rotation.x += .01;
this.cube.rotation.y += .01;
this.controls.update();
}
public draw() {
if (this.renderer) this.renderer.render(this.scene, this.camera);
}
}
export class Animation3D extends Component {
constructor() {
super('Animation3D', true);
}
public async init(): Promise {
await super.init();
new Animation('#canvas-3d', {width: 640, height: 480, alpha: true});
this.success();
}
}
import { Animation3D } from '../components/Animation3D';
(new Animation3D()).init();