The menu

Some quick example text to build on the card title and make up the bulk of the card's content.

About

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.

Documentation

For a complete informations on Class, Methods, etc. please read documentation on /docs/ folder.

Components

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.).

Files structure
my-website-project
├── index.html
├── compontents
|   ├── MyFirstCompontent
|   |   └── index.ts
|   └── ...
└── ts
    └── index.ts
Install hanako

First of all you need to install Hanako framework

npm init
npm install hanako-ts --save-dev

es2015, es2020, ...

Files in hanako-ts/dist are compiled in es2020. If you need compatibility with es2015, please use hanako-ts/dist-legacy

MyFirstComponent/index.html
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
}
index.ts
import { MyFirstCompontent } from '../components/MyFirstCompontent';
        
(new MyFirstCompontent()).init();

Collection

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

Selector

Collection accept the following elements as selector:

  • Any valid CSS selector
  • HTMLCollection, NodeList, GenericElement, GenericElement[]
  • An other Collection (dupliacate a Collection)
  • No arguments (create an empty 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', () => { ... });

Examples

<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()

Manipulation

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

Examples

<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()

Traversing

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

Examples

<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()

HTML

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

Examples

<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>
prepend append before after
1: nothing Test
2: remove
2: remove
3: empty
4: prepend
5: append
6: before
7: after
8: wrap
9: html
10: text
11: attr
12: data
Please open browser console and look at "HTML demo" group for detailed console.log()

CSS

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

Examples

<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>
Has class text-warning?
Has class text-warning?
Add class text-warning
Remove class text-warning
Toggle class text-warning
Toggle class text-warning
Get CSS
Set CSS
Please open browser console and look at "CSS demo" group for detailed console.log()

Dimensions

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

Examples

<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>
Original width/height is 175px
Reference element
Please open browser console and look at "Dimensions demo" group for detailed console.log()

Events

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');
});

Examples

<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		

Network

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);
Please click on a button above.

Helpers

Helpers offers some handy functions like:

$.ready

Await for document ready state

// some stuff to do before document is loaded
await $.ready();
// some stuff to do after document is loaded

$.imagesLoaded

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 ;–)

$.scrollTo

Scroll document to a specified position

$.scrollTo(0);

$.parseHTML

Return a Collection from an HTML string.

let p: Collection = $.parseHTML('<p>This is a <strong>bold</strong> text.</p>');
$('#parsehtml-target').append(p);

Components examples

Jump to Menu, Scrollspy, Filtering, Boostrap Carsouel, Audio Player, Graphic Engine,

Compontent used in this page to toggle the fake menu (top right burger).

Menu/index.html
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();
    }
  }
}
index.ts
import { Menu } from '../components/Menu';

(new Menu('#buttonToggleSideMenu', '#sideMenu')).init();

Scrollspy

Compontent used in this page to handle scrollSpy menu.

Scrollspy/index.ts
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');
    }
  }
}
index.ts
import { ScrollSpy } from '../components/ScrollSpy';
                        
(new ScrollSpy('#menu', '.section', 55)).init();

Filtering

Some basic item filtering user dataset

Red Red Green Blue
Filters/index.ts
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')
    });
  }
}
index.ts
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).

Carsouel/index.ts
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();
  }
}
index.ts
import { Carousel } from '../components/Carousel';

(new Carousel()).init();

Audio Player

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

index.ts
import { AudioPlayers } from '../components/AudioPlayers';

(new AudioPlayers()).init();

Default

Every <audio> tag with class .ht-audio-player is automatically initized in the component.

<audio class="ht-audio-player" src="some.mp3" ></audio>

Custom HTML

<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>
0:00 0:00

Programmatical

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();
});

Graphic Engine

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 2D

<canvas id="canvas-2d"></canvas>
Animation2D/index.ts
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();
  }
}
Animation3D/index.ts
index.ts
import { Animation2D } from '../components/Animation2D';

(new Animation2D()).init();

Three.js

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>
Animation3D/index.ts
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();
  }
}
index.ts
import { Animation3D } from '../components/Animation3D';

(new Animation3D()).init();