(function() {
'use strict';
function mergeUnique(a, b) {
return [...a,...b].reduce((acc, v) => {
if(!acc.includes(v))
acc.push(v);
return acc;
}, []);
}
function filterUnique(a) {
return a.filter((e, k, a) => a.indexOf(e) == k);
}
function hasIntersection(a, b) {
return a.some(v => b.includes(v));
}
class QuotaRule {
selectors = [];
constructor(raw = '*') {
raw = raw
.trim()
.replace(/[\s]{2,}/g, ' ')
.replace(/([\w\*])\s([\w\*])/g, '$1 -> $2')
.split(' ');
for(var i = 0; i <= raw.length; i += 2) {
var props = raw[i];
var combinator = raw[i + 1];
var selector = new QuotaSelector(props, combinator);
this.selectors.push(selector);
}
}
static rawFrom(elem) {
var parents = elem.getParents();
var baseRawSelector = QuotaSelector.baseRawFrom(elem);
return parents.reduce((acc, parent) => {
var selector = QuotaSelector.rawFrom(parent);
return `${selector} > ${acc}`;
}, baseRawSelector);
}
getSelectors() {
return [...this.selectors];
}
getBaseSelector() {
return this.selectors[this.selectors.length - 1];
}
toString() {
return this.selectors.map(s => String(s)).join(' ');
}
}
class QuotaSelector {
prop;
values;
combinator;
constructor(raw, combinator = null) {
var prop = '(?<prop>[\\w|\\,]+|\\*)';
var vals = '(?<vals>[\\w|,|-]+|\\*)?';
var operator = '(?<operator>>=|<=|>|<|=|!=)?';
var re = new RegExp(`^${prop}${operator}${vals}$`);
var match = raw.match(re);
if(!match?.groups)
throw new Error('Cant parse selector rule');
prop = match.groups.prop;
vals = match.groups.vals;
operator = match.groups.operator;
this.prop = new QuotaProp(prop);
this.values = new QuotaValues(operator, vals);
this.combinator = new QuotaCombinator(combinator);
}
static baseRawFrom(elem) {
var prop = QuotaProp.rawFrom(elem);
return `${prop}=*`;
}
static rawFrom(elem, combinator = '>') {
var prop = QuotaProp.rawFrom(elem);
var values = QuotaValues.rawFrom(elem);
return `${prop}${values}`;
}
isPasses(values, prop) {
return this.values.isPasses(values) && this.prop.isPasses(prop);
}
getCombinator() {
return this.combinator;
}
getProp() {
return this.prop;
}
getValues() {
return this.values;
}
toString() {
var out = `${String(this.prop)}${String(this.values)}`;
if(this.combinator.isEmpty())
return out;
return `${out} ${String(this.combinator)}`;
}
}
class QuotaProp {
prop;
constructor(prop) {
this.prop = prop.split(',');
}
static rawFrom(elem) {
return String(elem.getProp());
}
isPasses(prop) {
prop = prop.getProp();
return this.prop == '*' || prop == '*' || hasIntersection(this.prop, prop);
}
isSame(prop) {
var aProp = this.getProp().sort();
var bProp = prop.getProp().sort();
return String(aProp) == String(bProp);
}
getProp() {
return [...this.prop];
}
add(prop) {
this.prop = mergeUnique(this.prop, prop.getProp());
this.prop.sort();
}
isXMin() {
return this.prop == 'min';
}
toString() {
return String(this.prop);
}
}
class QuotaOperator {
operator;
constructor(raw = '=') {
if(raw == '')
raw = '=';
this.operator = raw;
}
static rawFrom(elem) {
var values = elem.getValues();
return String(values.getOperator());
}
setRaw(raw) {
this.operator = raw;
}
isEqual() {
return this.operator === '=';
}
isNotEqual() {
return this.operator === '!=';
}
isMore() {
return this.operator === '>';
}
isLess() {
return this.operator === '<';
}
isSame(operator) {
return this.getOperator() === operator.getOperator();
}
getOperator() {
return this.operator;
}
toString() {
return this.operator;
}
}
class QuotaVals {
vals;
constructor(raw = '*') {
raw = raw.split(',');
this.vals = raw.reduce((acc, val) => {
return mergeUnique(acc, this.parse(val));
}, []);
}
static rawFrom(elem) {
var values = elem.getValues();
return String(values.getVals());
}
parse(raw = '*') {
if(typeof raw == 'number')
return [raw];
if(!isNaN(raw))
return [Number(raw)];
var range = [...raw.matchAll(/[^\d]*(\d+)\-(\d+)[^\d]*/ig)];
if(!range.length)
return [raw];
var range = range[0];
var start = Number(range[1]);
var end = Number(range[2]);
var length = end - start + 1;
return [...Array(length)].map((val, key) => start + key);
}
getVals() {
return [...this.vals];
}
hasIntersection(vals) {
var aVals = vals.getVals();
var bVals = this.getVals();
return hasIntersection(aVals, bVals);
}
isConsistency() {
var sorted = this.getVals().sort();
if(sorted.length == 1 || sorted.length == 2)
return false;
return sorted.every((v, k, arr) => {
if(k == 0)
return true;
return v - arr[k-1] == 1;
});
}
isPasses(vals) {
vals = vals.getVals();
return this.vals == '*' || vals == '*' || hasIntersection(this.vals, vals);
}
isSame(vals) {
var aVals = this.getVals().sort();
var bVals = vals.getVals().sort();
return String(aVals) == String(bVals);
}
isSingle() {
return this.vals.length == 1;
}
isAny() {
return this.vals == '*';
}
add(vals) {
this.vals = mergeUnique(this.vals, vals.getVals());
this.vals.sort();
}
setRaw(raw) {
this.vals = this.parse(raw);
}
toFirst() {
this.vals = [this.vals[0]];
}
toLast() {
this.vals = [this.vals[this.vals.length - 1]];
}
isOnlyInteger() {
return this.getVals().every(val => Number.isInteger(val));
}
decrease() {
--this.vals[0];
}
increase() {
++this.vals[0];
}
getStart() {
return this.getVals().sort((v1, v2) => v1 - v2)[0];
}
getEnd() {
return this.getVals().sort((v1, v2) => v1 - v2)[this.vals.length - 1];
}
getPoint() {
return this.getStart();
}
getPoints() {
return [this.getStart(), this.getEnd()];
}
toString() {
if(this.isConsistency())
return `${this.vals[0]}-${this.vals[this.vals.length-1]}`;
return String(this.vals);
}
}
class QuotaValues {
vals;
operator;
constructor(operator = '=', vals = '*') {
this.vals = new QuotaVals(vals);
this.operator = new QuotaOperator(operator);
if(this.operator.isEqual() ||
this.operator.isNotEqual() ||
this.operator.isLess() ||
this.operator.isMore())
return;
if(!this.vals.isOnlyInteger())
return;
var raw = this.operator.getOperator();
if(raw != '>=' && raw != '<=')
throw new Error('Unknown operator val');
if(!this.vals.isSingle()) {
if(raw === '>=')
this.vals.toFirst();
else
this.vals.toLast();
}
if(raw == '>=') {
this.vals.decrease();
this.operator.setRaw('>');
}
else {
this.vals.increase();
this.operator.setRaw('<');
}
}
static rawFrom(elem) {
return `${QuotaOperator.rawFrom(elem)}${QuotaVals.rawFrom(elem)}`;
}
isPasses(values) {
var aVals = values.getVals();
var bVals = this.getVals();
var aOper = values.getOperator();
var bOper = this.getOperator();
if(aVals.isAny() || bVals.isAny())
return true;
if(aOper.isEqual() && bOper.isEqual())
return aVals.hasIntersection(bVals);
if(aOper.isNotEqual() && bOper.isNotEqual())
return aVals.isSame(bVals);
if(aOper.isNotEqual() || bOper.isNotEqual())
throw new Error('Не умею находить пересечение "!=" с другими операторами');
if(aOper.isMore() && bOper.isMore() ||
aOper.isLess() && bOper.isLess())
return true;
if(aOper.isMore() && bOper.isLess() ||
aOper.isLess() && bOper.isMore()) {
var aData = [aOper, aVals];
var bData = [bOper, bVals];
var moreData = [aData, bData].find(d => d[0].isMore());
var lessData = [aData, bData].find(d => d[0].isLess());
var morePoint = moreData[1].getPoint();
var lessPoint = lessData[1].getPoint();
return morePoint < lessPoint;
}
if(aOper.isEqual() || bOper.isEqual())
return this.getHoleLength(values) <= 0;
throw new Error('Unknown operator val');
}
getVals() {
return this.vals;
}
getOperator() {
return this.operator;
}
isSame(values) {
var aVals = values.getVals();
var bVals = this.getVals();
var aOper = values.getOperator();
var bOper = this.getOperator();
return aOper.isSame(bOper) && aVals.isSame(bVals);
}
getHoleLength(values) {
var aVals = values.getVals();
var bVals = this.getVals();
var aOper = values.getOperator();
var bOper = this.getOperator();
var aData = [aOper, aVals];
var bData = [bOper, bVals];
var rangeData = [aData, bData].find(d => d[0].isEqual());
var directionData = [aData, bData].find(d => !d[0].isEqual());
var rangeVals = rangeData[1];
var directionVals = directionData[1];
var directionOperator = directionData[0];
var rangeStart = rangeVals.getStart();
var rangeEnd = rangeVals.getEnd();
var directionPoint = directionVals.getPoint();
if(directionOperator.isMore())
return (directionPoint + 1) - rangeEnd;
if(directionOperator.isLess())
return rangeStart - (directionPoint - 1);
}
add(values) {
var aVals = values.getVals();
var aOper = values.getOperator();
var bVals = this.getVals();
var bOper = this.getOperator();
if(aOper.isEqual() && bOper.isEqual())
return this.vals.add(values.getVals());
if(aOper.isNotEqual() && bOper.isNotEqual())
return;
if(aOper.isNotEqual() || bOper.isNotEqual())
throw new Error('Не умею сливать "!=" с другими операторами');
if(aOper.isMore() && bOper.isMore()) {
var aPoint = aVals.getPoint();
var bPoint = bVals.getPoint();
var less = aPoint < bPoint ? aPoint : bPoint;
this.vals.setRaw(less);
this.operator.setRaw('>');
return;
}
if(aOper.isLess() && bOper.isLess()) {
var aPoint = aVals.getPoint();
var bPoint = bVals.getPoint();
var more = aPoint > bPoint ? aPoint : bPoint;
this.vals.setRaw(more);
this.operator.setRaw('<');
return;
}
if(aOper.isEqual() || bOper.isEqual()) {
var range = [this, values].find(v => v.getOperator().isEqual());
var direction = [this, values].find(v => !v.getOperator().isEqual());
var dVals = direction.getVals();
var rVals = range.getVals();
var dPoint = dVals.getPoint();
if(aOper.isLess() || bOper.isLess()) {
var rEnd = rVals.getEnd() + 1;
var resultPoint = dPoint > rEnd ? dPoint : rEnd;
this.vals.setRaw(resultPoint);
this.operator.setRaw('<');
return;
}
if(aOper.isMore() || bOper.isMore()) {
var rStart = rVals.getStart() - 1;
var resultPoint = dPoint < rStart ? dPoint : rStart;
this.vals.setRaw(resultPoint);
this.operator.setRaw('>');
return;
}
}
throw new Error('Unknow add values type');
}
toString() {
return `${String(this.operator)}${String(this.vals)}`;
}
}
class QuotaCombinator {
combinator;
constructor(combinator = null) {
this.combinator = combinator;
}
isDirectParent() {
return this.combinator == '>';
}
isAnyParent() {
return this.combinator == '->';
}
isSiblings() {
return this.combinator == '~';
}
isEmpty() {
return this.combinator === null;
}
toString() {
return this.combinator;
}
}
class QuotaLimit {
limit;
constructor(raw) {
this.limit = raw === '' ? '' : Number(raw);
}
add(limit) {
if(this.isUnlimit() || limit.isUnlimit())
return this.limit = '';
this.limit += limit.getLimit();
}
getLimit() {
return this.limit;
}
isUnlimit() {
return this.limit === '';
}
}
class QuotaNode {
$;
level;
parent;
children;
constructor($elem, parent = null) {
this.$ = $elem;
this.parent = parent;
this.level = this.parent ? this.parent.getLevel() + 1 : 0;
var children = this.$
.find('.q-inside').first()
.find('> .q-block');
this.children = [...children].map(e => new QuotaNode($(e), this));
this.prop = new QuotaProp(this.$prop.text());
this.limit = new QuotaLimit(this.$limit.text());
this.values = new QuotaValues(this.$operator.text(), this.$vals.text());
}
remove() {
this.$.remove();
this.children = [];
if(this.parent)
this.parent.removeChild(this);
}
removeChild(child) {
this.children = this.children.filter(c => c != child);
}
get $prop() {
return this.$.find('.q-properties').first().find('div');
}
get $limit() {
return this.$.find('.q-limit').first().find('div');
}
get $vals() {
return this.$.find('.q-values').first().find('div');
}
get $operator() {
return this.$.find('.q-operator').first().find('div');
}
hasChildren() {
return this.children.length;
}
getAllChildren() {
return this.children.reduce((acc, child, k) => {
acc.push(child, ...child.getAllChildren());
return acc;
}, []);
}
getParents() {
if(this.hasParent())
return [this.parent, ...this.parent.getParents()];
return [];
}
getValues() {
return this.values;
}
getProp() {
return this.prop;
}
getLimit() {
return this.limit;
}
getParent() {
return this.parent;
}
getLevel() {
return this.level;
}
hasParent() {
return !!this.parent && !this.parent.isRoot();
}
getChildren() {
return [...this.children];
}
addLimit(limit) {
if(this.prop.isXMin())
return;
this.limit.add(limit);
this.$limit.text(this.limit.getLimit());
}
addProp(prop) {
this.prop.add(prop);
this.$prop.text(String(this.prop));
}
addValues(values) {
this.values.add(values);
var strVals = String(this.values.getVals());
var strOperator = String(this.values.getOperator());
this.$vals.text(strVals);
this.$operator.text(strOperator);
}
isRoot() {
return this.$.is('.midbox');
}
}
class QuotaMergeController {
constructor() {
this.root = null;
}
init() {
this.root = new QuotaNode($('.midbox'));
}
mergeByRule(rule) {
if(!this.root)
this.init();
var rule = new QuotaRule(rule);
var elems = this.root.getAllChildren();
var fitElems = QuotaSearchController.filter(elems, rule);
if(!fitElems)
return;
this._mergeSiblingsByLevels(fitElems);
this.root = null;
}
toggleSelectedView($view) {
$view.toggleClass('selected-for-merge');
if($view.is('.selected-for-merge'))
$view.css({fontWeight: 'bold'});
else
$view.css({fontWeight: 'inherit'});
}
mergeSiblingsToView($view) {
this.init();
var elem = QuotaSearchController.findByView(this.root, $view);
var rule = QuotaRule.rawFrom(elem);
this.mergeByRule(rule);
}
mergeSelected() {
this.init();
var selected = [];
$(".selected-for-merge").each(function() {
selected.push(QuotaSearchController.findByView(quotaMerge.root, $(this)));
});
this._mergeSiblingsByLevels(selected);
this._clearSelected();
}
clickHandler(event) {
event.stopPropagation();
if(!event.altKey)
quotaMerge._clearSelected();
if(event.altKey)
quotaMerge.toggleSelectedView($(this));
else if(event.ctrlKey)
quotaMerge.mergeSiblingsToView($(this));
}
keyHandler(event) {
if(event.code == "KeyQ" && event.altKey)
quotaMerge.mergeSelected();
}
_clearSelected() {
$('.selected-for-merge').each(function() {
$(this).removeClass('selected-for-merge');
$(this).css({fontWeight: 'inherit'});
})
}
_mergeSiblingsByLevels(elems) {
var levels = elems.reduce((acc, elem) => {
var level = elem.getLevel();
var oldElems = acc[level];
if(!oldElems) {
acc[level] = [elem];
return acc;
}
acc[level] = [...oldElems, elem];
return acc;
}, []).reverse().filter(l => l);
levels.forEach(this._mergeSiblings.bind(this));
}
_mergeSiblings(elems) {
var parents = new Map();
elems.forEach(elem => {
var parent = elem.getParent();
var oldElems = parents.get(parent);
if(!oldElems)
return parents.set(parent, [elem]);
parents.set(parent, [...oldElems, elem]);
});
parents.forEach((mergeChildren, parent) => {
if(mergeChildren.length == 1)
return;
var container = mergeChildren[0];
mergeChildren.forEach(child => {
if(child == container)
return;
this._mergeElems(child, container);
});
});
}
_canMergeChildren(a, b) {
var aChildren = a.getChildren();
var bChildren = b.getChildren();
if(aChildren.length != bChildren.length)
return;
return aChildren.every((aChild, k) => {
var bChild = bChildren[k];
if(!this._canStrictMerge(aChild, bChild))
return;
return this._canMergeChildren(aChild, bChild);
});
}
_mergeElems(from, to) {
if(!this._canMergeChildren(from, to))
return;
if(!this._canMergeVals(from, to))
return;
var fromChildren = from.getChildren();
var toChildren = to.getChildren();
fromChildren.forEach((fromChild, k) => {
var toChild = toChildren[k];
this._mergeElems(fromChild, toChild);
});
to.addLimit(from.getLimit());
to.addProp(from.getProp());
to.addValues(from.getValues());
from.remove();
}
_canStrictMerge(a, b) {
var aProp = a.getProp();
var bProp = b.getProp();
var aValues = a.getValues();
var bValues = b.getValues();
return aProp.isSame(bProp) && aValues.isSame(bValues);
}
_canMergeVals(a, b) {
var aValues = a.getValues();
var bValues = b.getValues();
var aVals = aValues.getVals();
var bVals = bValues.getVals();
var aOper = aValues.getOperator();
var bOper = bValues.getOperator();
if(aOper.isEqual() && bOper.isEqual())
return true;
if(aOper.isNotEqual() && bOper.isNotEqual())
return aVals.isSame(bVals);
if(aOper.isNotEqual() || bOper.isNotEqual())
throw new Error('Не умею сливать в одну ячейку "!=" с другими операторами');
if(aOper.isMore() && bOper.isMore() ||
aOper.isLess() && bOper.isLess())
return true;
if(aOper.isMore() && bOper.isLess() ||
bOper.isMore() && aOper.isLess())
return false;
if(aOper.isEqual() || bOper.isEqual())
return aValues.getHoleLength(bValues) <= 1;
throw new Error('Unknown operator val');
}
}
class QuotaSearchController {
static filter(elems, rule) {
var nodes = elems.map(elem => new QuotaSearchNode(elem));
var selectors = rule.getSelectors().reverse();
selectors.forEach(selector => {
nodes = nodes.reduce((acc, node) => {
acc.push(...node.findElemsBySelector(selector));
return acc;
}, []);
});
var bases = nodes.map(node => node.getBase());
return filterUnique(bases);
}
static findByView(elem, $view) {
if(elem.$[0] == $view[0])
return elem;
var children = elem.getChildren();
return children.reduce((acc, child) => {
var result = QuotaSearchController.findByView(child, $view);
if(result)
acc = result;
return acc;
}, null);
}
}
class QuotaSearchNode {
elem;
prev;
constructor(elem, prev = null) {
this.elem = elem;
this.prev = prev;
}
getBase() {
var current = this;
while(current.prev)
current = current.prev;
return current.elem;
}
findElemsBySelector(selector) {
var out = [];
var combinator = selector.getCombinator();
if(combinator.isDirectParent())
out = this.findDirectParents(selector);
else if(combinator.isAnyParent())
out = this.findAnyParents(selector);
else if(combinator.isEmpty())
out = this.checkSelf(selector);
else if(combinator.isSiblings())
out = this.findSiblings(selector);
else
throw new Error('Unknown combinator');
return out.map(elem => new QuotaSearchNode(elem, this));
}
findAnyParents(selector) {
var out = [];
var parent, prop, values;
var elem = this.elem;
while(elem.hasParent()) {
parent = elem.getParent();
prop = parent.getProp();
values = parent.getValues();
if(selector.isPasses(values, prop))
out.push(parent);
elem = elem.getParent();
}
return out;
}
checkSelf(selector) {
var elem = this.elem;
var prop = elem.getProp();
var values = elem.getValues();
return selector.isPasses(values, prop) ? [elem] : [];
}
findDirectParents(selector) {
var elem = this.elem;
var parent = elem.getParent();
if(!parent)
return [];
var values = parent.getValues();
var prop = parent.getProp();
return selector.isPasses(values, prop) ? [parent] : [];
}
findSiblings(selector) {
var elem = this.elem;
var parent = elem.getParent();
if(!parent)
return [];
var children = parent.getChildren();
return children.reduce((acc, child) => {
var prop = child.getProp();
var values = child.getValues();
if(selector.isPasses(values, prop))
acc.push(child);
return acc;
}, []);
}
}
window.quotaMerge = new QuotaMergeController();
$(document).on('keyup', quotaMerge.keyHandler);
$(document).on('click', '.q-block', quotaMerge.clickHandler);
})();