// テーブル・リレーションを格納するコンテナクラス // class tvFigures {
constructor(){ this.tables = {}; this.relations = {}; } addTable(name, tableObject){ this.tables[name] = tableObject; } addRelation(name, relationObject){ this.relations[name] = relationObject; } getTable(name){ return this.tables[name]; } static generateRelationName(parentTable, childTable){ return "relation-" + parentTable + "-" + childTable; } getRelation(name){ return this.relations[name]; } getRelationsAs(as, tableName){ return Object.values(this.relations).filter((relObj)=>{ return relObj[as].name == tableName }); } getRelationsAsChild(childTableName){ return this.getRelationsAs("child", childTableName); } getRelationsAsParent(parentTableName){ return this.getRelationsAs("parent", parent); } overlayExceptOptions(relation){ return { exceptTable: [relation.parent.name, relation.child.name], exceptRelation: [relation.name] }; } getOverlayedRelations(){ return Object.values(this.relations).filter((relation)=>{ return this.isOverlayAboutLines(relation.getFigure().getLines(), this.overlayExceptOptions(relation)); }) } isNotOverlay(relationFigure, options){ let overlay = true; while(overlay){ overlay = this.isOverlayAboutLines(relationFigure.getLines(), options); if(!overlay) break; if(!relationFigure.nextOffset()) break; } if(!overlay){ relationFigure.fixOffset(); } return !overlay; } isOverlayAboutLines(lines, options){ const MARGIN = 5; // ギリギリずれている場合に、重なっていると判定するための余白 return Object.keys(this.tables) .some((tblName)=>{ let { top, bottom, left, right } = this.tables[tblName].position; [ top, bottom, left, right ] = [ top + 1, bottom - 1, left + 1, right - 1 ]; return lines.some((line)=>{ return line.isOverlay(top, bottom, left, right) }) }) || Object.keys(this.tables).filter((tblName => !options.exceptTable.includes(tblName))) .some((tblName)=>{ let { top, bottom, left, right } = this.tables[tblName].position; [ top, bottom, left, right ] = [ top - MARGIN, bottom + MARGIN, left - MARGIN, right + MARGIN ]; return lines.some((line)=>{ return line.isOverlay(top, bottom, left, right) }) }) || Object.keys(this.relations).filter(relName => !options.exceptRelation.includes(relName)) .some((relName)=>{ let relationLines = this.relations[relName].getFigure().getLines(); return lines.some((line)=>{ return relationLines.some((relationLine)=>{ if(line.isVertical == relationLine.isVertical){ // 平行な線 return line.fixedPosition == relationLine.fixedPosition && line.start - MARGIN < relationLine.end && relationLine.start < line.end + MARGIN; }else{ // 交差する線 return relationLine.start < line.fixedPosition && line.fixedPosition < relationLine.end && line.start - MARGIN < relationLine.fixedPosition && relationLine.fixedPosition < line.end + MARGIN; } }) }) }) }
}
var figures = new tvFigures();
// 線 を表すクラス // class tvLine {
constructor(direction, fixedPosition){ this.isVertical = (direction == "vertical"); this.fixedPosition = fixedPosition; } setStartEnd(start, end){ this.start = parseFloat(start < end ? start : end); this.end = parseFloat(start < end ? end : start); return this; } getPointOf(startOrEnd){ if(this.isVertical){ return { left: this.fixedPosition, top: this[startOrEnd] }; }else{ return { left: this[startOrEnd], top: this.fixedPosition }; } } get startPoint(){ return this.getPointOf("start") } get endPoint(){ return this.getPointOf("end") } get centerPoint(){ let fixed = this.fixedPosition, center = (this.start + this.end)/2; return this.isVertical ? { left: fixed, top: center } : { left: center, top: fixed }; } length(){ return Math.abs(this.start - this.end) } pointsOnLine(){ if(this.points) return this.points; let [fixed, moving] = this.isVertical ? ["left", "top"]: ["top", "left"]; let center = (this.start + this.end) / 2; this.points = [ { [fixed]: this.fixedPosition, [moving]: center } ]; // 前後 20px ずつずらしていく。ただし両端 20px は空けておく for(let offset = 20; center - (offset + 20) > this.start; offset += 20){ this.points.push({ [fixed]: this.fixedPosition, [moving]: center - offset }); this.points.push({ [fixed]: this.fixedPosition, [moving]: center + offset }); } return this.points; } isOverlay(top, bottom, left, right){ let [fixedMin, fixedMax] = this.isVertical ? [left, right] : [top, bottom]; let [startEndMin, startEndMax] = this.isVertical ? [top, bottom] : [left, right]; if(this.fixedPosition < fixedMin || fixedMax < this.fixedPosition) return false; if(this.end < startEndMin || startEndMax < this.start) return false; return true; }
} class tvVerticalLine extends tvLine {
constructor(fixedPosition){ super("vertical", fixedPosition) }
} class tvHorizontalLine extends tvLine {
constructor(fixedPosition){ super("horizontal", fixedPosition) }
} // テーブルの枠線 を表すクラス(sideプロパティで四辺のいずれか(top,right,bottom,left)を識別する) // class tvTableLine extends tvLine {
constructor(tableName, side, fixedPosition){ super((side=="top"||side=="bottom")? "horizontal" : "vertical", fixedPosition) this.tableName = tableName; this.side = side; } searchBindPoint(anotherLine){ if(!this.points){ this.points = this.pointsOnLine(); this.boundPoints = new Set(); } this.temporaryBound = []; return this.nextBindPoint(anotherLine); } nextBindPoint(anotherLine){ let topOrLeft = this.isVertical ? "top" : "left"; let centerPosition = this.centerPoint[topOrLeft]; // anotherLine が指定されている場合は、検索方向を固定する(中央から端まで) let nextDirection; if(anotherLine){ nextDirection = { centerToEnd: centerPosition < anotherLine.centerPoint[topOrLeft] }; nextDirection.centerToStart = !nextDirection.centerToEnd; } for(let i=0; i < this.points.length; i++){ // 検索方向が指定されている場合 if(nextDirection){ if(nextDirection.centerToEnd && centerPosition > this.points[i][topOrLeft]) continue; if(nextDirection.centerToStart && centerPosition < this.points[i][topOrLeft]) continue; } if(this.boundPoints.has(i)) continue; if(this.temporaryBound.includes(i)) continue; this.temporaryBound.push(i); return this.points[i]; } // anotherLineの指定なしで呼び出す if(anotherLine){ return this.nextBindPoint(); } // それでも見つからない場合は undefined } // temporaryBound を確定する fixBindPoint(){ if(this.temporaryBound.length > 0){ this.boundPoints.add(this.temporaryBound[this.temporaryBound.length - 1]); } } // boundPoints を解除する unbind(point){ for(let i of this.boundPoints){ if(this.points[i].left == point.left && this.points[i].top == point.top){ this.boundPoints.delete(i); } } }
}
// テーブル を表すクラス // class tvTable {
constructor(name, left, top, width, height){ this.name = name; this.width = parseFloat(width); this.height = parseFloat(height); this.moveTo(parseFloat(left), parseFloat(top)); } moveTo(left, top){ this.left = parseFloat(left); this.top = parseFloat(top); this.center = { left: this.left + this.width/2, top: this.top + this.height/2 }; this.position = { left: this.left, right: this.left + this.width, top: this.top, bottom: this.top + this.height, }; this.sideLines = {}; // clear } getLineOf(side){ if(this.sideLines[side]) return this.sideLines[side]; this.sideLines[side] = new tvTableLine(this.name, side, this.position[side]) if(side=="top" || side=="bottom"){ this.sideLines[side].setStartEnd(this.position.left, this.position.right); }else{ this.sideLines[side].setStartEnd(this.position.top, this.position.bottom); } return this.sideLines[side]; } // "top", "bottom", "left", "right" のいずれかを返す getSideOf(line){ if(line.isVertical){ return line.fixedPosition == this.position.left ? "left" : "right" }else{ return line.fixedPosition == this.position.top ? "top" : "bottom" } } getVerticalLines(){ return [ this.getLineOf("left"), this.getLineOf("right") ]; } getHorizontalLines(){ return [ this.getLineOf("top"), this.getLineOf("bottom") ]; } // 指定されたテーブル側の辺(1つか2つ)を返す nearlySideLines(otherTable){ let otherPosition = otherTable.position; let lines = []; if(this.position.top > otherPosition.bottom) lines.push(this.getLineOf("top")); if(this.position.bottom < otherPosition.top) lines.push(this.getLineOf("bottom")); if(this.position.left > otherPosition.right) lines.push(this.getLineOf("left")); if(this.position.right < otherPosition.left) lines.push(this.getLineOf("right")); return lines; } // 指定されたテーブルの中央の距離のうち、近い方を返す。 // ただし、2つのテーブルの垂直位置が少しでも重なる場合は、水平距離は返さない(垂直距離を返す) // また、2つのテーブルの水平位置が少しでも重なる場合は、垂直距離は返さない(水平距離を返す) nearlyCenterPositionDistance(otherTable){ let distances = { left: Math.abs(this.center.left - otherTable.center.left), top: Math.abs(this.center.top - otherTable.center.top), }; if(this.position.top < otherTable.position.bottom && this.position.bottom > otherTable.position.top) return distances.top; if(this.position.left < otherTable.position.right && this.position.right > otherTable.position.left) return distances.left; else return Object.values(distances).sort((a, b)=> a - b)[0]; } static create(name, options){ let table = new tvTable(name, options.left, options.top, options.width, options.height); figures.addTable(name, table); return table; }
}
// リレーションを表すクラス // class tvRelation {
constructor(name, parentTbl, childTbl, childCardinality){ this.name = name; this.parent = parentTbl; this.child = childTbl; this.childCardinality = childCardinality; } unbind(){ this.parentSideLine && this.parentSideLine.unbind(this.parentPoint); this.childSideLine && this.childSideLine.unbind(this.childPoint); delete this.relationFigure; } calcLinePosition(){ this.unbind(); // 選択された辺により折れ線の種類・始点/終点を決定 let parentLines = this.parent.nearlySideLines(this.child); let childLines = this.child.nearlySideLines(this.parent); let TOO_NEAR_LIMIT = 30; if(parentLines.length == 2){ // 中央位置より優先する辺(方向:垂直/水平)を選択 let isPriorVertical = false; if(Math.abs(this.parent.center.left - this.child.center.left) > Math.abs(this.parent.center.top - this.child.center.top)){ isPriorVertical = true; } let paLine = parentLines.find(v => v.isVertical === isPriorVertical); let chLine = childLines.find(v => v.isVertical === isPriorVertical); /// 平行線が近すぎず、かつ点が見つかれば処理終了 if(Math.abs(paLine.fixedPosition - chLine.fixedPosition) > TOO_NEAR_LIMIT){ if(this.searchPoint(paLine, chLine)) return true; } // parentLines のもう片方を選択 let altPaLine = parentLines.find(v => v.isVertical !== isPriorVertical); let altChLine = childLines.find(v => v.isVertical !== isPriorVertical); /// 平行線が近すぎず、かつ点が見つかれば処理終了 if(altChLine && Math.abs(altPaLine.fixedPosition - altChLine.fixedPosition) > TOO_NEAR_LIMIT){ if(this.searchPoint(altPaLine, altChLine)) return true; } // それでも点が見つからない場合、交差する組み合わせを試す if(this.searchPoint(paLine, altChLine)) return true; if(this.searchPoint(altPaLine, chLine)) return true; }else if(parentLines.length == 1){ /// 一組しか線が選択されなかった場合、平行線が近すぎず、かつ点が見つかれば処理終了 if(Math.abs(parentLines[0].fixedPosition - childLines[0].fixedPosition) > TOO_NEAR_LIMIT){ if(this.searchPoint(parentLines[0], childLines[0])) return true; } }else{ // テーブル同士が重なっている場合, とりあえずtop同士でつなぐ return this.searchPoint(this.parent.getLineOf("top"), this.child.getLineOf("top"), true); } // 選択すべき点がない場合、平行でない辺を選択 let paLine = parentLines[0]; let chLine = childLines[0]; let otherDirection = paLine.isVertical ? "Horizontal": "Vertical"; // まず長さが短い方の辺を, 平行でない辺に変更 if(paLine.length() < chLine.length()){ paLine = this.selectOtherSideLineFor("parent", otherDirection); }else{ chLine = this.selectOtherSideLineFor("child", otherDirection); } // 点が見つかれば処理終了 if(paLine && chLine && this.searchPoint(paLine, chLine)) return true; // それでも見つからなければ長さが長い方の辺を変更(長い方の辺は取得できない場合がある) if(paLine.length() >= chLine.length()){ paLine = this.selectOtherSideLineFor("parent", otherDirection); }else{ chLine = this.selectOtherSideLineFor("child", otherDirection); } // 点が見つかれば処理終了 if(paLine && chLine && this.searchPoint(paLine, chLine)) return true; // それでも見つからない場合は 0 番目のLineで決定とする return this.searchPoint(parentLines[0], childLines[0], true); } // targetTable の direction (=Vertical or Horizontal) の辺を取得する selectOtherSideLineFor(targetTable, direction){ let nonTargetTable = targetTable == "parent" ? "child" : "parent"; let targetLines = this[targetTable][`get${direction}Lines`](); let nonTargetLines = this[nonTargetTable][`get${direction}Lines`](); if(targetLines[0].fixedPosition < nonTargetLines[1].fixedPosition && targetLines[1].fixedPosition > nonTargetLines[0].fixedPosition){ // 重なっている場合, nonTargetLines[0] と nonTargetLines[1] の間にある辺を返す return targetLines.find((line)=>{ return nonTargetLines[0].fixedPosition <= line.fixedPosition && line.fixedPosition <= nonTargetLines[1].fixedPosition; }); }else{ // 重なってない場合, nonTargetLine[0] に近い方の辺を返す return targetLines[targetLines[0].fixedPosition > nonTargetLines[0].fixedPosition ? 0 : 1]; } } // 線を引き、他の何かを重なってないか判定 // 重なっていれば、点を選択しなおす(選択すべき点がなければfalseを返す) searchPoint(paLine, chLine, noSearch = false){ // 選択された辺により折れ線の種類・始点/終点を決定 this.parentSideLine = paLine; this.childSideLine = chLine; this.parentPoint = paLine.searchBindPoint(chLine); this.childPoint = chLine.searchBindPoint(paLine); let found = noSearch; let relationFigure = noSearch && this.getFigure();
//DEBUG if(noSearch) console.log(“noSearch!!!!!!!!!!!! ” + this.name);
while(!found){ if(!this.parentPoint || !this.childPoint) break; if(found = figures.isNotOverlay(relationFigure = this.getFigure(), figures.overlayExceptOptions(this))) break; this.parentPoint = paLine.nextBindPoint(chLine); if(!this.parentPoint) break; if(found = figures.isNotOverlay(relationFigure = this.getFigure(), figures.overlayExceptOptions(this))) break; this.childPoint = chLine.nextBindPoint(paLine); } if(found){ this.parentSideLine.fixBindPoint(); this.childSideLine.fixBindPoint(); this.fixFigure(relationFigure.fixOffset()); } return found; } fixFigure(figure){ this.relationFigure = figure; } getFigure(){ if(this.relationFigure) return this.relationFigure; // Parent, Child が完全に重なっている場合など、this.parentPoint, this.childPointが undefined になっている。 // とりあえず中央点にする this.parentPoint = this.parentPoint || this.parentSideLine.centerPoint; this.childPoint = this.childPoint || this.childSideLine.centerPoint if(this.parentSideLine.isVertical == this.childSideLine.isVertical){ ///// └┐, ┌┘, │ のいずれか let prop = this.parentSideLine.isVertical ? "top" : "left"; if(this.parentPoint[prop] == this.childPoint[prop]){ return new tvRelationFigure1(this.parentPoint, this.childPoint) }else{ return new tvRelationFigure3(this.parentPoint, this.childPoint, ! this.parentSideLine.isVertical) } }else{ ///// └, ┘, ┌, ┐ のいずれか // 上にある方を posA、下にある方を posB とする let [posA, posB] = [this.parentPoint, this.childPoint] if(this.parentPoint.top > this.childPoint.top){ [posA, posB] = [posB, posA]; } let posASideLine = this.parentPoint.top < this.childPoint.top ? this.parentSideLine : this.childSideLine; if(posASideLine.isVertical){ // ┌, ┐ のいずれか return new tvRelationFigure2(posA, posB, { top: posA.top, left: posB.left }); }else{ // └, ┘ のいずれか return new tvRelationFigure2(posA, posB, { top: posB.top, left: posA.left }); } } } get parentEdge(){ if(!this.parentSideLine || !this.parentPoint) return; return { point: this.parentPoint, side: this.parent.getSideOf(this.parentSideLine) }; } get childEdge(){ if(!this.childSideLine || !this.childPoint) return; return { point: this.childPoint, side: this.child.getSideOf(this.childSideLine), cardinality: this.childCardinality }; } static create(name, parentTbl, childTbl, childCardinality){ let relation = new tvRelation(name, parentTbl, childTbl, childCardinality); figures.addRelation(name, relation); return relation; }
}
// 直線図形のリレーション class tvRelationFigure1 {
constructor(posA, posB){ if(posA.left == posB.left){ this.line = new tvLine("vertical", posA.left).setStartEnd(posA.top, posB.top) }else{ this.line = new tvLine("horizontal", posA.top).setStartEnd(posA.left, posB.left) } } getLines(){ return [ this.line ]; } nextOffset(){ return false } fixOffset(){ return this } displayInfo(){ return [ this.line.isVertical ? { top: this.line.start, left: this.line.fixedPosition, width: 10, height: this.line.length(), borders: ["left"] } : { top: this.line.fixedPosition, left: this.line.start, width: this.line.length(), height: 10, borders: ["top"] } ]; }
} // 折線(角1つ)図形のリレーション class tvRelationFigure2 {
constructor(posA, posB, corner){ this.lines = []; if(posA.top == corner.top){ this.lines.push(new tvHorizontalLine(posA.top).setStartEnd(posA.left, corner.left)) this.lines.push(new tvVerticalLine(posB.left).setStartEnd(posB.top, corner.top)) }else{ this.lines.push(new tvVerticalLine(posA.left).setStartEnd(posA.top, corner.top)) this.lines.push(new tvHorizontalLine(posB.top).setStartEnd(posB.left, corner.left)) } } getLines(){ return this.lines; } nextOffset(){ return false } fixOffset(){ return this } displayInfo(){ let [vline, hline] = this.lines[0].isVertical ? this.lines : this.lines.concat().reverse(); let borders = []; borders.push(vline.fixedPosition == hline.start ? "left" : "right") borders.push(hline.fixedPosition == vline.start ? "top" : "bottom") return [{ top: vline.start, left: hline.start, width: hline.length(), height: vline.length(), borders: borders }]; }
} // 折線(角2つ)図形のリレーション class tvRelationFigure3 {
constructor(posA, posB, isVertical){ this.isVertical = isVertical; this.offset = 0; if(isVertical){ // 上にある方を start とする [this.start, this.end] = posA.top < posB.top ? [posA, posB] : [posB, posA]; }else{ // 左にある方を start とする [this.start, this.end] = posA.left < posB.left ? [posA, posB] : [posB, posA]; } } get center(){ let prop = this.isVertical ? "top" : "left"; return parseInt((this.start[prop] + this.end[prop]) / 2) + this.offset; } getLines(){ if(this.lines) return this.lines; let lines = []; if(this.isVertical){ lines.push(new tvVerticalLine(this.start.left).setStartEnd(this.start.top, this.center)); lines.push(new tvHorizontalLine(this.center).setStartEnd(this.start.left, this.end.left)); lines.push(new tvVerticalLine(this.end.left).setStartEnd(this.center, this.end.top)); }else{ lines.push(new tvHorizontalLine(this.start.top).setStartEnd(this.start.left, this.center)); lines.push(new tvVerticalLine(this.center).setStartEnd(this.start.top, this.end.top)); lines.push(new tvHorizontalLine(this.end.top).setStartEnd(this.center, this.end.left)); } return lines; } // 折れ線の位置を 0, 10, -10, 20, -20,, とずらしていく // 終端まで到達した場合は、false を返す nextOffset(){ let newOffset = this.offset <= 0 ? Math.abs(this.offset) + 10 : this.offset * -1; let prop = this.isVertical ? "top" : "left"; let centerLinePosition = parseInt((this.start[prop] + this.end[prop]) / 2) + newOffset; if(this.start[prop] < centerLinePosition - 15 && centerLinePosition + 15 < this.end[prop]){ this.offset = newOffset; return true; } return false; } fixOffset(){ this.lines = this.getLines(); return this; } displayInfo(){ function toggle(dir){ return { left: "right", right: "left", top: "bottom", bottom: "top" }[dir] } if(this.isVertical){ // └┐, ┌┘ のどちらか let [vline, hline, vline2] = this.lines; let borders = [ vline.fixedPosition == hline.start ? "left" : "right", "bottom" ]; let base = { left: hline.start, width: hline.length() }; return [ Object.assign(Object.create(base), { top: vline.start, height: vline.length(), borders: borders }), Object.assign(Object.create(base), { top: vline2.start, height: vline2.length(), borders: [ toggle(borders[0]) ] }) ]; }else{ // ┌ ┐ // ┘, └ のいずれか let [hline, vline, hline2] = this.lines; let borders = [ hline.fixedPosition == vline.start ? "top" : "bottom", "right" ]; let base = { top: vline.start, height: vline.length() }; return [ Object.assign(Object.create(base), { left: hline.start, width: hline.length(), borders: borders }), Object.assign(Object.create(base), { left: hline2.start, width: hline2.length(), borders: [ toggle(borders[0]) ] }) ]; } }
} var Custom = {
apply: function(){ $('.table').bind('moved', Custom.saveTablePosition); $('.table .title').on('click.table_show', Custom.moveToShowPage); $('.editable input').bind('click', Custom.setEditMode); }, initLayout: function(){ $(".table").each(function(){ if($(this).hasClass("default-position")){ $(this).attr("data-pos-left", $(this).offset().left); $(this).attr("data-pos-top", $(this).offset().top); } $(this).css({ "width": $(this).width() + 20, "left": $(this).attr("data-pos-left") + "px", "top": $(this).attr("data-pos-top") + "px" }); tvTable.create($(this).attr("data-table-name"), { left: $(this).attr("data-pos-left"), top: $(this).attr("data-pos-top"), width: parseInt($(this).outerWidth(true)), height: parseInt($(this).outerHeight(true)) }); }); $(".table") .filter(function(){ return $(this).hasClass("default-position") }) .each(function(){ return $(this).removeClass("default-position") }) $(".table") .filter(function(){ return $(this).attr("data-relation-to") }) .sort((tblA, tblB)=>{ return $(tblB).attr("data-relation-to").split(",").length - $(tblA).attr("data-relation-to").split(",").length }) .each(function(){ Custom.private.refreshRelationLines($(this)) }); // 描画順によって重なってしまう場合があるため、重複している線について再描画を試みる figures.getOverlayedRelations().forEach((relation)=>{ Custom.private.drawRelationLines(relation.name, relation); }); $("a[data-link-url]").each(function(){ if($(this).attr("href")) return; $(this).attr("href", Custom.URLSuffix($(this).attr("data-link-url"))); }); }, saveTablePosition: function(e){ var position = $(this).offset().left+","+$(this).offset().top; $.ajax({ type: 'POST', url: "/tables/" + $(this).attr("data-table-name"), data: { layout: position } }) .done(function( data, textStatus, jqXHR ) { // message. }) }, moveToShowPage: function(e){ window.location.href = Custom.URLSuffix("tables/" + $(this).parent().attr("data-table-name")); }, URLSuffix: function(url){ var suffix = /.+\.html/.test(window.location.href)? ".html": ""; return url == "/" && suffix == ".html" ? "./..": url + suffix; }, setEditMode: function(){ if($(this).is(':checked')){ Custom.dragable(".table"); $(".table").addClass("editing"); $('.table .title').off('click.table_show'); }else{ Custom.unDragable(".table"); $(".table").removeClass("editing"); $('.table .title').on('click.table_show', Custom.moveToShowPage); } }, unDragable: function(cssSelector){ $(document).off('mousedown.draggable', cssSelector); $(document).off('mouseup.draggable'); $(document).off('mousemove.draggable'); }, dragable: function(cssSelector){ $(document).on('mousedown.draggable', cssSelector, function(e){ $(document).data("drag-target", this); $(this).data("dragStartX", e.pageX); $(this).data("dragStartY", e.pageY); $(this).data("startLeft", $(this).offset().left); $(this).data("startTop", $(this).offset().top); $(this).addClass("dragging"); }); $(document).on('mouseup.draggable', function(e){ var target = $(document).data("drag-target"); if(target){ Custom.private.moveTo($($(target).data("drag-target")), e); } $(document).data("drag-target", false); $(target).removeClass("dragging"); $(target).trigger("moved"); }); $(document).on('mousemove.draggable', function(e){ if($(document).data("drag-target")){ Custom.private.moveTo($($(document).data("drag-target")), e); } }); }, private: { moveTo: function($obj, e){ var startX = $obj.data("dragStartX"); var startY = $obj.data("dragStartY"); if(!startX || !startY) return; $obj.css({ "left": $obj.data("startLeft") + (e.pageX - startX), "top": $obj.data("startTop") + (e.pageY - startY) }); figures.getTable($obj.attr("data-table-name")).moveTo( $obj.data("startLeft") + (e.pageX - startX), $obj.data("startTop") + (e.pageY - startY) ) // 親テーブルとしての関連を再描画 Custom.private.refreshRelationLines($obj); // 子テーブルとしての関連を再描画 figures.getRelationsAsChild($obj.attr("data-table-name")).forEach((relObj)=>{ Custom.private.refreshRelationLines($(`.table[data-table-name=${relObj.parent.name}]`)); }) // 親テーブルとして関連しているテーブルを再描画 figures.getRelationsAsParent($obj.attr("data-table-name")).forEach((relObj)=>{ Custom.private.refreshRelationLines($(`.table[data-table-name=${relObj.child.name}]`)); }) }, refreshRelationLines: function($obj){ let tableObject = figures.getTable($obj.attr("data-table-name")); // 関連テーブルを取得 let relationTableObjects = $obj.attr("data-relation-to").split(",") .filter(v => v).map(rel => figures.getTable(rel)); let relationCardinalities = $obj.attr("data-relation-cardinality").split(",") .reduce((sum, cardinality)=>{ sum[cardinality.split(":")[0]] = cardinality.split(":")[1]; return sum; }, {}); // 関連テーブルを中心の絶対座長(X or Y)の差異が小さい順でソート relationTableObjects.sort((relTblA, relTblB)=>{ return relTblA.nearlyCenterPositionDistance(tableObject) - relTblB.nearlyCenterPositionDistance(tableObject) }) .forEach((relTable, index)=>{ var relName = tvFigures.generateRelationName(tableObject.name, relTable.name); if($(`#${relName}`).length == 0){ $("body").append("<div id=" + relName + " class='relation-line' ></div>") } let relation = figures.getRelation(relName) || tvRelation.create(relName, tableObject, relTable, relationCardinalities[relTable.name]); Custom.private.drawRelationLines(`#${relName}`, relation); }) }, drawRelationLines: function(relId, relation){ relation.calcLinePosition(); let displayInfo = relation.getFigure().displayInfo(); // └┐, ┌┘ のように2つの角をもつ線の場合 if(displayInfo.length == 2){ if($(relId).children().length == 0){ $(relId).append("<div class=\"child-1\"></div><div class=\"child-2\"></div>"); } $(relId).css({ top: displayInfo[0].top, left: displayInfo[0].left, border: "none" }); $(relId).find(".child-1").css({ top: 0, left: 0, width: displayInfo[0].width, height: displayInfo[0].height }); ["top", "bottom", "left", "right"].forEach((border)=>{ $(relId).find(".child-1").css(`border-${border}`, displayInfo[0].borders.includes(border) ? "solid 1px black" : "none"); }); $(relId).find(".child-2").css({ width: displayInfo[1].width, height: displayInfo[1].height }); $(relId).find(".child-2").css({ left: displayInfo[1].left - displayInfo[0].left, top: displayInfo[1].top - displayInfo[0].top }); ["top", "bottom", "left", "right"].forEach((border)=>{ $(relId).find(".child-2").css(`border-${border}`, displayInfo[1].borders.includes(border) ? "solid 1px black" : "none"); }); }else{ // ┌, ┘, |, のように1 or 0個の角をもつ線の場合 if($(relId).children().length > 0){ $(relId).empty(); } $(relId).css(displayInfo[0]); ["top", "bottom", "left", "right"].forEach((border)=>{ $(relId).css(`border-${border}`, displayInfo[0].borders.includes(border) ? "solid 1px black" : "none"); }) } // 親側の記号を描画 Custom.private.drawRelationEdge_1(`${relId}-parent-edge`, relation.parentEdge); // 子側の記号を描画 if(relation.childCardinality == "1"){ Custom.private.drawRelationEdge_1(`${relId}-child-edge`, relation.childEdge); }else{ Custom.private.drawRelationEdge_N(`${relId}-child-edge`, relation.childEdge); } }, drawRelationEdge_1: function(edgeId, edge){ let { top: offsetTop, left: offsetLeft } = { top: { top: -5, left: -5 }, bottom: { top: 0, left: -5 }, left: { top: -5, left: -5 }, right: { top: -5, left: 0 } }[edge.side]; if($(edgeId).length == 0){ $("body").append("<div id='" + edgeId.substring(1) + "' class='relation-edge' ></div>"); } $(edgeId).css({ top: edge.point.top + offsetTop, left: edge.point.left + offsetLeft }); $(edgeId).css( ["left", "right"].includes(edge.side) ? { width: 3, height: 10 } : { width: 10, height: 3 }); ["top", "bottom", "left", "right"].forEach((border)=>{ $(edgeId).css(`border-${border}`, edge.side == border ? "solid 1px black" : "none"); }) }, drawRelationEdge_N: function(edgeId, edge){ let offsets = { top: [{ left: -6 }, { top: -9, left: 1 }, { left: 7 }], bottom: [{ left: -5, top: -1 }, { top: 7 }, { left: 5, top: -1 }], left: [{ top: -6 }, { left: -8, top: -1 }, { top: 4 }], right: [{ top: -6 }, { left: 8, top: -1 }, { top: 4 }], }[edge.side]; let points = offsets.map((offset)=>{ let point = Object.assign({}, edge.point); if("top" in offset) Object.assign(point, { top: offset.top + edge.point.top }); if("left" in offset) Object.assign(point, { left: offset.left + edge.point.left }); return point; }); if($(`${edgeId}-1`).length == 0){ $("body").append("<div id='" + edgeId.substring(1) + "-1' class='relation-edge-diagonal' ></div>") $("body").append("<div id='" + edgeId.substring(1) + "-2' class='relation-edge-diagonal' ></div>") } let drawDiagonalLine = (rectId, p1, p2)=>{ let pos = p1.left < p2.left? p1: p2; let width = Math.abs(p1.left - p2.left); let height = Math.abs(p1.top - p2.top); let isUpToDown = ((p1.top - p2.top) * (p1.left - p2.left) < 0); var deg = Math.atan(height / width) / (Math.PI / 180) * (isUpToDown ? -1: 1); $(rectId).css({ top: pos.top, left: pos.left, width: Math.sqrt((width * width) + (height * height)), height: 1, transform: "rotate(" + deg + "deg)", "transform-origin": "left top" }); }; drawDiagonalLine(`${edgeId}-1`, points[0], points[1]); drawDiagonalLine(`${edgeId}-2`, points[1], points[2]); }, }
}
$(document).ready(Custom.initLayout); $(document).ready(Custom.apply);