@innovation-system/vuetify-week-scheduler-v3
Version:
Vuetify week scheduler for Vue 3
670 lines (669 loc) • 24.2 kB
JavaScript
import { ref as R, createBlock as T, openBlock as p, withCtx as m, createElementVNode as h, mergeProps as B, withModifiers as c, withDirectives as v, createVNode as u, createTextVNode as D, vShow as y, toDisplayString as b, resolveComponent as S, createElementBlock as g, createCommentVNode as H, Fragment as x, renderList as C, normalizeStyle as L } from "vue";
import { VBtn as k } from "vuetify/components/VBtn";
import { VHover as z } from "vuetify/components/VHover";
import { VIcon as E } from "vuetify/components/VIcon";
import { VRow as I, VCol as Y } from "vuetify/components/VGrid";
import { VList as _, VListItem as V } from "vuetify/components/VList";
import { VMenu as j } from "vuetify/components/VMenu";
import { VTextField as W } from "vuetify/components/VTextField";
const M = (t, e) => {
const i = t.__vccOpts || t;
for (const [n, o] of e)
i[n] = o;
return i;
}, U = {
name: "VuetifyWeekSchedulerPeriod",
props: {
period: {
type: Object,
default: () => ({})
},
settings: {
type: Object,
default: () => ({})
},
editable: { type: Boolean, default: !1 }
},
emits: ["edit", "delete", "clone", "period-drag", "period-resize"],
setup() {
return { root: R(null) };
},
computed: {
options() {
return this.period.options;
},
shortPeriod() {
return this.period.height <= 30;
}
},
methods: {
onPeriodResize(t, e) {
this.$emit("period-resize", {
$event: t,
isUp: e,
$el: this.root
});
}
}
}, O = { class: "vws-period-container" }, A = { class: "vws-period-time" }, N = {
class: "vws-period-buttons",
justify: "end"
};
function q(t, e, i, n, o, s) {
return p(), T(z, null, {
default: m(({ isHovering: l, props: r }) => [
h("div", B({
ref: "root",
class: "vws-period",
style: {
backgroundColor: s.options.backgroundColor,
outlineColor: s.options.borderColor,
textColor: s.options.textColor,
color: s.options.textColor,
top: i.period.top + "px",
height: i.period.height + "px"
}
}, r, {
onMousedown: e[10] || (e[10] = c((d) => t.$emit("period-drag", d), ["stop"])),
onTouchstart: e[11] || (e[11] = c((d) => t.$emit("period-drag", d), ["stop"])),
onContextmenu: e[12] || (e[12] = c((d) => t.$emit("edit", d), ["stop", "prevent"])),
onDblclick: e[13] || (e[13] = c((d) => t.$emit("edit", d), ["stop", "prevent"]))
}), [
h("div", O, [
v(u(E, {
size: "small",
class: "vws-handle",
onMousedown: e[0] || (e[0] = c((d) => s.onPeriodResize(d, !0), ["stop"])),
onTouchstart: e[1] || (e[1] = c((d) => s.onPeriodResize(d, !0), ["stop"]))
}, {
default: m(() => e[14] || (e[14] = [
D(" mdi-chevron-up ")
])),
_: 2
}, 1536), [
[y, i.editable && l]
]),
h("div", A, [
D(b(s.options.start) + " - " + b(s.options.end) + " ", 1),
v(h("span", { class: "text--caption ml-2" }, b(s.options.title), 513), [
[y, s.shortPeriod]
])
]),
v(h("div", { class: "vws-period-title text-truncate" }, b(s.options.title), 513), [
[y, !s.shortPeriod]
]),
v(h("div", N, [
u(k, {
class: "mx-1 mt-1",
icon: "mdi-close",
size: "x-small",
density: "compact",
variant: "text",
title: i.settings.periodRemoveButton,
onClick: e[2] || (e[2] = c((d) => t.$emit("delete"), ["stop"])),
onMousedown: e[3] || (e[3] = c(() => {
}, ["stop"])),
onTouchstart: e[4] || (e[4] = c(() => {
}, ["stop"]))
}, null, 8, ["title"]),
u(k, {
class: "mx-1 mt-1",
icon: "mdi-content-copy",
size: "x-small",
density: "compact",
variant: "text",
title: i.settings.periodDuplicateButton,
onClick: e[5] || (e[5] = c((d) => t.$emit("clone"), ["stop"])),
onMousedown: e[6] || (e[6] = c(() => {
}, ["stop"])),
onTouchstart: e[7] || (e[7] = c(() => {
}, ["stop"]))
}, null, 8, ["title"])
], 512), [
[y, i.editable && l]
]),
v(u(E, {
size: "small",
class: "vws-handle",
style: { bottom: "0" },
onMousedown: e[8] || (e[8] = c((d) => s.onPeriodResize(d, !1), ["stop"])),
onTouchstart: e[9] || (e[9] = c((d) => s.onPeriodResize(d, !1), ["stop"]))
}, {
default: m(() => e[15] || (e[15] = [
D(" mdi-chevron-down ")
])),
_: 2
}, 1536), [
[y, i.editable && l]
])
])
], 16)
]),
_: 1
});
}
const X = /* @__PURE__ */ M(U, [["render", q]]), G = {
name: "VuetifyWeekScheduler",
// vue component name
components: {
SchedulerPeriod: X
},
props: {
config: { type: Object, default: () => {
} },
modelValue: { type: Array, required: !0 },
editable: { type: Boolean, default: !1 }
},
emits: ["update:modelValue", "error", "edit"],
data() {
return {
settings: {},
counter: 0,
blocksEachHour: 0,
maxBlocks: 0,
blockHeight: 0,
events: [],
// keep track of listeners
newPeriod: null,
draggingPeriod: null,
resizingPeriod: null,
showEditMenu: !1,
x: 0,
y: 0,
editEvent: null
};
},
computed: {
data: {
get() {
let t = this.modelValue;
if (this.modelValue.length !== this.settings.days) {
t = [];
for (let e = 0; e < this.settings.days; e++)
t.push({
day: e,
periods: []
});
this.$emit("update:modelValue", t);
}
return t;
},
set(t) {
this.$emit("update:modelValue", t);
}
},
daysPeriods() {
var e;
const t = [];
for (let i = 0; i < this.settings.days; i++) {
const n = {
day: i,
periods: []
};
t.push(n);
const o = (e = this.data) == null ? void 0 : e[i];
if (o)
for (let s = 0; s < o.periods.length; s++) {
const l = o.periods[s], r = this.positionFormat(l.start), d = this.positionFormat(l.end);
n.periods.push({
options: this.addDefaults(l),
index: s,
top: r * this.blockHeight,
height: (d - r) * this.blockHeight
});
}
}
return t;
},
isMobile() {
return this.$vuetify.display.mdAndDown;
}
},
mounted() {
this.settings = { ...this.getDefaults(), ...this.config }, this.init(), this.handleEvents();
},
beforeUnmount() {
this.events.forEach((t) => {
t.element.removeEventListener(t.event, t.callback);
});
},
methods: {
getDefaults() {
return {
hour: 24,
// 12
days: 7,
// 7/5
periodDuration: 15,
// 15/30/60
periodTitle: "",
periodBackgroundColor: "#F44336FF",
periodBorderColor: "transparent",
periodTextColor: "#000",
periodRemoveButton: "Remove",
periodDuplicateButton: "Duplicate",
periodSelectWholeButton: "Select whole day",
inputType: "text",
daysList: ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"],
colors: ["#F44336", "#FF9800", "#FFEB3B", "#8BC34A", "#4CAF50", "#00BCD4", "#2196F3"]
};
},
/** When clicking on a day */
onDayDown(t, e) {
if (!this.editable) return;
const i = e.currentTarget.getBoundingClientRect(), n = this.getY(e, !0) - i.top, o = Math.floor(n / this.blockHeight), s = this.blocksToTime(o), l = this.blocksToTime(o + 2), r = 2 * this.blockHeight;
this.newPeriod = {
dragStart: this.getY(e, !0),
startHeight: r,
maxHeight: e.currentTarget.clientHeight,
day: t,
start: s,
end: l,
top: o * this.blockHeight,
height: r
};
},
/** Add defaults to period */
addDefaults(t) {
return {
title: this.settings.periodTitle,
backgroundColor: this.settings.periodBackgroundColor,
borderColor: this.settings.periodBorderColor,
textColor: this.settings.periodTextColor,
...t
};
},
handleEvents() {
const t = () => {
if (this.newPeriod)
this.addPeriod(this.newPeriod.day, {
start: this.newPeriod.start,
end: this.newPeriod.end
}), this.newPeriod = null;
else if (this.draggingPeriod) {
const { el: i, day: n, index: o } = this.draggingPeriod, s = i.offsetTop, l = i.clientHeight;
this.onPositionChange(n, o, { top: s, height: l }), this.draggingPeriod = null;
} else if (this.resizingPeriod) {
const { el: i, day: n, index: o } = this.resizingPeriod, s = i.offsetTop, l = i.clientHeight;
this.onPositionChange(n, o, { top: s, height: l }), this.resizingPeriod = null;
}
}, e = (i) => {
if (this.newPeriod) {
i.preventDefault();
const n = this.roundBlock(this.newPeriod.dragStart - this.getY(i, !0)), o = this.roundBlock(this.newPeriod.startHeight - n);
if (o > this.blockHeight && this.newPeriod.top + o <= this.newPeriod.maxHeight) {
const { start: s, end: l } = this.positionToInterval(this.newPeriod), { periods: r } = this.data[this.newPeriod.day];
this.isValid({ start: s, end: l }, r) && Object.assign(this.newPeriod, { height: o, start: s, end: l });
}
} else if (this.draggingPeriod) {
i.preventDefault();
const { startDrag: n, startTop: o, el: s, day: l, index: r } = this.draggingPeriod, d = n - this.getY(i, !0), f = this.roundBlock(o - d), a = s.clientHeight, w = s.parentElement.clientHeight;
f + a <= w && f >= 0 && (s.style.top = `${f}px`, this.onPositionChange(l, r, {
top: f,
height: a
}));
} else if (this.resizingPeriod) {
i.preventDefault();
const { startDrag: n, startTop: o, startHeight: s, el: l, day: r, index: d, isUp: f } = this.resizingPeriod, a = this.roundBlock(n - this.getY(i, !0)), w = f ? this.roundBlock(o - a) : l.offsetTop;
let P = this.roundBlock(s + (f ? a : -a));
const F = document.querySelector(".vws-day").clientHeight;
P = Math.max(P, this.blockHeight), w + P <= F && w >= 0 && (l.style.height = `${P}px`, f && (l.style.top = `${w}px`), this.onPositionChange(r, d, {
top: w,
height: P
}));
}
};
this.addListener(document, "mouseup", t), this.addListener(document, "touchend", t), this.addListener(document, "mousemove", e), this.addListener(document, "touchmove", e);
},
onPeriodDown(t, e, i) {
if (this.editable) {
const n = t.currentTarget;
this.draggingPeriod = {
el: n,
day: e,
index: i,
startDrag: this.getY(t, !1),
startTop: n.offsetTop
};
}
},
onPeriodResize(t, e, i) {
if (this.editable) {
const { $event: n, isUp: o, $el: s } = t;
this.resizingPeriod = {
el: s,
day: e,
index: i,
isUp: o,
startDrag: this.getY(n, !1),
startTop: s.offsetTop,
startHeight: s.clientHeight
};
}
},
addPeriod(t, e) {
this.data[t] || (this.data[t] = {
day: t,
periods: []
});
const { periods: i } = this.data[t];
e = this.addDefaults(e), this.isValid(e, i) && i.push(e);
},
init() {
try {
if (![15, 30, 60].includes(this.settings.periodDuration))
throw new Error("Invalide period duration");
this.blocksEachHour = 60 / this.settings.periodDuration, this.maxBlocks = 24 * this.blocksEachHour, this.blockHeight = 40 / this.blocksEachHour, this.data.forEach((t) => {
t == null || t.periods.forEach((e, i) => {
this.isValid(e, t.periods) || (t.periods.splice(i, 1), console.error("Invalid period duration", e));
});
});
} catch (t) {
this.$emit("error", t);
}
},
deletePeriod(t, e) {
this.data[t].periods.splice(e, 1);
},
clonePeriod(t, e) {
const i = typeof e == "object" ? e : this.data[t].periods[e];
for (const n of this.data)
n.day !== t && this.isValid(i, n.periods) && n.periods.push({ ...i });
},
clearDayPeriods(t) {
this.data[t] && (this.data[t].periods = []);
},
cloneDayPeriods(t) {
if (this.data[t]) {
const { periods: e } = this.data[t];
for (const i of this.data)
if (i.day !== t)
for (const n of e)
this.clonePeriod(t, n);
}
},
async onPositionChange(t, e, i) {
var o;
t = (o = this.data) == null ? void 0 : o[t];
const n = t == null ? void 0 : t.periods[e];
if (n) {
const { start: s, end: l } = this.positionToInterval(i), r = n.start, d = n.end;
Object.assign(n, { start: s, end: l }), await this.$nextTick(), this.isValid(n, t.periods) || Object.assign(n, { start: r, end: d });
}
},
/** Round val to nearest multiple of blockHeight */
roundBlock(t) {
return Math.round(t / this.blockHeight) * this.blockHeight;
},
/** Returns start/end hours string (hh:mm) from period top/height in pixel */
positionToInterval(t) {
return {
start: this.blocksToTime(t.top / this.blockHeight),
end: this.blocksToTime(Math.floor((t.top + t.height) / this.blockHeight))
};
},
/**
* Return a readable hour from a block position
* @param position
* @returns {number}
*/
blocksToTime(t) {
t > this.maxBlocks && (t = 0), t < 0 && (t = 0);
let e = Math.floor(t / this.blocksEachHour), i = (t / this.blocksEachHour - e) * 60;
if (this.settings.hour === 12) {
let n = e, o = "";
return e >= 12 && (o = "p"), e > 12 && (n = e - 12), (e === 0 || e === 24) && (o = "", n = 12), i !== 0 && (n += `:${i}`), n + o;
}
return e < 10 && (e = `0${e}`), i < 10 && (i = `0${i}`), e === 23 && i === 59 && (e = "24", i = "00"), `${e}:${i}`;
},
/** Hour to string */
formatHour(t) {
if (this.settings.hour === 12)
switch (t) {
case 0:
case 24:
t = "12am";
break;
case 12:
t = "12pm";
break;
default:
t > 12 ? t = `${t - 12}pm` : t += "am";
}
else
t > 24 && (t = 0), t < 10 && (t = `0${t}`), t += ":00";
return t;
},
/**
* Return blocks from a time string (hh:mm)
* @param time
* @returns {number}
*/
positionFormat(t) {
const e = t.split(":");
let i = parseInt(e[0], 10), n = parseInt(e[1], 10);
if (this.settings.hour === 12) {
const s = t.match(/([0-1]?[0-9]):?([0-5][0-9])?\s?(am|pm|p)?/);
let l = s[3];
l || (l = "am"), i = parseInt(s[1], 10), n = parseInt(s[2], 10), n || (n = 0), i === 12 && l === "am" && (i = 0), i === 12 && (l === "pm" || l === "p") && (l = "am"), (l === "pm" || l === "p") && (i += 12);
}
let o = 0;
return o += i * this.blocksEachHour, o += n / 60 * this.blocksEachHour, o;
},
/**
* Check if a period is valid
* @param current
* @returns {boolean}
*/
isValid(t, e) {
const i = this.positionFormat(t.start), n = this.positionFormat(t.end);
let o = 0, s = 0, l = !0;
for (const r of e)
if (r !== t && (o = this.positionFormat(r.start), s = this.positionFormat(r.end), o > i && o < n && (l = !1), s > i && s < n && (l = !1), o < i && s > n && (l = !1), (o === i || s === n) && (l = !1), !l))
break;
return l;
},
addListener(t, e, i, n) {
this.events.push({
element: t,
event: e,
callback: i
}), t.addEventListener(e, i, n);
},
async editPeriod(t, e, i) {
if (this.editable)
if (this.$attrs.onEdit)
this.$emit("edit", { day: t, index: e });
else {
i.preventDefault(), this.showEditMenu = !1, this.x = i.clientX, this.y = i.clientY;
const { periods: n } = this.modelValue[t];
this.editEvent = n[e], await this.$nextTick(), this.showEditMenu = !0;
}
},
getY(t, e) {
let i = null;
return t.type === "touchstart" || t.type === "touchmove" || t.type === "touchend" || t.type === "touchcancel" ? (e && t.preventDefault(), i = (t.touches[0] || t.changedTouches[0]).clientY) : (t.type === "mousedown" || t.type === "mouseup" || t.type === "mousemove" || t.type === "mouseover" || t.type === "mouseout" || t.type === "mouseenter" || t.type === "mouseleave") && (i = t.clientY), i;
},
selectWholeDay(t) {
if (this.data[t]) {
const { periods: e } = this.data[t];
e.length > 0 ? (e[0].start = "00:00", e[0].end = "24:00", this.data[t].periods = [e[0]]) : this.data[t].periods = [
{
start: "00:00",
end: "24:00",
title: this.settings.periodTitle,
backgroundColor: this.settings.periodBackgroundColor,
borderColor: this.settings.periodBorderColor,
textColor: this.settings.periodTextColor
}
];
}
}
}
}, J = { class: "vws vws-mode-edit vws-mode-7" }, K = { class: "vws-table" }, Q = ["onTouchstart", "onMousedown"], Z = { class: "vws-period-helper-time" }, $ = { class: "vws-grid" }, tt = { class: "vws-grid-head" }, et = { class: "vws-grid-hour" };
function it(t, e, i, n, o, s) {
const l = S("scheduler-period");
return p(), g("div", J, [
h("table", K, [
h("tbody", null, [
h("tr", null, [
(p(!0), g(x, null, C(s.daysPeriods, (r) => (p(), g("td", {
key: "day-" + r.day
}, [
h("div", {
ref_for: !0,
ref: "day",
class: "vws-day",
onTouchstart: (d) => s.onDayDown(r.day, d),
onMousedown: (d) => s.onDayDown(r.day, d)
}, [
(p(!0), g(x, null, C(r.periods, (d, f) => (p(), T(l, {
key: f,
period: d,
settings: o.settings,
editable: i.editable,
onPeriodDrag: (a) => s.onPeriodDown(a, r.day, d.index),
onPeriodResize: (a) => s.onPeriodResize(a, r.day, d.index),
onDelete: (a) => s.deletePeriod(r.day, d.index),
onClone: (a) => s.clonePeriod(r.day, d.index),
onEdit: (a) => s.editPeriod(r.day, d.index, a)
}, null, 8, ["period", "settings", "editable", "onPeriodDrag", "onPeriodResize", "onDelete", "onClone", "onEdit"]))), 128)),
o.newPeriod && o.newPeriod.day === r.day ? (p(), g("div", {
key: 0,
style: L({
top: `${o.newPeriod.top}px`,
height: `${o.newPeriod.height}px`
}),
class: "vws-period-helper"
}, [
h("div", Z, b(o.newPeriod.start) + " - " + b(o.newPeriod.end), 1)
], 4)) : H("", !0)
], 40, Q)
]))), 128))
])
])
]),
h("div", $, [
h("div", tt, [
(p(!0), g(x, null, C(o.settings.days, (r) => (p(), g("div", {
key: r,
class: "vws-grid-day"
}, [
u(z, null, {
default: m(({ isHovering: d, props: f }) => [
u(I, B({ ref_for: !0 }, f), {
default: m(() => [
u(Y, null, {
default: m(() => [
h("strong", null, b(s.isMobile ? o.settings.daysList[r - 1].slice(0, 3) : o.settings.daysList[r - 1]), 1),
v(u(k, {
class: "mx-1 mb-1",
icon: "mdi-close",
density: "compact",
size: "x-small",
variant: "plain",
title: o.settings.periodRemoveButton,
onClick: (a) => s.clearDayPeriods(r - 1)
}, null, 8, ["title", "onClick"]), [
[y, i.editable && d]
]),
v(u(k, {
class: "mx-1 mb-1",
icon: "mdi-content-copy",
density: "compact",
size: "x-small",
variant: "plain",
title: o.settings.periodDuplicateButton,
onClick: (a) => s.cloneDayPeriods(r - 1)
}, null, 8, ["title", "onClick"]), [
[y, i.editable && d]
]),
v(u(k, {
class: "mx-1 mb-1",
icon: "mdi-select-all",
density: "compact",
size: "x-small",
variant: "plain",
title: o.settings.periodSelectWholeButton,
onClick: (a) => s.selectWholeDay(r - 1)
}, null, 8, ["title", "onClick"]), [
[y, i.editable && d]
])
]),
_: 2
}, 1024)
]),
_: 2
}, 1040)
]),
_: 2
}, 1024)
]))), 128))
]),
(p(), g(x, null, C(25, (r) => h("div", {
key: r,
class: "vws-grid-line"
}, [
h("div", et, b(s.formatHour(r - 1)), 1)
])), 64))
]),
o.showEditMenu ? (p(), T(j, {
key: 0,
modelValue: o.showEditMenu,
"onUpdate:modelValue": e[1] || (e[1] = (r) => o.showEditMenu = r),
target: [o.x, o.y],
"close-on-content-click": !1
}, {
default: m(() => [
u(_, { density: "compact" }, {
default: m(() => [
u(V, null, {
default: m(() => [
u(W, {
modelValue: o.editEvent.title,
"onUpdate:modelValue": e[0] || (e[0] = (r) => o.editEvent.title = r),
label: "Title",
required: "",
density: "compact",
variant: "solo-filled",
type: o.settings.inputType
}, null, 8, ["modelValue", "type"])
]),
_: 1
}),
u(V, { style: { "max-width": "300px" } }, {
default: m(() => [
(p(!0), g(x, null, C(o.settings.colors, (r) => (p(), T(k, {
key: r,
icon: "",
elevation: o.editEvent.backgroundColor === r ? 10 : 0
}, {
default: m(() => [
u(E, {
color: r,
size: "35px",
onClick: (d) => o.editEvent.backgroundColor = r
}, {
default: m(() => e[2] || (e[2] = [
D("mdi-circle")
])),
_: 2
}, 1032, ["color", "onClick"])
]),
_: 2
}, 1032, ["elevation"]))), 128))
]),
_: 1
})
]),
_: 1
})
]),
_: 1
}, 8, ["modelValue", "target"])) : H("", !0)
]);
}
const ut = /* @__PURE__ */ M(G, [["render", it]]);
export {
ut as default
};