はじめに
#1での(下記)、環境作成と初期設定の続きから行う。(全3記事を予定)
プロジェクトの作成(#1の続きから)
初期設定した年月を受け取りcalenadarView関数でカレンダーを初期表示する。
main.js
window.onload = async () => {
// 略
document.getElementById('appmain').appendChild(app.canvas);
const fmtYYYYMM = calendar_month.innerText.replaceAll('-', '');
await calenadarView(fmtYYYYMM);
}
calenadarView関数で左サイドの日付を1日からその月の末日まで表示する。
task表示するメインのCanvas部では、
該当の年月に紐づいたLocal上のTaskのデータがあればその数だけTaskをcreateTask関数で表示する。
main.js
/**
* カレンダーを初期表示する関数
* @param {string} strYYYYMM - 表示する年月
*/
const calenadarView = async (strYYYYMM) => {
// 左サイドに日を記載
leftcanvas.style.height = gridHeight + 'px';
leftcanvas.height = gridHeight;
leftcanvas.width = cellWidth;
const leftctx = leftcanvas.getContext('2d');
leftctx.font = `20px monospace`;
leftctx.fillStyle = 'lightgray';
let y = 0;
for (let i = 1; i <= cellHeightCount; i++) {
y += cellHeight
const buffer = 10;
const day = format({ date: strYYYYMM.substring(0, 4) + '-' + strYYYYMM.substring(4, 6) + '-' + String(i).padStart(2, '0'), format: 'DDddd', tz: 'Asia/Tokyo', })
leftctx.fillText(day, buffer, y - buffer);
}
document.getElementById('timemaincanvas').style.height = gridHeight + 'px';
// re create
const ret = await db.query(`SELECT * from task_master WHERE yearmonth = $1;`, [Number(strYYYYMM)]);
for (const row of ret.rows) {
await createTask(row.taskid, Number(row.x), Number(row.y), Number(row.width), row.color, row.title);
}
}
UUID,表示位置x,y,表示幅,表示色,表示文字を受け取りカレンダー上にTaskを作成し表示する関数を作成する。
- correctPositionの関数を使い、表示位置x,y,表示幅がカレンダーアプリの罫線上で収まるように補正しておく。
- 表示文字はgetBoundsを使い、文字がObject外へ飛び出して見えないように調整。
- ObjectのHover時にtitleが表示されるようにイベントを付与。
- ユーザーがObjectをFocus選択できるようにpointerdownでFocusTask関数を使い、あたかもFocusしているように見せる。
- Resize時にResizeできるようにResizerをObjectの左右両端に配置。
- createdistinctTaskの関数を使い、Task同士が重なり合っていれば赤い警告を表示させる。
- PGliteでLocalのテーブル上に今回作成したTaskを登録しておく。
main.js
/**
* TaskObjectをCreateする関数
* @param {string} name - 一意の名前(UUID)
* @param {number} objx - 表示位置x
* @param {number} objy - 表示位置y
* @param {number} objw - 表示幅w
* @param {string} c - 表示色
* @param {string} title - 表示文字
*/
const createTask = async (name, objx, objy, objw, c, title) => {
const { x, y, w } = correctPosition(objx, objy, objw, objh);
// Obj本体
let obj = new PIXI.Graphics()
.rect(x, y, w, objh)
.fill(c);
obj.label = name + _Graphics;
obj.interactive = true;
obj.buttonMode = true;
// Obj Resize
let objL = new PIXI.Graphics()
.rect(x, y, 4, objh)
.fill(c);
objL.label = name + _ResizeL;
objL.interactive = true;
objL.buttonMode = true;
objL.cursor = 'col-resize';
let objR = new PIXI.Graphics()
.rect(x + w - 4, y, 4, objh)
.fill(c);
objR.label = name + _ResizeR;
objR.interactive = true;
objR.buttonMode = true;
objR.cursor = 'col-resize';
// TextView作成
const txt = title === '' ? defaulttitle : title;
let text = new PIXI.Text({
text: txt,
style: new PIXI.TextStyle({
fontFamily: 'monospace',
fontSize: objh,
fill: 0xffffff,
wordWrapWidth: w,
})
})
text.label = name + _Text;
text.x = x + 4;
text.y = y;
text.interactive = true;
text.buttonMode = true;
const bounds = text.getBounds();
if (w < bounds.width) {
let temptext = '';
for (let i = 0; i < w / (cellHeight / 4) - 2; i++) {
temptext += text.text[i];
}
text.text = temptext
}
// コンテナ作成
const container = new PIXI.Container();
container.zIndex = 1;
container.label = name;
container.my_x = x;
container.my_y = y;
container.my_width = w;
container.my_color = c;
container.my_title = txt;
container.normalobj = true;
container.addChild(obj);
container.addChild(objL);
container.addChild(objR);
container.addChild(text);
app.stage.addChild(container);
const fmtYYYYMM = calendar_month.innerText.replaceAll('-', '');
createdistinctTask(name); // Obj同士の重なりを確認
focusTask(name); // 初期Focus
obj.on('pointerover', (e) => { txthover(txt); });
text.on('pointerover', (e) => { txthover(txt); });
objL.on('pointerover', (e) => { txthover(txt); });
objR.on('pointerover', (e) => { txthover(txt); });
obj.on('pointerdown', (e) => {
draggingTask = container;
// rect_topleftの距離
const currentPos = e.data.global;
draggingTask.my_dragx = currentPos.x - x;
focusTask(name);
});
text.on('pointerdown', (e) => {
draggingTask = container;
// rect_topleftの距離
const currentPos = e.data.global;
draggingTask.my_dragx = currentPos.x - x;
focusTask(name);
});
objL.on('pointerdown', (e) => {
resizingTask = container;
resizingTask.my_lr = 'L';
focusTask(name);
});
objR.on('pointerdown', (e) => {
resizingTask = container;
resizingTask.my_lr = 'R';
focusTask(name);
});
}
main.js
/**
* TaskObjectの位置を補正する関数
* @param {number} x - 位置x
* @param {number} y - 位置y
* @param {number} w - width
* @param {number} h - height
* @returns {{x: number, y: number, w: number}} 補正された位置情報オブジェクト。
*/
const correctPosition = (x, y, w, h) => {
// check if the object is out of bounds (x,y)
let objX = x < 0 ? 0 : x;
let objY = y < 0 ? 0 : y + h > gridHeight ? gridHeight - h : y;
// check if the object is correcting (x,y)
objX = objX - (objX % (cellWidth / 12));
objY = objY - (objY % cellHeight) + padtoph;
// check if the object is out of bounds width
let objW = w <= cellWidth / 12 ? cellWidth / 12 : x + w > cellWidth * maxtime ? cellWidth * maxtime - x : w;
// check if the object is correcting width
objW = objW - (objW % (cellWidth / 12));
return { x: objX, y: objY, w: objW };
}
/**
* TaskObject内のTextをHover時にTitleを表示する関数
* @param {string} txt
*/
const txthover = (txt) => {
document.getElementById('appmain').title = txt;
setTimeout(() => {
document.getElementById('appmain').title = '';
}, 1000)
}
/**
* TaskObjectをDistinctを表示する関数
* @param {string} name - 一意の名前
*/
const createdistinctTask = (name) => {
const targetcontainer = app.stage.getChildByLabel(name);
const targetchild = targetcontainer.getChildByLabel(name + _Graphics);
app.stage.children.forEach(container => {
const lbl = container.label
if (lbl != name && container.normalobj) {
if (targetcontainer.my_y == container.my_y) {
const a1 = targetcontainer.my_x;
const a2 = targetcontainer.my_x + targetcontainer.my_width;
const b1 = container.my_x;
const b2 = container.my_x + container.my_width;
const overlap = Math.min(a2, b2) - Math.max(a1, b1)
if (overlap > 0) {
let objD = app.stage.getChildByLabel(name + lbl + _Distinct);
if (objD) {
objD.destroy({ children: true, texture: true, textureSource: true, context: true });
}
objD = new PIXI.Graphics()
.rect(Math.max(a1, b1), container.my_y - padtoph, overlap, cellHeight)
.fill(0xff0000);
objD.label = name + lbl + _Distinct;
objD.my_distinct_ids = [name, lbl];
objD.interactive = true;
objD.buttonMode = true;
app.stage.addChild(objD);
objD.on('pointerover', (e) => { txthover(name + '\r\n' + lbl); });
}
}
}
})
}
/**
* TaskObjectをDistinctを削除する関数
* @param {string} name - 一意の名前
*/
const deletedistinctTask = (name) => {
app.stage.children.forEach(objD => {
if (objD.my_distinct_ids) {
if (objD.my_distinct_ids.includes(name)) {
if (objD) {
objD.destroy({ children: true, texture: true, textureSource: true, context: true });
}
}
}
})
}
/**
* TaskObjectをFocusする関数
* @param {string} name - 一意の名前
*/
const focusTask = (name) => {
blurTask();
let targetcontainer = app.stage.getChildByLabel(name);
if (targetcontainer) {
// Obj Focus
let objF = new PIXI.Graphics()
.rect(targetcontainer.my_x, targetcontainer.my_y + objh, targetcontainer.my_width, 3)
.fill(0xffff00);
objF.my_id = name;
const container = new PIXI.Container();
container.zIndex = 2;
container.label = focusGraphics;
container.my_id = name;
container.addChild(objF);
app.stage.addChild(container);
targetcontainer.zIndex = 2;
app.stage.children.forEach(container => {
const lbl = container.label
if (lbl != name && lbl && container.normalobj) {
container.zIndex = 1;
}
})
// focus color&title
taskchange.disabled = false;
taskdelete.disabled = false;
taskchange.dataset.taskid = name;
taskdelete.dataset.taskid = name;
tasktitle.disabled = false;
taskcolor.disabled = false;
tasktitle.value = targetcontainer.my_title;
taskcolor.value = targetcontainer.my_color;
}
}
/**
* TaskObjectをBlurする関数
*/
const blurTask = () => {
let objF = app.stage.getChildByLabel(focusGraphics);
if (objF) {
objF.destroy({ children: true, texture: true, textureSource: true, context: true });
// blur color&title
taskchange.disabled = true;
taskdelete.disabled = true;
taskchange.dataset.taskid = '';
taskdelete.dataset.taskid = '';
tasktitle.disabled = true;
taskcolor.disabled = true;
tasktitle.value = '';
taskcolor.value = '#000000';
}
}
/**
* TaskObject内のTextをHover時にTitleを表示する関数
* @param {string} txt
*/
const txthover = (txt) => {
document.getElementById('appmain').title = txt;
setTimeout(() => {
document.getElementById('appmain').title = '';
}, 1000)
}
/**
* TaskObjectをDistinctを表示する関数
* @param {string} name - 一意の名前
*/
const createdistinctTask = (name) => {
const targetcontainer = app.stage.getChildByLabel(name);
const targetchild = targetcontainer.getChildByLabel(name + _Graphics);
app.stage.children.forEach(container => {
const lbl = container.label
if (lbl != name && container.normalobj) {
if (targetcontainer.my_y == container.my_y) {
const a1 = targetcontainer.my_x;
const a2 = targetcontainer.my_x + targetcontainer.my_width;
const b1 = container.my_x;
const b2 = container.my_x + container.my_width;
const overlap = Math.min(a2, b2) - Math.max(a1, b1)
if (overlap > 0) {
let objD = app.stage.getChildByLabel(name + lbl + _Distinct);
if (objD) {
objD.destroy({ children: true, texture: true, textureSource: true, context: true });
}
objD = new PIXI.Graphics()
.rect(Math.max(a1, b1), container.my_y - padtoph, overlap, cellHeight)
.fill(0xff0000);
objD.label = name + lbl + _Distinct;
objD.my_distinct_ids = [name, lbl];
objD.interactive = true;
objD.buttonMode = true;
app.stage.addChild(objD);
objD.on('pointerover', (e) => { txthover(name + '\r\n' + lbl); });
}
}
}
})
}
/**
* TaskObjectをDistinctを削除する関数
* @param {string} name - 一意の名前
*/
const deletedistinctTask = (name) => {
app.stage.children.forEach(objD => {
if (objD.my_distinct_ids) {
if (objD.my_distinct_ids.includes(name)) {
if (objD) {
objD.destroy({ children: true, texture: true, textureSource: true, context: true });
}
}
}
})
}
/**
* TaskObjectをFocusする関数
* @param {string} name - 一意の名前
*/
const focusTask = (name) => {
blurTask();
let targetcontainer = app.stage.getChildByLabel(name);
if (targetcontainer) {
// Obj Focus
let objF = new PIXI.Graphics()
.rect(targetcontainer.my_x, targetcontainer.my_y + objh, targetcontainer.my_width, 3)
.fill(0xffff00);
objF.my_id = name;
const container = new PIXI.Container();
container.zIndex = 2;
container.label = focusGraphics;
container.my_id = name;
container.addChild(objF);
app.stage.addChild(container);
targetcontainer.zIndex = 2;
app.stage.children.forEach(container => {
const lbl = container.label
if (lbl != name && lbl && container.normalobj) {
container.zIndex = 1;
}
})
// focus color&title
taskchange.disabled = false;
taskdelete.disabled = false;
taskchange.dataset.taskid = name;
taskdelete.dataset.taskid = name;
tasktitle.disabled = false;
taskcolor.disabled = false;
tasktitle.value = targetcontainer.my_title;
taskcolor.value = targetcontainer.my_color;
}
}
/**
* TaskObjectをBlurする関数
*/
const blurTask = () => {
let objF = app.stage.getChildByLabel(focusGraphics);
if (objF) {
objF.destroy({ children: true, texture: true, textureSource: true, context: true });
// blur color&title
taskchange.disabled = true;
taskdelete.disabled = true;
taskchange.dataset.taskid = '';
taskdelete.dataset.taskid = '';
tasktitle.disabled = true;
taskcolor.disabled = true;
tasktitle.value = '';
taskcolor.value = '#000000';
}
}
まとめ
今回は、メインのロジックとなるTaskCreateの関数やTask上のイベント付与まで行った。
次回のD&D周辺ロジックで完成です!ラストスパート頑張ります!
次回につづく。