UNPKG

@innovation-system/vuetify-week-scheduler-v3

Version:
670 lines (669 loc) 24.2 kB
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 };