Commit 9a32a0e5 by Иван Кубота

zero version. first test generator

parents
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# production
/build
# misc
.DS_Store
.vscode
/.idea
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
package-lock.json
[
{
"id": 1,
"name": "мучное"
},
{
"id": 2,
"name": "мясо"
},
{
"id": 3,
"name": "сыр"
},
{
"id": 4,
"name": "сахар"
},
{
"id": 5,
"name": "варенье"
},
{
"id": 6,
"name": "сладкое"
},
{
"id": 7,
"name": "шоколад"
},
{
"id": 8,
"name": "морепродукты"
},
{
"id": 9,
"name": "овощи"
},
{
"id": 10,
"name": "шампунь"
},
{
"id": 11,
"name": "гигиена"
},
{
"id": 12,
"name": "чай"
},
{
"id": 13,
"name": "крупа"
},
{
"id": 14,
"name": "булгур"
},
{
"id": 15,
"name": "киноа"
},
{
"id": 16,
"name": "рис"
},
{
"id": 17,
"name": "молочный продукт"
},
{
"id": 18,
"name": "молочное"
},
{
"id": 19,
"name": "риет"
},
{
"id": 20,
"name": "харам"
},
{
"id": 21,
"name": "кускус"
},
{
"id": 22,
"name": "напиток"
},
{
"id": 23,
"name": "вода"
},
{
"id": 24,
"name": "паштет"
},
{
"id": 25,
"name": "салат"
},
{
"id": 26,
"name": "рыба"
},
{
"id": 27,
"name": "травы"
},
{
"id": 28,
"name": "канцтовары"
},
{
"id": 29,
"name": "суп"
}
]
\ No newline at end of file
import csv from 'csv-parser';
import fs from 'fs';
const readCSV = async function(scope) {
const {fileName, types, keys, index} = scope,
postProcess = scope.postProcess || (a => a);
const trash = Symbol('Trash')
const hash = {};
return await new Promise(function(resolve, reject) {
let id = 1;
const inc = ()=>id++;
fs.createReadStream(fileName)
.pipe(csv({
mapHeaders: ({header}) => keys[header] || trash,
mapValues: ({header, value}) => header in types ? types[header](value) : null
}))
.on('data', (obj) => {
postProcess(obj, inc);
hash[obj[index]] = obj;
})
.on('end', () => {
resolve(hash);
console.log('CSV parsed: '+ fileName)
});
});
};
import connections from './data/connections.json';
import tags from './data/tags.json';
export const data = {tags, connections};
(async()=>{
data.components = await readCSV({
fileName: './data/components.csv',
keys: {
'номер карточки': 'id',
'ингредиент': 'name'
},
types: {
id: Number,
name: String
},
postProcess: (a, inc) => a.iID = inc(),
index: 'iID'
});
data.products = await readCSV({
fileName: './data/products.csv',
keys: {
'': 'id',
'название': 'title',
'описание': 'description',
'категория': 'cat',
'подкатегория': 'subcat',
'подподкатегория': 'subsubcat',
'участвует в тесте': 'use'
},
types: {
id: Number,
title: String,
description: String,
cat: String,
subcat: String,
subsubcat: String,
use: a=>Boolean(Number(a))
},
index: 'id'
});
})();
const keys = {
'номер карточки': 'id',
'ингредиент': 'name'
};
const mapper = {
id: Number,
name: String
};
<!DOCTYPE html>
<html>
<head>
<title>Category mapper</title>
<meta charset="utf-8" />
<script src="js/releasable-observer.js"></script>
<script src="js/pcg-base.js"></script>
<script src="js/pcg-dom-util.js"></script>
<script src="js/csv.js"></script>
<script src="js/model/store.js"></script>
<script src="js/model/data.js"></script>
<script src="js/model/provider.js"></script>
<script src="js/model/slice/productTable.js"></script>
<script src="js/model/tag.js"></script>
<script src="js/model/product.js"></script>
<script src="js/helpers/textFilter.js"></script>
<script src="js/view/base.js"></script>
<script src="js/view/cmp/base.js"></script>
<script src="js/view/cmp/menu.js"></script>
<script src="js/view/cmp/switch.js"></script>
<script src="js/view/cmp/table.js"></script>
<script src="js/view/cmp/tag.js"></script>
<script src="js/view/cmp/tagfield.js"></script>
<script src="js/view/page/products.js"></script>
<script src="js/view/page/export.js"></script>
<script src="js/view/page/generate.js"></script>
<script src="js/controller/exportLogic.js"></script>
<script src="js/controller/quizGenerator.js"></script>
<script src="main.js"></script>
<link type="text/css" rel="stylesheet" href="main.css">
</head>
<body>
<script>initDataProvider($DATA$);init();</script>
</body>
</html>
\ No newline at end of file
var env = {};
const APP_HOST = env.APP_HOST || "127.0.0.1";
const APP_PORT = env.APP_PORT || 4000;
const DB_PATH = env.DB_PATH || "./db/users.json";
import {data} from "./db.js";
import App from 'express';
import Router from 'node-async-router';
const app = App(),
router = Router();
import fs from 'fs';
const tpls = {
index: fs.readFileSync('./index.html')+''
};
const print = function(data) {
return tpls.index.replace('$DATA$', data)
};
router.get('ivasic', async function(req, res) {
res.end(print('lalka'));
});
router.all('/', function(req, res) {
res.end(print(JSON.stringify(data)));
});
app.use(router);
app.use(App.static('public'))
app.listen(APP_PORT);
console.log(`LISTEN port :`+APP_PORT);
{
"type": "module",
"dependencies": {
"csv-parser": "^2.3.2",
"express": "^4.17.1",
"node-async-router": "^0.0.2"
},
"scripts": {
"start": "node --experimental-modules index.js"
}
}
function sqlEscape(str) {
return (str+'').replace(/[\0\x08\x09\x1a\n\r"'\\\%]/g, function (char) {
switch (char) {
case "\0":
return "\\0";
case "\x08":
return "\\b";
case "\x09":
return "\\t";
case "\x1a":
return "\\z";
case "\n":
return "\\n";
case "\r":
return "\\r";
case "\"":
case "'":
case "\\":
case "%":
return "\\"+char; // prepends a backslash to backslash, percent,
// and double/single quotes
default:
return char;
}
});
}
const indexKey = {
connections: 'cid',
tags: 'id'//'iID'
},
yamlEscape = function(a) {
return typeof a === 'number'? a : '\''+(a+'').replace(/'/,'\'\'')+'\''
};
const formatters = {
json: function(data) {
return JSON.stringify(data, null, 2);
},
csv: toCSV,
sql: function(data) {
if(data.length){
const item = data[0], header = [];
for(let key in item){
header.push(key);
}
const rows = data.map(item=>header.map(key=>item[key]));
return [ 'INSERT INTO `' + store.get( 'exportTableName' ) + '` (' + header.map( sqlEscape ).map( a => '`' + a + '`' ).join( ', ' ) + ') VALUES' ].concat(
rows.map( row => '(' + row.map( sqlEscape ).map( a => '\'' + a + '\'' ).join( ', ' ) + ')' ).join( ',\n' )
).join( '\n' ) + ';';
}else{
return ';'
}
},
yaml: function(data, exportData) {
const index = indexKey[exportData]
return data
.map(item=>
[exportData.substr(0,exportData.length-1)+item[index]+':'].concat(
Object.keys(item)
.filter(key=>key !== index)
.map(key=>' '+key+': '+ yamlEscape(item[key]))
).join('\n')
).join('\n')
}
};
const exportLogic = function(exportEl) {
store.sub(['exportData', 'exportFormat'], function(exportData, exportFormat) {
var arr = [];
var fullData = dP[exportData];
for(var k in fullData){
arr.push(fullData[k]);
}
exportEl.value = exportFormat in formatters ? formatters[exportFormat](arr, exportData): 'Create formatter '+exportFormat;
});
return exportEl;
};
\ No newline at end of file
const rand = function(a, b){
if(Array.isArray(a)){
return a[Math.random()*a.length|0];
}
a = Math.ceil(a);
b = Math.floor(b);
return Math.floor(Math.random() * (b - a + 1)) + a;
return (r-a)*(b-a)|0;
},
probabilityRand = function(items) {
let sum = 0;
for( let i = 0, _i = items.length; i < _i; i++ ){
const item = items[ i ];
sum += item.probability |0;
}
const theRandom = Math.random();
let total = 0;
for( let i = 0, _i = items.length; i < _i; i++ ){
const item = items[ i ];
total += item.probability/sum;
if(theRandom < total)
return item;
}
};
const normalizeText = function(text) {
return text.trim().toLowerCase().replace(/[^а-я]/g,'').replace(/[аоуеэюёиыя]+/g,'о')
};
const quizTypes = {
checkbox: [],
checkboxPhoto: {
},
radio: [{
type: 'Do not contain',
from: ()=>
rand(
Object.keys(dP.products)
.map(k=>dP.products[k])
.filter((p)=>dP.Product.getComponents(p).length > 1)
),
answer( product, log ){
const out =
shuffle(
dP.Product.getComponents(product)
.map(i=>({correct: false, text: i.name}))
)
.slice(0, rand(this.answers.from - 1, this.answers.to - 1)),
// get tags
tags = product.tags,
donors = [];
log.push('Base product: '+product.title);
log.push(`Have ${dP.Product.getComponents(product).length} components. Took ${out.length}:`);
out.forEach(function(a) {
log.push(' > '+ a.text)
});
log.push('\nTags: '+dP.Product.getTags(product).map(t=>t.name).join(', '));
Object.keys(dP.products)
.map(k=>dP.products[k])
.forEach(item => {
if(item.id === product.id)
return;
const matchedCount = tags.filter(tid=>item.tags.indexOf(tid)>-1).length;
if(matchedCount>0){
donors.push({
count: matchedCount,
item,
title: item.title,
component: dP.Product.getComponents(item).map(a=>a.name)
});
}
});
log.push('Matched '+donors.length+' component donors.');
donors.sort((a,b)=>b.count-a.count);
if(!donors.length){
log.push('Not enough');
return false;
}
const maxCount = donors[0].count;
const minCount = Math.round(maxCount - maxCount/4);
const alreadyExist = out.reduce((store, i)=>{
store[normalizeText(i.text)] = true;
return store;
}, {'соль': true,'сольпощово': true, 'сохор': true});
log.push('Nearest candidate have got '+ maxCount +' similar tag.');
const may = ([].concat.apply(
[],
donors
.filter(i=>i.count > 0 && i.count >= minCount)
.map(i=>
i.component
.filter(c=>{
if(!c.trim())
return false;
const normalized = normalizeText(c);
if(normalized in alreadyExist)
return false;
alreadyExist[normalized] = true;
return true;
})
.map(x => ({count: i.count, item: i.item, text: x}))
)
));
log.push('Filtered candidates with ['+ (minCount===maxCount? minCount : minCount+' - '+ maxCount) +'] similar tags.');
if(may.length<2)
return false;
//console.clear()
//const addAnswersCount = Math.min(rand(this.answers.from, this.answers.to)-out.length, may.length);
const answer = rand(may);
log.push('Use '+ answer.item.title +' as donor.');
log.push('Because it has '+ answer.count +' similar tags: '+ dP.Product.getTags(answer.item).map(t=>t.name).join(', '));
out.push({correct: true, text: answer.text});
log.push('\n << '+ answer.text);
/*
for(let i = 0; i < addAnswersCount; i++){
if(may.length-i<0)
break;
const idx = Math.random()*(may.length-i)|0;
if(idx>=may.length || idx < 0)debugger
console.log(i, may[idx], may)
out.push({correct: true, text: may[idx]});
may[idx] = may[may.length - 1 - i]
}*/
console.log(dP.Product.getTags(product), donors)
return out;
},
answers: {from: 3, to: 6},
question: (product)=> `Выберите лишний ингредиент, НЕ входящий в продукт "${product.title}"`,
probability: 10
}],
radioPhoto: {
}
};
const shuffle = function (a) {
for (let i = a.length - 1; i > 0; i--) {
const j = Math.floor(Math.random() * (i + 1));
[a[i], a[j]] = [a[j], a[i]];
}
return a;
}
const quizGenerator = function(type, photo) {
const cfg = probabilityRand(quizTypes[type+(photo?'Photo':'')]);
const log = ['Quiz generate '+type+(photo ? ' with photo':'') +'. '+(cfg?cfg.type:'FAIL')];
if(!cfg)
return {answers: [], log};
const source = cfg.from.call(cfg, log),
answers = shuffle(cfg.answer.call(cfg, source, log));
if(answers === false){
// давай по новой
return quizGenerator( type, photo );
}
return {question: cfg.question.call(cfg, source, log), answers, log}
};
/*
К какому из продуктов относится это описание: "Важно знать"
В какой из продуктов входят данные ингредиенты: Ингредиент 1, Ингредиент 2, Ингредиент 3, Ингредиент 4
Выберите один ингредиент, который входит в продукт "Название продукта"
Какое из этих описаний относится к продукту "Название продукта"
Какие ингредиенты входят в продукт "Название продукта"
Какие ингредиенты НЕ входят в продукт "Название продукта"
В какие продукты входит следующий ингредиент "Название ингредиента"
Какой продукт изображен на картинке
Какое из этих описаний относится к продукту, изображенному на картинке
Выберите один ингредиент, который входит в продукт, изображенный на картинке
Какие ингредиенты входят в продукт, изображенный на картинке?
Какие ингредиенты НЕ входят в продукт, изображенный на картинке
*/
\ No newline at end of file
const checkSpecialCharsAndEmpty = (value) => {
const thisValue = value.toString().toLowerCase();
let hasSpecialChars = false;
if (typeof value === 'string') {
hasSpecialChars = thisValue.includes('\n')
|| thisValue.includes('\t')
|| thisValue.includes(',')
|| thisValue.includes(';')
|| thisValue.includes('.')
|| thisValue.includes('"')
|| thisValue.includes('\'')
|| thisValue.includes('`')
|| thisValue.includes('´')
|| thisValue.includes(' ')
|| thisValue.length === 0;
}
return hasSpecialChars;
};
const separatorOrLineBreak = (length, elementIdx, separator) => (
length - 1 === elementIdx ? '\n' : separator
);
const escapeDoubleQuotesInsideElement = (element) => {
const thisElement = element.replace(/"/g, '""');
return thisElement;
};
const appendElement = (element, lineLength, elementIdx, separator) => {
const includesSpecials = checkSpecialCharsAndEmpty(element);
let thisElement = element;
if (includesSpecials) {
thisElement = escapeDoubleQuotesInsideElement(thisElement);
}
return (
includesSpecials
? `"${thisElement}"${separatorOrLineBreak(lineLength, elementIdx, separator)}`
: `${thisElement}${separatorOrLineBreak(lineLength, elementIdx, separator)}`
);
};
const toCSV = data => convertArrayOfObjectsToCSV(data, {separator: ','});
const convertArrayOfObjectsToCSV = (data, { header, separator }) => {
const array = [...data];
let csv = '';
if (header) {
header.forEach((headerEl, i) => {
const thisHeaderEl = headerEl || (headerEl === 0 ? 0 : '');
csv += appendElement(thisHeaderEl, header.length, i, separator);
});
}
array.forEach((row, idx) => {
const thisRow = Object.keys(row);
if (!header && idx === 0) {
thisRow.forEach((key, i) => {
const value = key || (key === 0 ? 0 : '');
csv += appendElement(value, thisRow.length, i, separator);
});
}
thisRow.forEach((key, i) => {
const value = row[key] || (row[key] === 0 ? 0 : '');
csv += appendElement(value, thisRow.length, i, separator);
});
});
return csv;
};
\ No newline at end of file
var textCache = {};
const textFilterMatched = function(where, what) {
var prepared = textCache[what];
if(!prepared){
var tokens = what.split(/\s/)
textCache[what] = prepared = tokens.map(token => token[0] === '-' ? {has: false, text: token.substr(1)}: {has: true, text: token});
}
var yep = false;
for( var i = 0, _i = prepared.length; i < _i; i++ ){
var preparedElement = prepared[ i ];
var matched = where.indexOf(preparedElement.text)>-1;
if(preparedElement.has === false && matched){
return false
}
if(matched)
yep = true;
}
return yep;
};
const textFormat = function(text) {
if(typeof text !== 'string')
return text;
const income = text;
if(text.split(/[«"»]/).length % 2 === 0){
// odd count of quotes
text = text.replace(/[«»]/,'')
}
let valids = {}, id = 1, add = (val)=>{
const uid = '_!@#'+id+'#@!_';
id++;
valids[uid] = val;
return uid;
};
text = text
.trim()
.replace(/\n\n/g,'|||||')
.replace(/\s\s+/g,' ')
.replace(/ /g,' ')
.replace(/([а-я][-–—][0-9][-–—][а-я])/g, add)
.replace(/(\d)(руб)/g,'$1 $2')
//.replace(/([а-яА-Я])[—-]([а-яА-Я])/g, (full, a, b)=>add(a+'-'+b))
.replace(/([а-яА-Я])[—-]([а-яА-Я])/g, (full, a, b)=>add('<nobr>'+a+'-'+b+'</nobr>'))
.replace(/([0-9])[,]([0-9])/g, (full, a, b)=>add(a+','+b))
.replace(/(ВкусВилл)/g, add)
.replace(/([^\s])[—-]/g, '$1 —')
.replace(/([,\.:])([а-яА-Я0-9\.,"])/g, '$1 $2')
.replace(/\s([,\.:])/g, '$1')
.replace(/([\(])\s/g, '$1')
.replace(/[^\s]([\(])/g, ' $1')
.replace(/[\s+]([\)])/g, '$1')
.replace(/([а-я\?\.])(\d+\.)/g, '$1 $2')
.replace(/ [-–—] /g, '&nbsp;— ')
.replace(/-(\d)/g, '–$1')
.replace(/ (\d\. [а-яА-Я])/g, '\n$1')
.replace(/"([а-яА-Я][^"]+)\s+"([а-яА-Я\s][^"]+)"\s+([а-яА-Я][^"]+)"/g,'«$1 “$2” $3»')
.replace(/"([^"]+)"/g,'«$1»')
.replace(\s+/g,'«')
.replace(/\s+»/g,'»')
.replace(/([а-я])([А-Я])/g,(ful, a, b)=>a+' '+b.toLowerCase())
//.replace(/\. /g,'.&shy; ')
// phone
.replace(/(\+7|8)[\s\(]*(\d{3})[\s\)]*(\d{3})[\s\-—–]*(\d{2})[\s-—–]*(\d{2})/, '+7&nbsp;($2)&nbsp;$3-$4-$5')
.replace(/[А-Я]+/g, function(text) {
return text.length > 2 || text === 'НЕ' ? '<span class="important">'+text.toLowerCase()+'</span>': text;
})
.replace(/свершении/g,'совершении')
.replace(/, ([А-Я])/g, (f, a)=>', '+a.toLowerCase())
.replace(/\|\|\|\|\|/g,'\n\n')
.replace(/\*\*([^*]+)\*\*/g,'<span class="notificate-text">$1</span>')
;
// HANGING
/*
text = text
.replace(/([«(])/g,'<span class="before-hanging-open"></span>&shy;<span class="hanging-open">$1</span>')
.replace(/([»)])/g,'<span class="hanging-close">$1</span>');
*/
//<span class="hanging-close">»</span>');
var lines = text.split('\n');
var numberStarting = 0;
var lastNumber = 0;
var remake = [];
lines.forEach(function(line) {
line.replace(/^(\d+)\.\s/, function(full, num) {
num = num-0;
if(lastNumber !== num - 1){
if(lastNumber>num){
// ok
}else{
var text = remake.pop();
while(lastNumber<num - 1){
text.replace(new RegExp('[^\\d]'+(lastNumber+1)+'[^\\d]'), function(ful, pos,a){
remake.push(text.substr(0, pos).replace(lastNumber+1, lastNumber+1+'.'))
text = text.substr(pos+(lastNumber+1+'').length)
});
lastNumber++;
}
remake.push(text.replace(lastNumber, lastNumber+'.'));
}
}
lastNumber = num;
});
remake.push(line);
});
var longSentance = 200;
if(remake.length === 1 && remake[0].length>longSentance){
remake = remake[ 0 ].split( '. ' ).map( a => a + '.' ).reduce((store, sentance)=>{
var last = (store.pop()||'');
if(last.length+sentance.length< longSentance*1.3){
store.push([ last, sentance ].join( ' ' ));
}else{
if(last)
store.push(last);
store.push(sentance);
}
return store
},[]);
remake = [].concat.apply( [], remake.map( a => [ a, '' ] ));
}
let out = remake.length === 1 ? remake[0] : remake.map(a=>'<p>'+a+'</p>').join('\n');
out = out.replace(/_!@#(\d+)#@!_/g, (f, num)=>{
return valids[f]
});
console.log(income);
console.log(out);
return out;
};
\ No newline at end of file
const store = new Store({
mainMenuActive: 'products',
exportFormat: 'yaml',
exportData: 'tags',
exportTableName: 'SOMETABLE',
productFilterText: '',
'productFilterByTitle': true,
'productFilterByComponent': true,
'productFilterByTag': true
});
try{
var data = JSON.parse( localStorage.getItem( 'store' ) );
console.log(data)
for(var k in data){
store.set(k, data[k]);
}
}catch( e ){}
store.on('change', function() {
localStorage.setItem('store', JSON.stringify(store._props));
});
dP.Product = {
getByID: function(id) {
return dP.products[id];
},
getComponents: function(product) {
return dP.componentsListHashByProduct[ product.id ] || [];
},
getTags: function(product) {
return product.tags.map(dP.Tag.getByID);
}
};
\ No newline at end of file
const dataProvider = {
maxConnection: 0,
maxTagID: 0,
slice: {},
tags: [],
connections: {},
products: {}
},
dP = dataProvider,
initDataProvider = function(data) {
Object.assign(dP, data);
Object.assign(dP, {
componentsList: [],
componentsListHashByProduct: {}
});
let key;
for(key in data.components){
const cmp = data.components[key];
cmp.tags = [];
dP.componentsList.push(cmp);
(dP.componentsListHashByProduct[cmp.id] || (dP.componentsListHashByProduct[cmp.id] = [])).push(cmp);
}
dP.tagsHash = data.tags.reduce((store, item)=>{
dP.maxTagID = Math.max(dP.maxTagID, item.id);
store[item.id] = item;
return store
}, {});
for(key in data.products){
let product = data.products[key];
product.tags = [];
}
data.connections.forEach((connection) => {
dP.maxConnection = Math.max(dP.maxConnection, connection.cid);
if(connection.type === 0){
data.products[connection.eid].tags.push(connection.tag)
}
});
};
dP.slice.productTable = {
getItems: function() {
return Object.keys(dP.products)
.map(k=>dP.products[k])
.map(item=>{
dP.slice.productTable.updateFilterCache(item);
return item;
})
},
updateFilterCache: function(item) {
item._titleFilter = item.title.toLowerCase();
item._componentsFilter = (dP.componentsListHashByProduct[item.id] || []).map(a=>a.name).join(' ');
item._tagsFilter = (item.tags || []).map(t=>dP.tagsHash[t].name).join(' ');
}
};
\ No newline at end of file
const isEqual = function(original, fn) {
return function(val) {
fn(val===original);
}
};
const Store = function(cfg) {
Observable.call(this);
this._props = cfg || {};
};
Store.prototype = {
set: function(key, val) {
if(this._props[key] !== val){
this._props[ key ] = val;
this.fire( 'change', key, val );
this.fire( key, val );
}
},
get: function(key) {
return this._props[key];
},
sub: function(key, fn) {
if(Array.isArray(key)){
var wrap = () => fn.apply(this, key.map(key=>this.get(key)));
for( var i = 0, _i = key.length; i < _i; i++ ){
this.on(key[i], wrap)
}
wrap();
}else{
this.on( key, fn )
fn( this.get( key ) );
}
return this;
},
equal: function(key, val, fn) {
const wrap = isEqual(val, fn);
this.on(key, wrap);
wrap(this.get(key));
return this;
}
};
Store.prototype = Object.assign(new Observable, Store.prototype);
dP.Tag = {
getOrCreate: function(val) {
const matched = dP.Tag.getByName(val);
if(matched)
return matched;
// create
const tagID = ++dP.maxTagID,
item = {
id: tagID,
name: val
};
dP.tagsHash[tagID] = item;
dP.tags.push(item);
return item;
},
remove: function(tagID) {
dP.tags.splice(dP.tags.indexOf(dP.tagsHash[tagID]), 1);
delete dP.tagsHash[tagID];
},
getByID: function(tagID) {
return dP.tagsHash[tagID]
},
getByName: function(name) {
return dP.tags.filter(a=>a.name === name)[0];
},
disconnectFromProduct: function(product, tag) {
const tagID = tag.id;
if(product.tags.indexOf(tagID) > -1){
product.tags.splice( product.tags.indexOf(tagID), 1 );
const connection = dP.connections
.filter(connection =>
connection.tag === tagID &&
connection.type === 0 &&
connection.eid === product.id
)[0];
if(!connection)
return;
if(connection.cid === dP.maxConnection-1)
dP.maxConnection--;
dP.connections.splice(
dP.connections.indexOf(
connection
) ,1);
dP.Tag.removeIfNoConnections(tag);
dP.slice.productTable.updateFilterCache(product);
}
},
connectToProduct: function(product, tag) {
const tagID = tag.id;
if(product.tags.indexOf(tagID) === -1){
product.tags.push( tagID );
dP.connections.push({
cid: ++dP.maxConnection,
type: 0,
eid: product.id,
tag: tagID
});
dP.slice.productTable.updateFilterCache(product);
}
},
removeIfNoConnections: function({tagID}) {
if(dP.connections.filter(connection=>connection.tag === tagID).length === 0){
dP.Tag.remove(tagID)
}
}
};
\ No newline at end of file
// Pico pico graph!
var PCG = window['PCG'] = function(cfg) {
if(cfg.consts){
this.consts = PCG.apply(PCG.apply({}, this.consts), cfg.consts);
delete cfg.consts;
}
PCG.apply(this, cfg);
this._update = this._update.bind(this);
this.resize = this.resize.bind(this);
this.init();
};
PCG.apply = function(a,b) {
for(var k in b){
a[k] = b[k];
}
return a;
};
PCG.ZOOM = {
PIE: 1,
CUSTOM: 2,
LOAD: 3
};
\ No newline at end of file
(function(PCG){
var svgNS = 'http://www.w3.org/2000/svg';
// I am too lazy to do DOM manually / anyway this solution is optimal enough
var setters = {
cls: function(el) {
return function(cls) {
if( el.tagName.toLowerCase() === 'svg' ){
el.setAttribute( 'class', cls );
}else{
el.className = cls;
}
}
},
attr: function(el, attrName) {
return function(val) {
if(val !== void 0 && val !== false){
el.setAttribute( attrName, val );
}else{
el.removeAttribute(attrName)
}
}
}
};
// ~jsx h function
var domEl = function( type, cfg ){
cfg = cfg || {};
var cls = cfg.cls,
style = cfg.style,
attr = cfg.attr,
prop = cfg.prop,
on = cfg.on,
renderTo = cfg.renderTo,
el = cfg.el || document.createElement( type ),
classList = el.classList;
var i, _i;
if( cls ){
if(typeof cls === 'function'){
cls(setters.cls(el));
}else{
if( el.tagName.toLowerCase() === 'svg' ){
el.setAttribute( 'class', cls );
}else{
el.className = cls;
}
}
//if(el.className !== cls)debugger
//cls.split( ' ' ).forEach( function( clsItem ){ classList.add( clsItem ); });
}
if( style ){
PCG.apply( el.style, style );
}
for( i in attr ){
if(attr.hasOwnProperty( i )){
if( typeof attr[ i ] === 'function' ){
attr[ i ]( setters.attr( el, i ) );
}else{
setters.attr( el, i )( attr[ i ] );
}
}
}
for( i in prop ){
prop.hasOwnProperty( i ) && ( el[ i ] = prop[ i ] );
}
for( i in on ){
on.hasOwnProperty( i ) && el.addEventListener( i, on[ i ] );
}
for( i = 2, _i = arguments.length; i < _i; i++ ){
var child = arguments[ i ];
D.appendChild( el, child );
}
if( renderTo ){
D.appendChild( renderTo, el );
}
return el;
};
var D = PCG.D = {
svg: null,
label: null,
div: null,
span: null,
path: null,
canvas: null,
input: null,
textarea: null,
tBody: null,
tHead: null,
th: null,
td: null,
tr: null,
Text: function( val ){ return document.createTextNode( val );}
};
'div,span,input,label,canvas,span,textarea,table,tr,td,th,tBody,tHead'.split( ',' ).forEach( function( name ){
D[ name ] = function(){
return domEl.apply( null, [ name ].concat([].slice.call(arguments)))
};
} );
'svg,path,circle'.split( ',' ).forEach( function( name ){
D[ name ] = function( cfg){
if( !cfg ){
cfg = {};
}
cfg.el = document.createElementNS( svgNS, name );
cfg.el.setAttribute( 'xmlns', svgNS );
return domEl.apply( null, [ null ].concat([].slice.call(arguments)))
};
} );
D.html = function(cfg){
var el = domEl('div', cfg);
el.innerHTML = [].slice.call(arguments,1).join('\n');
return el;
};
D.removeChildren = function(el){
var subEl;
while((subEl = el.lastChild)){
el.removeChild(subEl);
}
};
D.appendChild = function(el, subEl){
var type = typeof subEl;
if( type !== 'object' ){
if(subEl !== void 0 && subEl !== false){
el.appendChild( D.Text( subEl ) );
}
}else if('dom' in subEl){
el.appendChild( subEl.dom );
}else if( Array.isArray(subEl) ){
subEl.forEach(subEl => D.appendChild( el, subEl ) );
}else{
el.appendChild( subEl );
}
};
D.join = function(arr, delimiter){
var out = [], isFn = typeof delimiter === 'function';
for( var i = 0, _i = arr.length - 1; i < _i; i++ ){
out.push(arr[i], isFn?delimiter():delimiter);
}
if(i < _i+1)
out.push(arr[i]);
return out;
};
D.cls = function() {
var out = [], i = 0, _i = arguments.length, token, tmp, key;
for(;i<_i;i++){
token = arguments[i];
if(typeof token === 'string'&& token){
out.push( token );
}else if(typeof token === 'object'){
if(Array.isArray(token)){
tmp = D.cls.apply(null, token);
tmp && out.push( token );
}else{
for(key in token){
token[key] && out.push( key );
}
}
}else if(typeof token === 'function'){
}
}
return out.join(' ');
};
D.escapeCls = function(cls) {
return (cls+'').replace(/[^a-zA-Z0-9\-_]/g,'');
}
})(window['PCG']);
\ No newline at end of file
var Observable = function() {
this._listeners = {};
};
Observable.prototype = {
on: function(k, v) {
(this._listeners[k] || (this._listeners[k] = [])).push(v);
var _self = this;
return function ReleaseObservable() {
_self.un(k, v);
};
},
un: function(k, v) {
var list = this._listeners[k];
if(list){
var id = list.indexOf(v);
if(id > -1){
list.splice(id, 1);
}
}
},
fire: function(k) {
var listeners = this._listeners[k];
if(listeners === void 0)
return;
for( var i = 0, _i = listeners.length; i < _i; i++ ){
const listener = listeners[ i ];
listener.apply(this, [].slice.call(arguments, 1));
}
}
};
const D = PCG.D;
const {div, span} = D;
const view = {
page: {},
cmp: {}
};
\ No newline at end of file
view.cmp.Menu = ({title, id, key, cls}) =>
div({
cls: update => store.equal(
key,
id,
active => update(
D.cls(
'main-menu-item',
'main-menu-item__'+D.escapeCls(id),
cls,
{'main-menu-item__active':active}
)
)
),
on: {
click: ()=> store.set(key, id)}
}, title);
\ No newline at end of file
view.cmp.Switch = (cfg, contentHash) => {
const cmp = div( {
cls: update => store.sub(
cfg.key,
val => update(
D.cls(
'cmp-switch',
cfg.cls,
{ 'cmp-switch__filled': val in contentHash } ) ) ) } );
store.sub( cfg.key, (val)=> {
D.removeChildren(cmp);
if(val in contentHash)
D.appendChild( cmp, contentHash[ val ] );
});
return cmp;
};
\ No newline at end of file
view.cmp.Table = function(cfg){
Object.assign(this, cfg);
const children = this.childrenEl = D.div({
cls: 'cmp-table__children'
});
this.dom = D.div({
cls: D.cls('cmp-table', cfg.cls)
}, children);
this.currentSlice = [];
this.childrenEls = [];
setTimeout(()=>this.updateChildren(),0);
};
view.cmp.Table.prototype = {
currentSlice: [],
childrenEls: [],
childrenEl: null,
filterText: '',
filterFn: () => true,
afterFilter: ()=>true,
filter: function(text) {
this.filterText = text;
this.filterRegExp = new RegExp(text.replace(/[\?\*\.\+]/g,''), 'ig')
},
fetch: function() {
const field = Object.keys(this.sort[0])[0]
this.currentSlice = this.items
.filter(item=>this.filterFn.call(item, this.filterText, this))
.sort((a,b)=>a[field] > b.field ? 1 : a[field] < b.field ? -1 : 0)
.map(data=>({data, dom: null}));
this.afterFilter(this.getSelected());
},
updateChildren: function() {
this.fetch();
D.removeChildren(this.childrenEl);
this.currentSlice.forEach((item) => {
D.appendChild(this.childrenEl, item.dom = this.itemTpl(item.data, this, item));
})
},
updateChild: function(item) {
if(item.dom){
const dom = this.itemTpl(item.data, this, item);
item.dom.parentNode.replaceChild(dom, item.dom);
item.dom = dom;
}
},
updateChildByData: function(data) {
const match = this.currentSlice.filter(i=>i.data===data),
item = match[0];
if(!item)
return;
if(item.dom){
const dom = this.itemTpl(item.data, this, item);
item.dom.parentNode.replaceChild(dom, item.dom);
item.dom = dom;
}
},
getSelected: function() {
return this.currentSlice.filter(i=>!i.hidden).map(i=>i.data)
},
highlight: function(text) {
if(!this.filterText)
return text;
return D.join(text.split(this.filterRegExp), ()=>D.span({cls: 'highlighted'}, this.filterText))
},
getActionDelegate: function() {
var me = this;
return function(e) {
const [type, val] = this.getAttribute('data-action').split(':');
const match = me.currentSlice.filter(({data})=>data.id+'' === val+'');
if(type === 'hide'){
if(match.length){
match[0].hidden = true;
me.updateChild(match[0])
me.afterFilter(me.getSelected());
}
}
if(type === 'toggle'){
if(match.length){
match[0].full = !match[0].full;
me.updateChild(match[0])
}
}
e.stopPropagation();
}
},
};
\ No newline at end of file
view.cmp.Tag = function(text) {
return D.span({cls: 'tag'}, '#', text)
};
\ No newline at end of file
view.cmp.TagField = (function(){
const TagField = function( data ){
Observable.call( this );
var field, isFocused = false, lastVal, dropDown;
//var cacheTagsDom = {};
var offset = 0;
var matchedItems = 0;
var neBlur = false;
var autocomplete = ( e ) => {
offset = 0;
var val = field.value.trim().toLowerCase();
if( lastVal !== val ){
var matched = val === '' ? [] : dP.tags.filter( t => t.name.toLowerCase().trim() !== val && t.name.toLowerCase().trim().indexOf( val ) > -1 ).slice( 0, 20 );
this.value = lastVal = val;
matchedItems = matched.length;
D.removeChildren( dropDown );
matched.forEach( function( match ){
D.appendChild( dropDown, div( {
cls: 'cmp-tag-dropdown-item',
on: {
click: () => {
field.value = match.name;
autocomplete();
},
mousedown: () => {
neBlur = true;
setTimeout( () => neBlur = false, 40 )
clearTimeout( blurTimeout )
}
}
},
view.cmp.Table.prototype.highlight.call( {
filterText: val,
filterRegExp: new RegExp( val, 'ig' )
}, match.name ) ) );//cacheTagsDom[match.id] || (cacheTagsDom[match.id] = );
} );
dropDownUpdateCls();
}
};
var _dropDownUpdateCls,
dropDownUpdateCls = function(){
_dropDownUpdateCls(
D.cls( 'cmp-tag-dropdown', {
'cmp-tag-dropdown__hidden': matchedItems === 0 || isFocused === false
} )
)
};
var blurTimeout;
var el = div( { cls: 'cmp-tag-field' },
field = D.input( {
cls: 'cmp-tag-input',
attr: { placeholder: 'Тэг' },
on: {
input: autocomplete,
focus: () => {
clearTimeout( blurTimeout );
isFocused = true;
dropDownUpdateCls()
},
blur: () => {
if( !neBlur ){
blurTimeout = setTimeout( function(){
isFocused = false;
dropDownUpdateCls();
}, 100 )
}
}
}
} ),
dropDown = div( {
cls: ( update ) => {
_dropDownUpdateCls = update;
dropDownUpdateCls();
}
} )
);
this.dom = el;
};
TagField.prototype = {
getValue: function(){
return this.value;
}
};
TagField.prototype = Object.assign( new Observable, TagField.prototype );
return TagField;
})();
\ No newline at end of file
view.page.Export = function() {
this.dom =
div({cls: 'export-panel'},
div({cls: 'title-gradient'},
div({cls: 'export-sub-menu'},
div({cls: 'export-sub-menu'},
view.cmp.Menu({title: 'Тэги', id: 'tags', key: 'exportData'}),
view.cmp.Menu({title: 'Связи', id: 'connections', key: 'exportData'})
),
div({cls: 'export-sub-menu'},
view.cmp.Menu({title: 'CSV', id: 'csv', key: 'exportFormat'}),
view.cmp.Menu({title: 'JSON', id: 'json', key: 'exportFormat'}),
view.cmp.Menu({title: 'YAML', id: 'yaml', key: 'exportFormat'}),
view.cmp.Menu({title: 'SQL', id: 'sql', key: 'exportFormat'}),
)
)
),
exportLogic(D.textarea({cls: 'export-data'})),
div({cls: 'export-comment'}, `Connetction type<Enum>: 0 — продукт, 1 — компонент`)
);
};
\ No newline at end of file
view.cmp.Answer = function(answer, type) {
return D.label({cls: D.cls('quiz-answer-label', 'quiz-answer-label__'+type)},
D.input({
attr: {type, checked: answer.correct}
}),
answer.text
)
};
view.page.Generate = function() {
const update = function() {
const photo = store.get('generatePhoto') === 'photo',
type = store.get('generateType'),
result = quizGenerator(type, photo);
title.innerText = result.question;
debug.value = result.log.join('\n')
D.removeChildren(answers);
D.appendChild(answers, result.answers.map((a)=>view.cmp.Answer(a, type)));
};
let title, answers, debug;
this.dom =
div({cls: 'generate-panel'},
div({cls: 'title-gradient'},
div({cls: 'generate-sub-menu'},
div({cls: 'generate-sub-menu'},
view.cmp.Menu({title: 'Единственный', id: 'radio', key: 'generateType'}),
view.cmp.Menu({title: 'Множественный', id: 'checkbox', key: 'generateType'})
),
div({cls: 'generate-sub-menu'},
view.cmp.Menu({title: 'С фото', id: 'photo', key: 'generatePhoto'}),
view.cmp.Menu({title: 'Без фото', id: 'noPhoto', key: 'generatePhoto'})
)
)
),
div({cls: 'generate-controls'},
D.input({
attr: {type: 'button', value: 'Refresh'},
on: {click: update}})),
div({cls: 'generate-example'},
title = div({cls: 'generate-title'}),
answers = div({cls: 'generate-answers'}),
debug = D.textarea({cls: 'generate-debug'})
)
);
store.sub(['generateType', 'generatePhoto'], update);
};
\ No newline at end of file
view.page.Products = function() {
let productTable, productFilterByTitle, productFilterByComponent, productFilterByTag, productFilterText,
tagField;
this.dom = div({cls: 'content-products'},
div({cls: 'product-filter__area title-gradient'},
div({cls: 'product-filter'},
div({cls: 'product-filter__title'},'Фильтр'),
D.input({
attr: {
value: (update)=>store.sub('productFilterText', (val)=>update(val))},
cls: 'product-filter__input',
on: {input: (e)=>store.set('productFilterText', e.target.value)}})
),
div({cls: 'product-filter'},
span({cls: 'product-filter__hint'},'Область фильтрации:'),
D.label({cls: 'product-filter__label'},
D.input({
attr:{
type: 'checkbox',
checked: (update)=>store.sub('productFilterByTitle', (val)=>update(val))},
on: {input: (e)=>{store.set('productFilterByTitle', e.target.checked)}}
}), 'Название'
),
D.label({cls: 'product-filter__label'},
D.input({
attr:{
type: 'checkbox',
checked: (update)=>store.sub('productFilterByComponent', (val)=>update(val))},
on: {input: (e)=>{store.set('productFilterByComponent', e.target.checked)}}
}), 'Ингредиенты'
),
D.label({cls: 'product-filter__label'},
D.input({
attr:{
type: 'checkbox',
checked: (update)=>store.sub('productFilterByTag', (val)=>update(val))},
on: {input: (e)=>{store.set('productFilterByTag', e.target.checked)}}
}), 'Тэги'
)
)
),
div({cls: 'tag-manipulations'},
D.input({attr:{type: 'button', value: 'Удалить тэг'}, on: {
click: ()=> {
var val = tagField.getValue().trim();
if(val!==''){
var tag = dP.Tag.getByName(val);
if(!tag)
return;
productTable.getSelected().forEach(function(item) {
dP.Tag.disconnectFromProduct(dP.Product.getByID(item.id), tag);
productTable.updateChildByData(item);
});
}
}}}),
tagField = new view.cmp.TagField(),
D.input({
cls: update =>
store.sub('productFilteredCount', count =>
update(D.cls('tag-manipulations-add-tag', {
'tag-manipulations__not-active': count === 0 || count > 30}))),
attr:{type: 'button', value: 'Добавить тэг'},
on: {
click: ()=> {
var val = tagField.getValue().trim();
if(val==='')
return;
const tag = dP.Tag.getOrCreate(val);
productTable.getSelected().forEach(function(item) {
dP.Tag.connectToProduct(dP.Product.getByID(item.id), tag);
productTable.updateChildByData(item);
});
}
}
}),
),
productTable = new view.cmp.Table({
sorters: [{id: 'title', type: String}],
sort: [{title: 'asc'}],
filterBy: {title: true, tag: true, component: true},
filterFn: function(text, me) {
return !text ||
(me.filterBy.title && textFilterMatched(this._titleFilter, text)) ||
(me.filterBy.tag && textFilterMatched(this._tagsFilter,text)) ||
(me.filterBy.component && textFilterMatched(this._componentsFilter,text))
},
afterFilter: (items)=>store.set('productFilteredCount', items.length),
itemTpl: (item, me, bonus) => {
const dom = div({
cls: 'table-item table-item__product',
on: { click: tableAction },
attr:{ 'data-action': 'toggle:'+ item.id}
},
div( {
cls: 'table-item-action table-item-collapse',
attr: { title: 'Скрыть', 'data-action': 'hide:' + item.id },
on: { click: tableAction }
}, '←' ),
span( { cls: 'product-title' }, me.highlight( item.title ) ),
( item.tags || [] ).map( t => me.highlight( dP.tagsHash[ t ].name ) ).map( view.cmp.Tag ),
bonus.full && D.html( { cls: 'product-description' }, textFormat(item.description) ),
div(
{ cls: 'table-item__product-components' },
D.join( ( dP.componentsListHashByProduct[ item.id ] || [] ).map( a => D.span( { cls: 'product-cmp' }, me.highlight( a.name ) ) ), ', ' )
)
);
if(bonus.hidden){
if(bonus.hiddenFull){
dom.classList.add( 'table-item__hidden-full' );
}else{
setTimeout( () => {
dom.classList.add( 'table-item__hidden' );
}, 1 )
setTimeout( () => {
bonus.hiddenFull = true;
dom.classList.add( 'table-item__hidden-full' );
}, 300 )
}
}
return dom;
},
items: dP.slice.productTable.getItems()
})
);
const tableAction = productTable.getActionDelegate();
store.sub([
'productFilterText',
'productFilterByTitle',
'productFilterByComponent',
'productFilterByTag'
], function(text, title, component, tag) {
Object.assign(productTable.filterBy, {title, component, tag});
productTable.filter(text.trim().toLowerCase());
productTable.updateChildren();
});
};
\ No newline at end of file
body {
font-family: Verdana, Serif;
}
.main-menu, .export-sub-menu {
display: flex;
}
.main-menu-item.main-menu-item__active {
border-bottom: 5px solid #00832a;
}
.main-menu-item {
flex-direction: row;
padding: 4px 8px 4px;
margin: 5px 5px 2px 5px;
border-bottom: 5px solid transparent;
}
.main-menu-item__export {
margin-left: auto;
}
.main-menu {
border-bottom: 1px solid #00832a;
//margin-bottom: 15px;
}
.table-item {
padding: 10px;
transition: all ease-out 0.3s;
max-height: 500px;
opacity: 1;
position: relative;
}
.table-item:hover {
background: #ffe476;
}
.table-item:hover .product-cmp {
background: #fff;
}
.table-item__product-components {
margin-left: -5px;
}
.table-item__hidden {
max-height: 0;
opacity: 0;
padding: 0;
}
.table-item__hidden-full {
display: none;
}
.table-item-action {
cursor: pointer
}
.tag {
color: #125ba1;
font-style: italic;
font-size: 14px;
margin-left: 8px;
}
.product-cmp {
border-radius: 8px;
background: #fff3ca;
padding: 1px 5px;
font-size: 12px;
}
.product-title {
font-size: 17px;
margin-right: 16px;
}
textarea.export-data {
width: calc(100% - 32px);
margin: 16px 0 5px 14px;
height: calc(100vh - 200px);
}
.export-sub-menu {
margin-right: 10%;
margin-bottom: 10px;
}
.export-sub-menu .main-menu-item, .product-filter__title, .product-filter{
color: #fff;
}
.main-menu-item {
cursor: pointer;
transition: all 0.5s;
}
.product-filter {
display: flex;
padding: 5px;
}
.product-filter__title {
font-size: 20px;
padding-top:4px;
}
.product-filter__input {
margin-left: 20px;
padding: 4px 10px;
font-size: 20px;
font-family: Verdana;
flex-grow: 1;
}
.product-filter__label {
margin-right: 16px;
}
.highlighted {
background: #00832a;
color: #fff;
}
.product-filter__hint {
margin-right: 16px;
}
input:focus, textarea:focus, select:focus {
outline-color: #00832a;
}
.product-filter__area, .title-gradient {
background: linear-gradient(181deg, #afca0b, #00832a);
padding: 12px 8px 16px;
}
.export-panel .title-gradient {
padding-bottom: 0;
}
.export-panel .main-menu-item.main-menu-item__active,
.generate-panel .main-menu-item.main-menu-item__active{
border-bottom: 5px solid #fff;
}
body {margin: 0; padding: 0}
.tag-manipulations {
padding: 8px;
}
.tag-manipulations__not-active {
opacity: 0.3;
pointer-events: none;
}
.cmp-tag-field {
display: inline-block;
width: 300px;
position: relative;
}
.cmp-tag-input {
width: 100%;
box-sizing: border-box;
}
.cmp-tag-dropdown {
position: absolute;
width: 100%;
background: #fff;
border: 1px solid #158b26;
box-sizing: border-box;
margin-top: -1px;
}
.cmp-tag-dropdown-item {
margin: 8px;
}
.cmp-tag-dropdown__hidden {
display: none;
}
.table-item-action {
display: none;
cursor: pointer;
color: #a00;
position: absolute;
top: -4px;
}
.table-item:hover .table-item-action {
display: inline-block;
}
.product-description {
font-size: 14px;
padding: 16px 0 8px;
max-width: 480px;
}
.quiz-answer-label {
display: block;
}
textarea.generate-debug {
width: calc(100% - 32px);
margin: 16px 0 5px 14px;
height: 300px;
}
\ No newline at end of file
data = null;
const init = function() {
let
tagField, exportEl;
div({
renderTo: document.body,
cls: 'content'
},
div({
cls: 'main-menu'
},
view.cmp.Menu({title: 'Продукты', id: 'products', key: 'mainMenuActive'}),
view.cmp.Menu({title: 'Ингредиенты', id: 'components', key: 'mainMenuActive'}),
view.cmp.Menu({title: 'Генерируем', id: 'generate', key: 'mainMenuActive'}),
view.cmp.Menu({title: 'Export', id: 'export', key: 'mainMenuActive', cls: 'menu-item-export'}),
),
view.cmp.Switch({cls: 'content-area', key: 'mainMenuActive'}, {
products: new view.page.Products(),
components: div({}, 'components'),
'export': new view.page.Export(),
generate: new view.page.Generate()
})
);
};
This diff is collapsed. Click to expand it.
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment