mirror of
https://github.com/skishore/makemeahanzi.git
synced 2025-10-29 01:46:07 +08:00
205 lines
6.1 KiB
JavaScript
205 lines
6.1 KiB
JavaScript
const animations = new ReactiveVar();
|
|
const character = new ReactiveVar();
|
|
const metadata = new ReactiveVar();
|
|
const strokes = new ReactiveVar();
|
|
const tree = new ReactiveVar();
|
|
|
|
let animation = null;
|
|
|
|
const orientation = new ReactiveVar('horizontal');
|
|
const short = new ReactiveVar(false);
|
|
|
|
// Methods used to render all the various pieces of character metadata.
|
|
|
|
const augmentTreeWithLabels = (node, dependencies) => {
|
|
const value = node.value;
|
|
if (node.type === 'compound') {
|
|
node.label = lower(makemeahanzi.Decomposition.ids_data[value].label);
|
|
node.children.map((child) => augmentTreeWithLabels(child, dependencies));
|
|
} else {
|
|
node.label = dependencies[node.value] || '(unknown)';
|
|
if (dependencies[node.value]) {
|
|
node.link = `#/codepoint/${node.value.charCodeAt(0)}`;
|
|
}
|
|
}
|
|
}
|
|
|
|
const constructTree = (row) => {
|
|
const util = makemeahanzi.Decomposition;
|
|
const tree = util.convertDecompositionToTree(row.decomposition);
|
|
augmentTreeWithLabels(tree, row.dependencies);
|
|
return tree;
|
|
}
|
|
|
|
const formatEtymology = (etymology) => {
|
|
const result = [etymology.type];
|
|
if (etymology.type === 'ideographic' ||
|
|
etymology.type === 'pictographic') {
|
|
if (etymology.hint) {
|
|
result.push(`- ${lower(etymology.hint)}`);
|
|
}
|
|
} else {
|
|
result.push('-');
|
|
result.push(etymology.semantic || '?');
|
|
if (etymology.hint) {
|
|
result.push(`(${lower(etymology.hint)})`);
|
|
}
|
|
result.push('provides the meaning while');
|
|
result.push(etymology.phonetic || '?');
|
|
result.push('provides the pronunciation.');
|
|
}
|
|
return result.join(' ');
|
|
}
|
|
|
|
const lower = (string) => {
|
|
if (string.length === 0) return string;
|
|
return string[0].toLowerCase() + string.substr(1);
|
|
}
|
|
|
|
const refreshMetadata = (row) => {
|
|
const options = {delay: 0.3, speed: 0.02};
|
|
animation = new makemeahanzi.Animation(options, row.strokes, row.medians);
|
|
animate(advanceAnimation);
|
|
metadata.set([
|
|
{label: 'Definition:', value: row.definition},
|
|
{label: 'Pinyin:', value: row.pinyin.join(', ')},
|
|
{label: 'Radical:', value: row.radical},
|
|
]);
|
|
if (row.etymology) {
|
|
metadata.push({
|
|
label: 'Formation:',
|
|
value: formatEtymology(row.etymology),
|
|
});
|
|
}
|
|
strokes.set(row.strokes.map((d) => ({d: d, class: 'incomplete'})));
|
|
tree.set(constructTree(row));
|
|
}
|
|
|
|
const updateCharacter = () => {
|
|
makemeahanzi.lookupCharacter(character.get(), (row) => {
|
|
if (row.character === character.get()) refreshMetadata(row); });
|
|
}
|
|
|
|
// Methods for running the stroke-order animation.
|
|
|
|
const animate = window.requestAnimationFrame ||
|
|
((callback) => setTimeout(callback, 1000 / 60));
|
|
|
|
const advanceAnimation = () => {
|
|
if (animation == null) {
|
|
return;
|
|
}
|
|
const step = animation.step();
|
|
const complete = step.animations.length - (step.complete ? 0 : 1);
|
|
|
|
if (complete > 0 && strokes.get()[complete - 1].class !== 'complete') {
|
|
const current = strokes.get();
|
|
for (let i = 0; i < complete ; i++) {
|
|
current[i].class = 'complete';
|
|
}
|
|
strokes.set(current);
|
|
}
|
|
animations.set(step.animations.slice(complete));
|
|
if (!step.complete) {
|
|
animate(advanceAnimation);
|
|
}
|
|
}
|
|
|
|
const resize = () => {
|
|
short.set(window.innerWidth <= 480 ? 'short ' : '');
|
|
orientation.set(window.innerWidth < window.innerHeight ?
|
|
'vertical' : 'horizontal');
|
|
}
|
|
|
|
// Various event handlers and other helpers.
|
|
|
|
const linkify = (value) => {
|
|
const result = [];
|
|
for (let character of value) {
|
|
if (character.match(/[\u3400-\u9FBF]/)) {
|
|
result.push(`<a href="#/codepoint/${character.charCodeAt(0)}" ` +
|
|
`class="link">${character}</a>`);
|
|
} else {
|
|
result.push(character);
|
|
}
|
|
}
|
|
return result.join('');
|
|
}
|
|
|
|
const showModal = () => {
|
|
// TODO(skishore): Maybe rewrite these jQuery hax in idiomatic Meteor.
|
|
const element = $('.modal');
|
|
const value = character.get();
|
|
element.find('.modal-title').text(`Report an error with ${value}`);
|
|
element.find('.form-control, .status')
|
|
.text('').val('').removeClass('error success');
|
|
element.unbind('shown.bs.modal').on('shown.bs.modal', function() {
|
|
$(this).find('.form-control:first').focus();
|
|
});
|
|
element.find('.submit.btn').unbind('click').on('click', () => {
|
|
const description = element.find('.description.form-control').val();
|
|
if (description.length === 0) {
|
|
element.find('.status').addClass('error').removeClass('success')
|
|
.text('You must enter a description of the problem.');
|
|
return;
|
|
}
|
|
element.find('.status').removeClass('error success')
|
|
.text('Submitting feedback...');
|
|
Meteor.call('reportError', value, description, (error, result) => {
|
|
if (error) {
|
|
element.find('.status').addClass('error').removeClass('success')
|
|
.text('There was an error in submitting your feedback.');
|
|
return;
|
|
}
|
|
element.find('.status').addClass('success').removeClass('error')
|
|
.text('Feedback submitted!');
|
|
element.modal('hide');
|
|
});
|
|
});
|
|
element.modal('show');
|
|
}
|
|
|
|
// Meteor template bindings.
|
|
|
|
Template.character.events({'click .panel-title-right': showModal});
|
|
|
|
Template.character.helpers({
|
|
character: () => character.get(),
|
|
metadata: () => metadata.get(),
|
|
tree: () => tree.get(),
|
|
|
|
linkify: linkify,
|
|
|
|
orientation: () => orientation.get(),
|
|
short: () => short.get(),
|
|
|
|
format: (label) => (short.get() ? label.slice(0, 3) + ':' : label),
|
|
horizontal: () => orientation.get() === 'horizontal',
|
|
vertical: () => orientation.get() === 'vertical',
|
|
});
|
|
|
|
Template.order.helpers({
|
|
animations: () => animations.get(),
|
|
strokes: () => (strokes.get() || []).slice().reverse(),
|
|
});
|
|
|
|
Meteor.startup(() => {
|
|
Deps.autorun(updateCharacter)
|
|
hashchange();
|
|
resize();
|
|
});
|
|
|
|
const hashchange = () => {
|
|
if (Session.get('route') === 'character') {
|
|
const hash = window.location.hash;
|
|
const codepoint = parseInt(hash.slice(hash.lastIndexOf('/') + 1), 10);
|
|
character.set(String.fromCharCode(codepoint));
|
|
[animations, metadata, strokes, tree].map((x) => x.set(null));
|
|
animation = null;
|
|
} else {
|
|
character.set(null);
|
|
}
|
|
}
|
|
|
|
window.addEventListener('hashchange', hashchange, false);
|