Rewrite it in TypeScript; I have no idea if that works. It's still a work-in-progress.

This commit is contained in:
Manuel Friedli 2019-05-05 01:07:55 +02:00
parent 079bda7fb0
commit 5420f9c817
14 changed files with 3420 additions and 47 deletions

View file

@ -11,5 +11,6 @@ insert_final_newline = true
[*.yml] [*.yml]
indent_size = 2 indent_size = 2
[*.md] [*.md]
trim_trailing_whitespace = false trim_trailing_whitespace = false

2239
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -1,50 +1,54 @@
{ {
"name": "uhr", "name": "uhr",
"version": "9.0.0-dev.0", "version": "9.0.0-dev.0",
"description": "jQuery QLOCKTWO plugin", "description": "jQuery QLOCKTWO plugin",
"keywords": [ "keywords": [
"jquery-plugin", "jquery-plugin",
"qlocktwo" "qlocktwo"
], ],
"homepage": "http://bärneruhr.ch/", "homepage": "http://bärneruhr.ch/",
"author": { "author": {
"name": "Manuel Friedli", "name": "Manuel Friedli",
"email": "manuel@fritteli.ch", "email": "manuel@fritteli.ch",
"url": "http://www.fritteli.ch/" "url": "http://www.fritteli.ch/"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://gittr.ch/manuel/uhr" "url": "https://gittr.ch/manuel/uhr"
}, },
"licenses": [ "licenses": [
{ {
"type": "GPL-3.0" "type": "GPL-3.0"
}
],
"license": "GPL-3.0",
"dependencies": {
"jquery": "3.4.1",
"jquery-ui": "1.12.1",
"jquery.cookie": "1.4.1"
},
"devDependencies": {
"@types/jquery": "3.3.29",
"@types/jqueryui": "1.12.7",
"chai": "4.2.0",
"grunt": "1.0.4",
"grunt-cli": "1.3.2",
"grunt-contrib-clean": "2.0.0",
"grunt-contrib-concat": "1.0.1",
"grunt-contrib-connect": "2.0.0",
"grunt-contrib-cssmin": "3.0.0",
"grunt-contrib-jshint": "2.1.0",
"grunt-contrib-qunit": "3.1.0",
"grunt-contrib-uglify": "4.0.1",
"grunt-contrib-watch": "1.1.0",
"grunt-mocha-phantomjs": "4.0.0",
"grunt-version": "1.3.0",
"jshint-stylish": "2.2.1",
"load-grunt-tasks": "4.0.0",
"mocha": "6.1.4",
"phantomjs-prebuilt": "2.1.16",
"time-grunt": "2.0.0",
"webpack": "4.30.0",
"webpack-cli": "3.3.1"
} }
],
"license": "GPL-3.0",
"dependencies": {
"jquery": "3.4.1",
"jquery-ui": "1.12.1",
"jquery.cookie": "1.4.1"
},
"devDependencies": {
"grunt": "1.0.4",
"grunt-cli": "1.3.2",
"grunt-contrib-clean": "2.0.0",
"grunt-contrib-concat": "1.0.1",
"grunt-contrib-connect": "2.0.0",
"grunt-contrib-cssmin": "3.0.0",
"grunt-contrib-jshint": "2.1.0",
"grunt-contrib-qunit": "3.1.0",
"grunt-contrib-uglify": "4.0.1",
"grunt-contrib-watch": "1.1.0",
"grunt-mocha-phantomjs": "4.0.0",
"grunt-version": "1.3.0",
"jshint-stylish": "2.2.1",
"load-grunt-tasks": "4.0.0",
"phantomjs-prebuilt": "2.1.16",
"time-grunt": "2.0.0",
"mocha": "6.1.4",
"chai": "4.2.0"
}
} }

87
src/domain/globals.ts Normal file
View file

@ -0,0 +1,87 @@
/*
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {Layout} from "./layout";
import {Theme} from "./theme";
export class Globals {
private static layouts: Layout[] = [];
private static themes: Theme[] = [];
static registerTheme(name: string, styleClass: string): void {
if (Globals.themes.some(value => value.name === name)) {
console.log(`Theme with name '${name}' already registered; ignoring register request for styleClass '${styleClass}'.`)
} else {
Globals.themes.push({
name,
styleClass
});
}
}
static hasThemes(): boolean {
return Globals.themes.length > 0;
}
static hasMultipleThemes(): boolean {
return Globals.themes.length > 1;
}
static getFirstTheme(): Theme {
return Globals.getTheme(0);
}
static getTheme(index: number): Theme {
return Globals.themes[index];
}
static getThemes(): Theme[] {
return Globals.themes;
}
static registerLayout(langCode: string, layout: Layout): void {
const available = Globals.layouts.some(element => {
if (langCode === element.code) {
console.error(
`Error: Language code '${langCode}' cannot be registered for layout '${layout.prettyName}'
because it is already registered for layout '${element.prettyName}'!`
);
return false;
}
return true;
}
);
if (available) {
layout.code = langCode;
Globals.layouts.push(layout);
}
}
static hasLayouts(): boolean {
return Globals.layouts.length > 0;
}
static hasMultipleLayouts(): boolean {
return Globals.layouts.length > 1;
}
static getFirstLayout(): Layout {
return Globals.layouts[0];
}
static getLayouts(): Layout[] {
return Globals.layouts;
}
}

50
src/domain/layout.ts Normal file
View file

@ -0,0 +1,50 @@
/*
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {Letter} from "./letter";
export interface Layout {
version: number;
code: string;
prettyName: string;
letters: string[];
permanent: WordDefinition | WordDefinition[];
hours: TimeDefinition;
minutes: TimeDefinition;
seconds?: TimeDefinition;
getHour?: (Date) => number;
getDotMinute?: (Date) => number;
getCoarseMinute?: (Date) => number;
getSeconds?: (Date) => number;
parsed?: Letter[][];
}
export interface WordDefinition {
[line: number]: number[];
}
export interface TimeDefinition {
[values: string]: WordDefinition | WordDefinition[];
}
export const EMPTY_LAYOUT: Layout = {
version: 2,
code: null,
prettyName: null,
letters: [],
permanent: [],
minutes: null,
hours: null
};

43
src/domain/letter.ts Normal file
View file

@ -0,0 +1,43 @@
/*
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
/**
* Ein Buchstabe. Hilfsklasse für den Renderer und Inhalt der Layout-Arrays.
* @param value Der Buchstabe, der Dargestellt werden soll.
* @param style Die CSS-Styleklassen des Buchstabens.
*/
export class Letter {
private readonly value: string;
private style: string = '';
constructor(value: string, style?: string) {
this.value = value;
if (!!style) {
this.style = style;
}
}
addStyle(style: string): void {
if (this.style === '') {
this.style = style;
} else {
this.style += ` ${style}`;
}
};
toString(): string {
return `<span class="item letter ${this.style}">${this.value}</span>`;
};
}

19
src/domain/theme.ts Normal file
View file

@ -0,0 +1,19 @@
/*
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
export interface Theme {
styleClass: string;
name: string;
}

11
src/index.d.ts vendored Normal file
View file

@ -0,0 +1,11 @@
declare namespace Fritteli {
interface Fritteli {
uhr: {
register: (options?: { checkboxSelector: JQuery | string }) => JQuery;
}
}
}
interface JQueryStatic {
fritteli: Fritteli.Fritteli;
}

71
src/index.ts Normal file
View file

@ -0,0 +1,71 @@
/*
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {Globals} from "./domain/globals";
import {autodetectThemes} from "./theme-autodetector";
import {Uehrli} from "./uehrli";
import {WidgetPrototype} from "./widget/widget-prototype";
// First things first: discover included themes and register them
autodetectThemes();
$.widget("fritteli.uhr", {
"options": {
width: '100%',
status: 'on',
language: 'de_CH',
theme: Globals.getFirstTheme().styleClass,
force: false,
controls: true,
cookiePath: undefined,
autoresize: true,
mode: 'normal'
},
"start": function () {
this.__fritteli_uhr_instance.start();
},
"stop": function () {
this.__fritteli_uhr_instance.stop();
},
"toggle": function () {
this.__fritteli_uhr_instance.toggle();
},
"language": function (key: string) {
this.__fritteli_uhr_instance.setLanguage(key);
},
"theme": function (styleClass: string) {
this.__fritteli_uhr_instance.setTheme(styleClass);
},
"time": function (time: Date) {
this.__fritteli_uhr_instance.setTime(time);
},
"mode": function (mode: string) {
this.__fritteli_uhr_instance.setMode(mode);
},
"width": function (width: string) {
this.__fritteli_uhr_instance.setWidth(width);
},
// constructor method
"_create": function () {
this.__fritteli_uhr_instance = new Uehrli(this);
},
// destructor method
"_destroy": function () {
this.__fritteli_uhr_instance.destroy();
},
"__fritteli_uhr_instance": null
} as WidgetPrototype);
$.fritteli.uhr.register = Globals.registerLayout;

322
src/renderer.ts Normal file
View file

@ -0,0 +1,322 @@
/*
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {Layout, TimeDefinition, WordDefinition} from "./domain/layout";
import {Letter} from "./domain/letter";
class UhrRendererV2Delegate {
private static readonly vorne0: WordDefinition = {
3: [2, 3, 4],
4: [1, 5],
5: [1, 4, 5],
6: [1, 3, 5],
7: [1, 2, 5],
8: [1, 5],
9: [2, 3, 4]
};
private static readonly hinten0: WordDefinition = {
3: [8, 9, 10],
4: [7, 11],
5: [7, 10, 11],
6: [7, 9, 11],
7: [7, 8, 11],
8: [7, 11],
9: [8, 9, 10]
};
private static readonly vorne1: WordDefinition = {
3: [3],
4: [2, 3],
5: [3],
6: [3],
7: [3],
8: [3],
9: [2, 3, 4]
};
private static readonly hinten1: WordDefinition = {
3: [9],
4: [8, 9],
5: [9],
6: [9],
7: [9],
8: [9],
9: [8, 9, 10]
};
private static readonly vorne2: WordDefinition = {
3: [2, 3, 4],
4: [1, 5],
5: [5],
6: [4],
7: [3],
8: [2],
9: [1, 2, 3, 4, 5]
};
private static readonly hinten2: WordDefinition = {
3: [8, 9, 10],
4: [7, 11],
5: [11],
6: [10],
7: [9],
8: [8],
9: [7, 8, 9, 10, 11]
};
private static readonly vorne3: WordDefinition = {
3: [1, 2, 3, 4, 5],
4: [4],
5: [3],
6: [4],
7: [5],
8: [1, 5],
9: [2, 3, 4]
};
private static readonly hinten3: WordDefinition = {
3: [7, 8, 9, 10, 11],
4: [10],
5: [9],
6: [10],
7: [11],
8: [7, 11],
9: [8, 9, 10]
};
private static readonly vorne4: WordDefinition = {
3: [4],
4: [3, 4],
5: [2, 4],
6: [1, 4],
7: [1, 2, 3, 4, 5],
8: [4],
9: [4]
};
private static readonly hinten4: WordDefinition = {
3: [10],
4: [9, 10],
5: [8, 10],
6: [7, 10],
7: [7, 8, 9, 10, 11],
8: [10],
9: [10]
};
private static readonly vorne5: WordDefinition = {
3: [1, 2, 3, 4, 5],
4: [1],
5: [1, 2, 3, 4],
6: [5],
7: [5],
8: [1, 5],
9: [2, 3, 4]
};
private static readonly hinten5: WordDefinition = {
3: [7, 8, 9, 10, 11],
4: [7],
5: [7, 8, 9, 10],
6: [11],
7: [11],
8: [7, 11],
9: [8, 9, 10]
};
private static readonly hinten6: WordDefinition = {
3: [9, 10],
4: [8],
5: [7],
6: [7, 8, 9, 10],
7: [7, 11],
8: [7, 11],
9: [8, 9, 10]
};
private static readonly hinten7: WordDefinition = {
3: [7, 8, 9, 10, 11],
4: [11],
5: [10],
6: [9],
7: [8],
8: [8],
9: [8]
};
private static readonly hinten8: WordDefinition = {
3: [8, 9, 10],
4: [7, 11],
5: [7, 11],
6: [8, 9, 10],
7: [7, 11],
8: [7, 11],
9: [8, 9, 10]
};
private static readonly hinten9: WordDefinition = {
3: [8, 9, 10],
4: [7, 11],
5: [7, 11],
6: [8, 9, 10, 11],
7: [11],
8: [10],
9: [8, 9]
};
private static readonly seconds: TimeDefinition = {
"0": [UhrRendererV2Delegate.vorne0, UhrRendererV2Delegate.hinten0],
"1": [UhrRendererV2Delegate.vorne0, UhrRendererV2Delegate.hinten1],
"2": [UhrRendererV2Delegate.vorne0, UhrRendererV2Delegate.hinten2],
"3": [UhrRendererV2Delegate.vorne0, UhrRendererV2Delegate.hinten3],
"4": [UhrRendererV2Delegate.vorne0, UhrRendererV2Delegate.hinten4],
"5": [UhrRendererV2Delegate.vorne0, UhrRendererV2Delegate.hinten5],
"6": [UhrRendererV2Delegate.vorne0, UhrRendererV2Delegate.hinten6],
"7": [UhrRendererV2Delegate.vorne0, UhrRendererV2Delegate.hinten7],
"8": [UhrRendererV2Delegate.vorne0, UhrRendererV2Delegate.hinten8],
"9": [UhrRendererV2Delegate.vorne0, UhrRendererV2Delegate.hinten9],
"10": [UhrRendererV2Delegate.vorne1, UhrRendererV2Delegate.hinten0],
"11": [UhrRendererV2Delegate.vorne1, UhrRendererV2Delegate.hinten1],
"12": [UhrRendererV2Delegate.vorne1, UhrRendererV2Delegate.hinten2],
"13": [UhrRendererV2Delegate.vorne1, UhrRendererV2Delegate.hinten3],
"14": [UhrRendererV2Delegate.vorne1, UhrRendererV2Delegate.hinten4],
"15": [UhrRendererV2Delegate.vorne1, UhrRendererV2Delegate.hinten5],
"16": [UhrRendererV2Delegate.vorne1, UhrRendererV2Delegate.hinten6],
"17": [UhrRendererV2Delegate.vorne1, UhrRendererV2Delegate.hinten7],
"18": [UhrRendererV2Delegate.vorne1, UhrRendererV2Delegate.hinten8],
"19": [UhrRendererV2Delegate.vorne1, UhrRendererV2Delegate.hinten9],
"20": [UhrRendererV2Delegate.vorne2, UhrRendererV2Delegate.hinten0],
"21": [UhrRendererV2Delegate.vorne2, UhrRendererV2Delegate.hinten1],
"22": [UhrRendererV2Delegate.vorne2, UhrRendererV2Delegate.hinten2],
"23": [UhrRendererV2Delegate.vorne2, UhrRendererV2Delegate.hinten3],
"24": [UhrRendererV2Delegate.vorne2, UhrRendererV2Delegate.hinten4],
"25": [UhrRendererV2Delegate.vorne2, UhrRendererV2Delegate.hinten5],
"26": [UhrRendererV2Delegate.vorne2, UhrRendererV2Delegate.hinten6],
"27": [UhrRendererV2Delegate.vorne2, UhrRendererV2Delegate.hinten7],
"28": [UhrRendererV2Delegate.vorne2, UhrRendererV2Delegate.hinten8],
"29": [UhrRendererV2Delegate.vorne2, UhrRendererV2Delegate.hinten9],
"30": [UhrRendererV2Delegate.vorne3, UhrRendererV2Delegate.hinten0],
"31": [UhrRendererV2Delegate.vorne3, UhrRendererV2Delegate.hinten1],
"32": [UhrRendererV2Delegate.vorne3, UhrRendererV2Delegate.hinten2],
"33": [UhrRendererV2Delegate.vorne3, UhrRendererV2Delegate.hinten3],
"34": [UhrRendererV2Delegate.vorne3, UhrRendererV2Delegate.hinten4],
"35": [UhrRendererV2Delegate.vorne3, UhrRendererV2Delegate.hinten5],
"36": [UhrRendererV2Delegate.vorne3, UhrRendererV2Delegate.hinten6],
"37": [UhrRendererV2Delegate.vorne3, UhrRendererV2Delegate.hinten7],
"38": [UhrRendererV2Delegate.vorne3, UhrRendererV2Delegate.hinten8],
"39": [UhrRendererV2Delegate.vorne3, UhrRendererV2Delegate.hinten9],
"40": [UhrRendererV2Delegate.vorne4, UhrRendererV2Delegate.hinten0],
"41": [UhrRendererV2Delegate.vorne4, UhrRendererV2Delegate.hinten1],
"42": [UhrRendererV2Delegate.vorne4, UhrRendererV2Delegate.hinten2],
"43": [UhrRendererV2Delegate.vorne4, UhrRendererV2Delegate.hinten3],
"44": [UhrRendererV2Delegate.vorne4, UhrRendererV2Delegate.hinten4],
"45": [UhrRendererV2Delegate.vorne4, UhrRendererV2Delegate.hinten5],
"46": [UhrRendererV2Delegate.vorne4, UhrRendererV2Delegate.hinten6],
"47": [UhrRendererV2Delegate.vorne4, UhrRendererV2Delegate.hinten7],
"48": [UhrRendererV2Delegate.vorne4, UhrRendererV2Delegate.hinten8],
"49": [UhrRendererV2Delegate.vorne4, UhrRendererV2Delegate.hinten9],
"50": [UhrRendererV2Delegate.vorne5, UhrRendererV2Delegate.hinten0],
"51": [UhrRendererV2Delegate.vorne5, UhrRendererV2Delegate.hinten1],
"52": [UhrRendererV2Delegate.vorne5, UhrRendererV2Delegate.hinten2],
"53": [UhrRendererV2Delegate.vorne5, UhrRendererV2Delegate.hinten3],
"54": [UhrRendererV2Delegate.vorne5, UhrRendererV2Delegate.hinten4],
"55": [UhrRendererV2Delegate.vorne5, UhrRendererV2Delegate.hinten5],
"56": [UhrRendererV2Delegate.vorne5, UhrRendererV2Delegate.hinten6],
"57": [UhrRendererV2Delegate.vorne5, UhrRendererV2Delegate.hinten7],
"58": [UhrRendererV2Delegate.vorne5, UhrRendererV2Delegate.hinten8],
"59": [UhrRendererV2Delegate.vorne5, UhrRendererV2Delegate.hinten9]
};
constructor(private layout: Layout) {
}
public parse(): Letter[][] {
const letters: Letter[][] = [];
this.layout.letters.forEach(lineString => {
const line: Letter[] = [];
for (let c = 0; c < lineString.length; c++) {
line.push(new Letter(lineString[c]));
}
letters.push(line);
});
this.parseArrayOrObject(letters, 'on', this.layout.permanent);
if (typeof this.layout.seconds !== 'undefined' && this.layout.seconds !== null) {
this.parseTimeDefinition(letters, 'second', this.layout.seconds);
} else {
this.parseTimeDefinition(letters, 'second', UhrRendererV2Delegate.seconds);
}
this.parseTimeDefinition(letters, 'minute', this.layout.minutes);
this.parseTimeDefinition(letters, 'hour', this.layout.hours);
return letters;
};
private parseObject(letters: Letter[][], styleClass: string, object: WordDefinition): void {
if (typeof object !== 'undefined' && object !== null) {
Object.keys(object)
.map(key => Number(key))
.forEach(
y => object[y].forEach(
x => letters[y - 1][x - 1].addStyle(styleClass)
)
);
}
}
private parseArrayOrObject(letters: Letter[][], styleClass: string, input: WordDefinition | WordDefinition[]): void {
if (typeof input !== 'undefined' && input !== null) {
if (Array.isArray(input)) {
input.forEach(item => this.parseObject(letters, styleClass, item));
} else {
this.parseObject(letters, styleClass, input);
}
}
}
private parseTimeDefinition(letters: Letter[][], styleClass: string, definition: TimeDefinition): void {
if (typeof definition !== 'undefined' && definition !== null) {
Object.keys(definition).forEach(listString => {
const timeValues: string[] = listString.split(',');
const highlightLetters: WordDefinition | WordDefinition[] = definition[listString];
timeValues.forEach(timeValue => this.parseArrayOrObject(letters, styleClass + timeValue, highlightLetters));
});
}
}
}
/**
* Hilfsklasse zum Rendern der Uhr.
* @param layout Layout-Objekt, das gerendert werden soll.
* @param renderarea Das jQuery-gewrappte HTML-Element, auf dem gerendert werden soll.
*/
export class UhrRenderer {
constructor(private layout: Layout, private renderarea: any/*FIXME any: jQuery-wrapped HTML element*/) {
}
render(beforeshow?: () => void): void {
if (this.layout.parsed === undefined) {
if (this.layout.version === 2) {
const delegate: UhrRendererV2Delegate = new UhrRendererV2Delegate(this.layout);
const parsedLayout: Letter[][] = delegate.parse();
Object.defineProperty(this.layout, "parsed", {
"value": parsedLayout,
"writable": false,
"configurable": false
});
} else {
console.warn(`Unknown layout version: '${this.layout.version}', expecting '2'`);
return;
}
}
const letters: Letter[][] = this.layout.parsed;
this.renderarea.fadeOut('fast', () => {
this.renderarea.empty();
letters.forEach((line, index, array) => {
line.forEach(letter => this.renderarea.append(letter.toString()));
if (index < array.length - 1) {
this.renderarea.append('<br/>');
}
});
if (typeof beforeshow === 'function') {
beforeshow();
}
this.renderarea.fadeIn('fast');
});
};
}

36
src/theme-autodetector.ts Normal file
View file

@ -0,0 +1,36 @@
/*
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import * as $ from 'jquery';
import {Globals} from "./domain/globals";
export function autodetectThemes() {
$('link[rel=stylesheet]').each((index, item) => {
const styleSheet = $(item);
const styleClass: string = styleSheet.attr('data-class');
if (styleClass !== undefined) {
let name: string = styleSheet.attr('data-name');
if (name === undefined) {
name = styleClass;
}
Globals.registerTheme(name, styleClass);
}
});
// fall-back if no theme was included
if (!Globals.hasThemes()) {
Globals.registerTheme('', '');
}
}

430
src/uehrli.ts Normal file
View file

@ -0,0 +1,430 @@
/*
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import {WidgetPrototype} from "./widget/widget-prototype";
import {Globals} from "./domain/globals";
import {UhrRenderer} from "./renderer";
import {EMPTY_LAYOUT, Layout} from "./domain/layout";
export class Uehrli {
private timer: number = null;
private currentMinute: number = null;
constructor(private widgetInstance: WidgetPrototype) {
const userTime = this.widgetInstance.options.time;
if (this.widgetInstance.options.time === undefined) {
this.widgetInstance.options.time = new Date();
}
this.parseHash();
this.setupHTML();
this.wireFunctionality();
if (userTime !== undefined) {
this.setTime(userTime);
}
}
destroy(): void {
if (!!this.timer) {
window.clearInterval(this.timer);
this.timer = null;
}
this.widgetInstance.element
.removeAttr('style')
.removeAttr('class')
.empty();
$(`#uhr-configlink${this.widgetInstance.uuid}`).remove();
$(`#uhr-controlpanel${this.widgetInstance.uuid}`).remove();
}
start(): void {
if (!this.isOn()) {
this.timer = window.setInterval(() => {
this.widgetInstance.options.time = new Date();
this.update();
}, 1000);
this.update();
this.setCookie('uhr-status', 'on');
}
}
stop(): void {
if (this.isOn()) {
window.clearInterval(this.timer);
this.timer = null;
this.update();
this.setCookie('uhr-status', 'off');
}
}
toggle(): void {
if (this.isOn()) {
this.stop();
} else {
this.start();
}
}
setLanguage(key: string): void {
if (key !== this.widgetInstance.options.language) {
this.widgetInstance.options.language = key;
const renderer = new UhrRenderer(this.getCurrentLayout(), this.widgetInstance.element.find('.letterarea'));
renderer.render(() => {
this.currentMinute = -1;
this.update();
});
this.setCookie('uhr-language', key);
this.update();
}
}
setTheme(styleClass: string): void {
if (styleClass !== this.widgetInstance.options.theme) {
this.widgetInstance.element.removeClass(this.widgetInstance.options.theme).addClass(styleClass);
$(`#uhr-onoffswitch${this.widgetInstance.uuid}`).removeClass(this.widgetInstance.options.theme).addClass(styleClass);
this.widgetInstance.options.theme = styleClass;
this.setCookie('uhr-theme', styleClass);
}
}
setTime(time: Date): void {
this.currentMinute = null;
if (time === null) {
this.widgetInstance.options.time = new Date();
} else {
if (this.timer !== null) {
window.clearInterval(this.timer);
}
this.widgetInstance.options.time = time;
}
this.update();
}
setMode(mode: string): void {
this.widgetInstance.options.mode = mode;
this.currentMinute = null;
this.update();
this.setCookie('uhr-mode', mode);
}
setWidth(width: string): void {
const e = this.widgetInstance.element;
e.css('width', width);
const realWidth = e.width();
e.width(realWidth);
e.height(realWidth);
e.css('font-size', (realWidth / 40) + 'px');
}
private setupHTML(): void {
const e: JQuery<HTMLElement> = this.widgetInstance.element;
// Base clock area
e.addClass('uhr')
.empty()
.append('<span class="item dot dot1"></span>')
.append('<span class="item dot dot2"></span>')
.append('<span class="item dot dot3"></span>')
.append('<span class="item dot dot4"></span>')
.append('<div class="letterarea"></div>')
.append('<div class="reflection"></div>');
this.setWidth(this.widgetInstance.options.width);
if (this.widgetInstance.options.controls) {
const controlpanel = $(`<div class="uhr-controlpanel" id="uhr-controlpanel${this.widgetInstance.uuid}"></div>`);
const content = $('<div class="content"></div>');
controlpanel.append(content);
// on/off switch
const toggleSwitch = $(`<div class="onoffswitch" id="uhr-onoffswitch${this.widgetInstance.uuid}"></div>`);
toggleSwitch.append(`<input type="checkbox" class="onoffswitch-checkbox" id="uhr-onoffswitch-checkbox${this.widgetInstance.uuid}" checked="checked" />`);
toggleSwitch.append(`<label class="onoffswitch-label" for="uhr-onoffswitch-checkbox${this.widgetInstance.uuid}"><div class="onoffswitch-inner"></div><div class="onoffswitch-switch"></div></label>`);
content.append(toggleSwitch);
// time mode switch
const modeSwitch = $(`<div class="onoffswitch" id="uhr-modeswitch${this.widgetInstance.uuid}"></div>`);
modeSwitch.append(`<input type="checkbox" class="onoffswitch-checkbox" id="uhr-modeswitch-checkbox${this.widgetInstance.uuid}" checked="checked" />`);
modeSwitch.append(`<label class="onoffswitch-label" for="uhr-modeswitch-checkbox${this.widgetInstance.uuid}"><div class="modeswitch-inner"></div><div class="onoffswitch-switch"></div></label>`);
content.append(modeSwitch);
// language chooser
if (Globals.hasMultipleLayouts()) {
const languageChooser = $(`<select id="uhr-languagechooser${this.widgetInstance.uuid}"></select>`);
Globals.getLayouts().forEach(layout => {
languageChooser.append(`<option value="${layout.code}">${layout.prettyName}</option>`);
});
content.append(languageChooser);
}
// theme chooser
if (Globals.hasMultipleThemes()) {
const themeChooser = $(`<select id="uhr-themechooser${this.widgetInstance.uuid}"></select>`);
Globals.getThemes().forEach(theme => {
themeChooser.append(`<option value="${theme.styleClass}">${theme.name}</option>`);
});
content.append(themeChooser);
}
const closebutton = $(`<a class="uhr-closecontrolpanel" id="uhr-closecontrolpanel${this.widgetInstance.uuid}"></a>`);
//FIXME deprecated?!
closebutton.on('click', function () {
$(`#uhr-controlpanel${this.uuid}`).hide('fast');
}.bind(this.widgetInstance));
content.append(closebutton);
e.after(controlpanel);
controlpanel.hide();
const configlink = $(`<a class="uhr-configlink" id="uhr-configlink${this.widgetInstance.uuid}"></a>`);
// FIXME deprecated!?
configlink.on('click', function () {
this.toggleConfigScreen();
}.bind(this));
e.after(configlink);
}
};
private wireFunctionality(): void {
// on/off switch
const toggleSwitch = $(`#uhr-onoffswitch-checkbox${this.widgetInstance.uuid}`);
// FIXME deprecated!?
toggleSwitch.on('click', () => this.toggle());
let status = this.getCookie('uhr-status');
if (status === undefined || this.widgetInstance.options.force) {
status = this.widgetInstance.options.status;
}
toggleSwitch.prop('checked', status === 'on');
if (status === 'on') {
this.start();
} else {
this.stop();
}
// time mode switch
const modeSwitch = $(`#uhr-modeswitch-checkbox${this.widgetInstance.uuid}`);
// FIXME deprecated!?
modeSwitch.on('click', () => {
if (this.widgetInstance.options.mode === 'seconds') {
this.setMode('normal');
} else {
this.setMode('seconds');
}
});
let mode = this.getCookie('uhr-mode');
if (mode === undefined || this.widgetInstance.options.force) {
mode = this.widgetInstance.options.mode;
}
modeSwitch.prop('checked', mode !== 'seconds');
if (mode === 'seconds') {
this.setMode('seconds');
} else {
this.setMode('normal');
}
// language chooser
const languageChooser = $(`#uhr-languagechooser${this.widgetInstance.uuid}`);
// FIXME deprecated!?
languageChooser.on('change', () => {
const languageKey = $(`#uhr-languagechooser${this.widgetInstance.uuid}`).val() as string;
this.setLanguage(languageKey);
});
let selectedLayout = this.getCookie('uhr-language');
if (selectedLayout === undefined || this.widgetInstance.options.force) {
selectedLayout = this.widgetInstance.options.language;
}
let found = Globals.getLayouts().some(item => selectedLayout === item.code);
if (!found) {
let fallbackLanguage;
if (Globals.hasLayouts()) {
fallbackLanguage = Globals.getFirstLayout().code;
} else {
fallbackLanguage = '';
}
console.warn(`Language '${selectedLayout}' not found! Using fallback '${fallbackLanguage}'.`);
selectedLayout = fallbackLanguage;
}
languageChooser.val(selectedLayout);
this.widgetInstance.options.language = "";
this.setLanguage(selectedLayout);
// theme chooser
const themeChooser = $(`#uhr-themechooser${this.widgetInstance.uuid}`);
// FIXME deprecated!?
themeChooser.on('change', () => {
const themeKey = $(`#uhr-themechooser${this.widgetInstance.uuid}`).val() as string;
this.setTheme(themeKey);
});
let selectedTheme = this.getCookie('uhr-theme');
if (selectedTheme === undefined || this.widgetInstance.options.force) {
selectedTheme = this.widgetInstance.options.theme;
}
found = Globals.getThemes().some(item => selectedTheme === item.styleClass);
if (!found) {
const fallbackTheme = Globals.getFirstTheme().styleClass;
console.warn(`Theme '${selectedTheme}' not found! Using fallback '${fallbackTheme}'.`);
selectedTheme = fallbackTheme;
}
themeChooser.val(selectedTheme);
this.widgetInstance.options.theme = "";
this.setTheme(selectedTheme);
if (this.widgetInstance.options.autoresize) {
// FIXME deprecated!?
$(window).on('resize', () => {
const $e = this.widgetInstance.element;
const $parent = $e.parent();
const $window = $(window);
const parentWidth = $parent.width();
const parentHeight = $parent.height();
const windowWidth = $window.width();
const windowHeight = $window.height();
const size = `${Math.min(parentWidth, parentHeight, windowWidth, windowHeight)}px`;
this.setWidth(size);
});
}
}
private isOn(): boolean {
return this.timer !== null;
}
private getCookie(cookieName: string): string {
return $.cookie(cookieName + this.widgetInstance.uuid);
}
private setCookie(cookieName: string, cookieValue: string): void {
let options;
if (this.widgetInstance.options.cookiePath !== undefined) {
options = {expires: 365, path: this.widgetInstance.options.cookiePath};
} else {
options = {expires: 365};
}
$.cookie(cookieName + this.widgetInstance.uuid, cookieValue, options);
}
private update(): void {
if (this.isOn()) {
const time = this.widgetInstance.options.time;
if (!this.getCurrentLayout().hasOwnProperty('seconds') && this.widgetInstance.options.mode !== 'seconds') {
if (time.getMinutes() === this.currentMinute) {
return;
}
this.currentMinute = time.getMinutes();
}
this.show(time);
} else {
this.clear();
this.currentMinute = -1;
}
}
private show(time: Date): void {
const second = this.getSecond(time);
const dotMinute = this.getDotMinute(time);
const hour = this.getHour(time);
const coarseMinute = this.getCoarseMinute(time);
this.clear();
if (this.widgetInstance.options.mode === 'seconds') {
this.highlight(`second${second}`);
} else {
this.highlight('on');
for (let i = 1; i <= dotMinute; i++) {
this.highlight(`dot${i}`);
}
this.highlight(`minute${coarseMinute}`);
this.highlight(`hour${hour}`);
}
}
private clear(): void {
this.widgetInstance.element.find('.item').removeClass('active');
}
private highlight(itemClass: string): void {
this.widgetInstance.element.find(`.item.${itemClass}`).addClass('active');
}
private getSecond(time: Date): number {
if (typeof this.getCurrentLayout().getSeconds === 'function') {
return this.getCurrentLayout().getSeconds(time);
}
return time.getSeconds();
};
private getDotMinute(date: Date): number {
if (typeof this.getCurrentLayout().getDotMinute === 'function') {
return this.getCurrentLayout().getDotMinute(date);
}
return date.getMinutes() % 5;
};
private getCoarseMinute(date: Date): number {
if (typeof this.getCurrentLayout().getCoarseMinute === 'function') {
return this.getCurrentLayout().getCoarseMinute(date);
}
return date.getMinutes();
};
private getHour(date: Date): number {
if (typeof this.getCurrentLayout().getHour === 'function') {
return this.getCurrentLayout().getHour(date);
}
const hour = date.getHours();
if (date.getMinutes() >= 25) {
return (hour + 1) % 24;
}
return hour;
};
private parseHash(): void {
let hash: string = window.location.hash;
if (hash !== undefined && hash.charAt(0) === '#') {
hash = hash.substring(1);
hash = decodeURIComponent(hash);
const params: string[] = hash.split('&');
params.forEach(element => {
const pair: string[] = element.split('=');
const key = pair[0];
const value = pair[1];
switch (key) {
case 'l':
case 'language':
this.widgetInstance.options.language = value;
this.widgetInstance.options.force = true;
break;
case 't':
case 'theme':
this.widgetInstance.options.theme = value;
this.widgetInstance.options.force = true;
break;
case 'm':
case 'mode':
this.widgetInstance.options.mode = value;
this.widgetInstance.options.force = true;
break;
case 's':
case 'status':
this.widgetInstance.options.status = value;
this.widgetInstance.options.force = true;
break;
}
});
}
}
private getCurrentLayout(): Layout {
const matchingLanguages: Layout[] = Globals.getLayouts().filter(element => element.code === this.widgetInstance.options.language, this);
if (matchingLanguages.length > 0) {
return matchingLanguages[0];
}
// fallback: return empty object
return EMPTY_LAYOUT;
};
}

27
src/widget/options.ts Normal file
View file

@ -0,0 +1,27 @@
/*
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
export interface Options {
width: string;
status: string;
language: string;
theme: string;
force: boolean;
controls: boolean;
cookiePath?: string;
autoresize: boolean;
mode: string;
time?: Date;
}

View file

@ -0,0 +1,33 @@
/*
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
import WidgetCommonProperties = JQueryUI.WidgetCommonProperties;
import {Uehrli} from "../uehrli";
import {Options} from "./options";
export interface WidgetPrototype extends WidgetCommonProperties {
options: Options;
start: () => void;
stop: () => void;
toggle: () => void;
language: (key: string) => void;
theme: (id: string) => void;
time: (time: Date) => void;
mode: (mode: string) => void;
width: (width: string) => void;
_create: () => void;
_destroy: () => void;
__fritteli_uhr_instance: Uehrli;
}