Added colorwheel plugin for later use
This commit is contained in:
parent
945f8212ab
commit
325fb092ad
3 changed files with 1169 additions and 1 deletions
761
amd/src/vue-color-picker-wheel.js
Normal file
761
amd/src/vue-color-picker-wheel.js
Normal file
|
@ -0,0 +1,761 @@
|
||||||
|
/* eslint-disable */
|
||||||
|
/********************************************************************************
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2018 Stijlbreuk
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
********************************************************************************/
|
||||||
|
function isIE () {
|
||||||
|
var userAgent = window.navigator.userAgent;
|
||||||
|
return /MSIE|Trident/.test(userAgent);
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
|
||||||
|
var DEFAULT_WIDTH_HEIGHT = 300;
|
||||||
|
var DEFAULT_START_COLOR = '#000000';
|
||||||
|
|
||||||
|
var script = {
|
||||||
|
name: 'color-picker',
|
||||||
|
props: {
|
||||||
|
width: {
|
||||||
|
required: false,
|
||||||
|
type: Number,
|
||||||
|
default: DEFAULT_WIDTH_HEIGHT
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
required: false,
|
||||||
|
type: Number,
|
||||||
|
default: DEFAULT_WIDTH_HEIGHT
|
||||||
|
},
|
||||||
|
disabled: {
|
||||||
|
required: false,
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
startColor: {
|
||||||
|
required: false,
|
||||||
|
type: String,
|
||||||
|
},
|
||||||
|
value: {
|
||||||
|
required: false,
|
||||||
|
type: String
|
||||||
|
}
|
||||||
|
},
|
||||||
|
mounted: function mounted() {
|
||||||
|
/**
|
||||||
|
* @deprecated since: 0.4.0, remove in: 1.0.0, https://github.com/stijlbreuk/vue-color-picker-wheel/issues/6
|
||||||
|
*/
|
||||||
|
if (this.hasCamelCaseColorChangeListener) {
|
||||||
|
console.warn("Using the colorChange event is deprecated since version 0.4.0. It will be deleted in version 1.0.0. 'v-model' or the kebab-case variant 'color-change' should be used.");
|
||||||
|
}
|
||||||
|
this.initWidget();
|
||||||
|
this.setColor(this.value || this.startColor || DEFAULT_START_COLOR);
|
||||||
|
},
|
||||||
|
data: function data() {
|
||||||
|
return {
|
||||||
|
debug: false,
|
||||||
|
dragging: false,
|
||||||
|
circleDrag: false,
|
||||||
|
color: '',
|
||||||
|
rgb: '',
|
||||||
|
hsl: '',
|
||||||
|
radius: '',
|
||||||
|
square: '',
|
||||||
|
mid: '',
|
||||||
|
markerSize: '',
|
||||||
|
ctxMask: '',
|
||||||
|
ctxOverlay: '',
|
||||||
|
cnvMask: '',
|
||||||
|
cnvOverlay: '',
|
||||||
|
offset: {
|
||||||
|
left: '',
|
||||||
|
top: ''
|
||||||
|
}
|
||||||
|
};
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
value: function value(newVal, oldVal) {
|
||||||
|
if (newVal !== oldVal) {
|
||||||
|
this.setColor(newVal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
/**
|
||||||
|
* @deprecated since: 0.4.0, remove in: 1.0.0, https://github.com/stijlbreuk/vue-color-picker-wheel/issues/6
|
||||||
|
*/
|
||||||
|
hasCamelCaseColorChangeListener: function hasCamelCaseColorChangeListener() {
|
||||||
|
return this.$listeners && this.$listeners.colorChange;
|
||||||
|
},
|
||||||
|
solidStyle: function solidStyle() {
|
||||||
|
return {
|
||||||
|
'background-color': this.pack(this.HSLToRGB([this.hsl[0], 1, 0.5])),
|
||||||
|
width: ((this.square * 2 - 1) + "px"),
|
||||||
|
height: ((this.square * 2 - 1) + "px"),
|
||||||
|
left: ((this.mid - this.square) + "px"),
|
||||||
|
top: ((this.mid - this.square) + "px")
|
||||||
|
};
|
||||||
|
},
|
||||||
|
wheelWidth: function wheelWidth() {
|
||||||
|
return (this.width || 300) / 10;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
setColor: function setColor(color, noEmit) {
|
||||||
|
if ( noEmit === void 0 ) noEmit = false;
|
||||||
|
|
||||||
|
var unpack = this.unpack(color);
|
||||||
|
if (this.color !== color && unpack) {
|
||||||
|
this.color = color;
|
||||||
|
this.rgb = unpack;
|
||||||
|
this.hsl = this.RGBToHSL(this.rgb);
|
||||||
|
this.updateDisplay(noEmit);
|
||||||
|
}
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
setHSL: function setHSL(hsl) {
|
||||||
|
this.hsl = hsl;
|
||||||
|
this.rgb = this.HSLToRGB(hsl);
|
||||||
|
this.color = this.pack(this.rgb);
|
||||||
|
this.updateDisplay();
|
||||||
|
return this;
|
||||||
|
},
|
||||||
|
initWidget: function initWidget() {
|
||||||
|
// Determine layout
|
||||||
|
this.radius = (this.width - this.wheelWidth) / 2 - 1;
|
||||||
|
this.square = Math.floor((this.radius - this.wheelWidth / 2) * 0.7) - 1;
|
||||||
|
this.mid = Math.floor(this.width / 2);
|
||||||
|
this.markerSize = this.wheelWidth * 0.3;
|
||||||
|
|
||||||
|
// Set up drawing context.
|
||||||
|
this.cnvMask = this.$refs['farbtastic-mask'];
|
||||||
|
this.ctxMask = this.cnvMask.getContext('2d');
|
||||||
|
this.cnvOverlay = this.$refs['farbtastic-overlay'];
|
||||||
|
this.ctxOverlay = this.cnvOverlay.getContext('2d');
|
||||||
|
this.devicePixelRatio = window.devicePixelRatio || 1;
|
||||||
|
this.upscaleCanvas(this.cnvMask);
|
||||||
|
this.upscaleCanvas(this.cnvOverlay);
|
||||||
|
this.ctxMask.translate(this.mid, this.mid);
|
||||||
|
this.ctxOverlay.translate(this.mid, this.mid);
|
||||||
|
|
||||||
|
// Draw widget base layers.
|
||||||
|
this.drawCircle();
|
||||||
|
this.drawMask();
|
||||||
|
},
|
||||||
|
upscaleCanvas: function upscaleCanvas(cnv) {
|
||||||
|
var ctx = cnv.getContext('2d');
|
||||||
|
var backingStoreRatio =
|
||||||
|
ctx.webkitBackingStorePixelRatio ||
|
||||||
|
ctx.mozBackingStorePixelRatio ||
|
||||||
|
ctx.msBackingStorePixelRatio ||
|
||||||
|
ctx.oBackingStorePixelRatio ||
|
||||||
|
ctx.backingStorePixelRatio ||
|
||||||
|
1;
|
||||||
|
if (this.devicePixelRatio !== backingStoreRatio) {
|
||||||
|
var ratio = this.devicePixelRatio / backingStoreRatio;
|
||||||
|
|
||||||
|
var oldWidth = cnv.width;
|
||||||
|
var oldHeight = cnv.height;
|
||||||
|
cnv.width = oldWidth * ratio;
|
||||||
|
cnv.height = oldHeight * ratio;
|
||||||
|
cnv.style.width = oldWidth + "px";
|
||||||
|
cnv.style.height = oldHeight + "px";
|
||||||
|
ctx.scale(ratio, ratio);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
drawCircle: function drawCircle() {
|
||||||
|
var tm = +(new Date());
|
||||||
|
// Draw a hue circle with a bunch of gradient-stroked beziers.
|
||||||
|
// Have to use beziers, as gradient-stroked arcs don't work.
|
||||||
|
var n = 24;
|
||||||
|
var r = this.radius;
|
||||||
|
var w = this.wheelWidth;
|
||||||
|
var nudge = (8 / r / n) * Math.PI; // Fudge factor for seams.
|
||||||
|
var m = this.ctxMask;
|
||||||
|
var angle1 = 0;
|
||||||
|
var angle2;
|
||||||
|
// let d1;
|
||||||
|
var color1;
|
||||||
|
var color2;
|
||||||
|
m.save();
|
||||||
|
m.lineWidth = w / r;
|
||||||
|
m.scale(r, r);
|
||||||
|
// Each segment goes from angle1 to angle2.
|
||||||
|
// eslint-disable-next-line
|
||||||
|
for (var i = 0; i <= n; ++i) {
|
||||||
|
var d2 = i / n;
|
||||||
|
angle2 = d2 * Math.PI * 2;
|
||||||
|
// Endpoints
|
||||||
|
var x1 = Math.sin(angle1);
|
||||||
|
var y1 = -Math.cos(angle1);
|
||||||
|
var x2 = Math.sin(angle2);
|
||||||
|
var y2 = -Math.cos(angle2);
|
||||||
|
// Midpoint chosen so that the endpoints are tangent to the circle.
|
||||||
|
var am = (angle1 + angle2) / 2;
|
||||||
|
var tan = 1 / Math.cos((angle2 - angle1) / 2);
|
||||||
|
var xm = Math.sin(am) * tan;
|
||||||
|
var ym = -Math.cos(am) * tan;
|
||||||
|
// New color
|
||||||
|
color2 = this.pack(this.HSLToRGB([d2, 1, 0.5]));
|
||||||
|
if (i > 0) {
|
||||||
|
// Create gradient fill between the endpoints.
|
||||||
|
var grad = m.createLinearGradient(x1, y1, x2, y2);
|
||||||
|
grad.addColorStop(0, color1);
|
||||||
|
grad.addColorStop(1, color2);
|
||||||
|
m.strokeStyle = grad;
|
||||||
|
// Draw quadratic curve segment.
|
||||||
|
m.beginPath();
|
||||||
|
m.moveTo(x1, y1);
|
||||||
|
m.quadraticCurveTo(xm, ym, x2, y2);
|
||||||
|
m.stroke();
|
||||||
|
}
|
||||||
|
// Prevent seams where curves join.
|
||||||
|
angle1 = angle2 - nudge;
|
||||||
|
color1 = color2;
|
||||||
|
// d1 = d2;
|
||||||
|
}
|
||||||
|
m.restore();
|
||||||
|
if (this.debug) {
|
||||||
|
var debugElement = document.createElement('div');
|
||||||
|
debugElement.textContent = "drawCircle " + ((+(new Date()) - tm)) + " ms";
|
||||||
|
document.body.appendChild(debugElement);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
drawMask: function drawMask() {
|
||||||
|
var this$1 = this;
|
||||||
|
|
||||||
|
var tm = +(new Date());
|
||||||
|
|
||||||
|
// Iterate over sat/lum space and calculate appropriate mask pixel values.
|
||||||
|
var size = this.square * 2;
|
||||||
|
var sq = this.square;
|
||||||
|
function calculateMask(sizex, sizey, outputPixel) {
|
||||||
|
var isx = 1 / sizex;
|
||||||
|
var isy = 1 / sizey;
|
||||||
|
// eslint-disable-next-line
|
||||||
|
for (var y = 0; y <= sizey; ++y) {
|
||||||
|
var l = 1 - y * isy;
|
||||||
|
// eslint-disable-next-line
|
||||||
|
for (var x = 0; x <= sizex; ++x) {
|
||||||
|
var s = 1 - x * isx;
|
||||||
|
// From sat/lum to alpha and color (grayscale)
|
||||||
|
var a = 1 - 2 * Math.min(l * s, (1 - l) * s);
|
||||||
|
var c = a > 0 ? (2 * l - 1 + a) * (0.5 / a) : 0;
|
||||||
|
outputPixel(x, y, c, a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Method #1: direct pixel access (new Canvas).
|
||||||
|
if (this.ctxMask.getImageData) {
|
||||||
|
// Create half-resolution buffer.
|
||||||
|
var sz = Math.floor(size / 2);
|
||||||
|
var buffer = document.createElement('canvas');
|
||||||
|
buffer.width = sz + 1;
|
||||||
|
buffer.height = sz + 1;
|
||||||
|
var ctx = buffer.getContext('2d');
|
||||||
|
var frame = ctx.getImageData(0, 0, sz + 1, sz + 1);
|
||||||
|
|
||||||
|
var i = 0;
|
||||||
|
calculateMask(sz, sz, function (x, y, c, a) {
|
||||||
|
// eslint-disable-next-line
|
||||||
|
frame.data[i++] = frame.data[i++] = frame.data[i++] = c * 255;
|
||||||
|
// eslint-disable-next-line
|
||||||
|
frame.data[i++] = a * 255;
|
||||||
|
});
|
||||||
|
|
||||||
|
ctx.putImageData(frame, 0, 0);
|
||||||
|
this.ctxMask.drawImage(
|
||||||
|
buffer,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
sz + 1,
|
||||||
|
sz + 1,
|
||||||
|
-sq,
|
||||||
|
-sq,
|
||||||
|
sq * 2,
|
||||||
|
sq * 2
|
||||||
|
);
|
||||||
|
} else if (!isIE()) {
|
||||||
|
// Render directly at half-resolution
|
||||||
|
var sz$1 = Math.floor(size / 2);
|
||||||
|
calculateMask(sz$1, sz$1, function (x, y, _c, a) {
|
||||||
|
var c = Math.round(_c * 255);
|
||||||
|
this$1.ctxMask.fillStyle = "rgba(" + c + ", " + c + ", " + c + ", " + a + ")";
|
||||||
|
this$1.ctxMask.fillRect(x * 2 - sq - 1, y * 2 - sq - 1, 2, 2);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
var cacheLast;
|
||||||
|
var cache;
|
||||||
|
var w = 6; // Each strip is 6 pixels wide.
|
||||||
|
var sizex = Math.floor(size / w);
|
||||||
|
// 6 vertical pieces of gradient per strip.
|
||||||
|
calculateMask(sizex, 6, function (x, y, c, a) {
|
||||||
|
if (x === 0) {
|
||||||
|
cacheLast = cache;
|
||||||
|
cache = [];
|
||||||
|
}
|
||||||
|
c = Math.round(c * 255);
|
||||||
|
a = Math.round(a * 255);
|
||||||
|
// We can only start outputting gradients
|
||||||
|
// once we have two rows of pixels.
|
||||||
|
if (y > 0) {
|
||||||
|
var cLast = cacheLast[x][0];
|
||||||
|
var aLast = cacheLast[x][1];
|
||||||
|
var color1 = this$1.packDX(cLast, aLast);
|
||||||
|
var color2 = this$1.packDX(c, a);
|
||||||
|
var y1 = Math.round(this$1.mid + ((y - 1) * 0.333 - 1) * sq);
|
||||||
|
var y2 = Math.round(this$1.mid + (y * 0.333 - 1) * sq);
|
||||||
|
// Append div to canvasMask
|
||||||
|
var div = document.createElement('div');
|
||||||
|
div.style.position = 'absolute';
|
||||||
|
div.style.filter = "progid:DXImageTransform.Microsoft.Gradient(StartColorStr=" + color1 + ", EndColorStr=" + color2 + ", GradientType=0)";
|
||||||
|
div.style.top = y1;
|
||||||
|
div.style.height = y2 - y1;
|
||||||
|
div.style.left = this$1.mid + (x * w - sq - 1);
|
||||||
|
div.style.width = this$1.mid + (x * w - sq - 1);
|
||||||
|
this$1.cnvMask.appendChild(div);
|
||||||
|
}
|
||||||
|
cache.push([c, a]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (this.debug) {
|
||||||
|
var debugElement = document.createElement('div');
|
||||||
|
debugElement.textContent = "drawMask " + ((+(new Date()) - tm)) + " ms";
|
||||||
|
document.body.appendChild(debugElement);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
drawMarkers: function drawMarkers() {
|
||||||
|
// Determine marker dimensions
|
||||||
|
var sz = this.width;
|
||||||
|
var lw = Math.ceil(this.markerSize / 4);
|
||||||
|
var r = this.markerSize - lw + 1;
|
||||||
|
var angle = this.hsl[0] * 6.28;
|
||||||
|
var x1 = Math.sin(angle) * this.radius;
|
||||||
|
var y1 = -Math.cos(angle) * this.radius;
|
||||||
|
var x2 = 2 * this.square * (0.5 - this.hsl[1]);
|
||||||
|
var y2 = 2 * this.square * (0.5 - this.hsl[2]);
|
||||||
|
var c1 = this.invert ? '#fff' : '#000';
|
||||||
|
var c2 = this.invert ? '#000' : '#fff';
|
||||||
|
var circles = [
|
||||||
|
{ x: x1, y: y1, r: r, c: '#000', lw: lw + 1 },
|
||||||
|
{ x: x1, y: y1, r: this.markerSize, c: '#fff', lw: lw },
|
||||||
|
{ x: x2, y: y2, r: r, c: c2, lw: lw + 1 },
|
||||||
|
{ x: x2, y: y2, r: this.markerSize, c: c1, lw: lw }
|
||||||
|
];
|
||||||
|
|
||||||
|
// Update the overlay canvas.
|
||||||
|
this.ctxOverlay.clearRect(-this.mid, -this.mid, sz, sz);
|
||||||
|
for (var i = 0; i < circles.length; i += 1) {
|
||||||
|
var c = circles[i];
|
||||||
|
this.ctxOverlay.lineWidth = c.lw;
|
||||||
|
this.ctxOverlay.strokeStyle = c.c;
|
||||||
|
this.ctxOverlay.beginPath();
|
||||||
|
this.ctxOverlay.arc(c.x, c.y, c.r, 0, Math.PI * 2, true);
|
||||||
|
this.ctxOverlay.stroke();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
updateDisplay: function updateDisplay(noEmit) {
|
||||||
|
// Determine whether labels/markers should invert.
|
||||||
|
this.invert =
|
||||||
|
this.rgb[0] * 0.3 + this.rgb[1] * 0.59 + this.rgb[2] * 0.11 <= 0.6;
|
||||||
|
|
||||||
|
// Draw markers
|
||||||
|
this.drawMarkers();
|
||||||
|
|
||||||
|
if (!noEmit) {
|
||||||
|
// Emit color
|
||||||
|
this.$emit('input', this.color);
|
||||||
|
/**
|
||||||
|
* @deprecated since: 0.4.0, remove in: 1.0.0, https://github.com/stijlbreuk/vue-color-picker-wheel/issues/6
|
||||||
|
*/
|
||||||
|
this.$emit('colorChange', this.color);
|
||||||
|
this.$emit('color-change', this.color);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
widgetCoords: function widgetCoords(event) {
|
||||||
|
return {
|
||||||
|
x: event.clientX - this.offset.left - this.mid,
|
||||||
|
y: event.clientY - this.offset.top - this.mid
|
||||||
|
};
|
||||||
|
},
|
||||||
|
mousedown: function mousedown(event) {
|
||||||
|
if (this.disabled) { return false; }
|
||||||
|
// Capture mouse
|
||||||
|
if (!this.dragging) {
|
||||||
|
document.addEventListener('mousemove', this.mousemove);
|
||||||
|
document.addEventListener('mouseup', this.mouseup);
|
||||||
|
this.dragging = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the stored offset for the widget.
|
||||||
|
this.offset = {
|
||||||
|
left: this.$refs['color-wheel'].getBoundingClientRect().left,
|
||||||
|
top: this.$refs['color-wheel'].getBoundingClientRect().top
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check which area is being dragged
|
||||||
|
var pos = this.widgetCoords(event);
|
||||||
|
this.circleDrag =
|
||||||
|
Math.max(Math.abs(pos.x), Math.abs(pos.y)) > this.square + 2;
|
||||||
|
|
||||||
|
// Process
|
||||||
|
this.mousemove(event);
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
mousemove: function mousemove(event) {
|
||||||
|
// Get coordinates relative to color picker center
|
||||||
|
var pos = this.widgetCoords(event);
|
||||||
|
|
||||||
|
// Set new HSL parameters
|
||||||
|
if (this.circleDrag) {
|
||||||
|
var hue = Math.atan2(pos.x, -pos.y) / 6.28;
|
||||||
|
this.setHSL([(hue + 1) % 1, this.hsl[1], this.hsl[2]]);
|
||||||
|
} else {
|
||||||
|
var sat = Math.max(0, Math.min(1, -(pos.x / this.square / 2) + 0.5));
|
||||||
|
var lum = Math.max(0, Math.min(1, -(pos.y / this.square / 2) + 0.5));
|
||||||
|
this.setHSL([this.hsl[0], sat, lum]);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
mouseup: function mouseup() {
|
||||||
|
// Uncapture mouse
|
||||||
|
document.removeEventListener('mousemove', this.mousemove);
|
||||||
|
document.removeEventListener('mouseup', this.mouseup);
|
||||||
|
this.dragging = false;
|
||||||
|
},
|
||||||
|
/* Constious color utility functions */
|
||||||
|
dec2hex: function dec2hex(x) {
|
||||||
|
return (x < 16 ? '0' : '') + x.toString(16);
|
||||||
|
},
|
||||||
|
packDX: function packDX(c, a) {
|
||||||
|
return ("#" + (this.dec2hex(a) +
|
||||||
|
this.dec2hex(c) +
|
||||||
|
this.dec2hex(c) +
|
||||||
|
this.dec2hex(c)));
|
||||||
|
},
|
||||||
|
pack: function pack(rgb) {
|
||||||
|
var r = Math.round(rgb[0] * 255);
|
||||||
|
var g = Math.round(rgb[1] * 255);
|
||||||
|
var b = Math.round(rgb[2] * 255);
|
||||||
|
return ("#" + (this.dec2hex(r) + this.dec2hex(g) + this.dec2hex(b)));
|
||||||
|
},
|
||||||
|
unpack: function unpack(color) {
|
||||||
|
if (color.length === 7) {
|
||||||
|
return [1, 3, 5].map(
|
||||||
|
function (i) { return parseInt(color.substring(i, i + 2), 16) / 255; }
|
||||||
|
);
|
||||||
|
} else if (color.length === 4) {
|
||||||
|
return [1, 2, 3].map(function (i) { return parseInt(color.substring(i, i + 1), 16) / 15; });
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
HSLToRGB: function HSLToRGB(hsl) {
|
||||||
|
var h = hsl[0];
|
||||||
|
var s = hsl[1];
|
||||||
|
var l = hsl[2];
|
||||||
|
var m2 = l <= 0.5 ? l * (s + 1) : l + s - l * s;
|
||||||
|
var m1 = l * 2 - m2;
|
||||||
|
return [
|
||||||
|
this.hueToRGB(m1, m2, h + 0.33333),
|
||||||
|
this.hueToRGB(m1, m2, h),
|
||||||
|
this.hueToRGB(m1, m2, h - 0.33333)
|
||||||
|
];
|
||||||
|
},
|
||||||
|
hueToRGB: function hueToRGB(m1, m2, h) {
|
||||||
|
h = (h + 1) % 1;
|
||||||
|
if (h * 6 < 1) { return m1 + (m2 - m1) * h * 6; }
|
||||||
|
if (h * 2 < 1) { return m2; }
|
||||||
|
if (h * 3 < 2) { return m1 + (m2 - m1) * (0.66666 - h) * 6; }
|
||||||
|
return m1;
|
||||||
|
},
|
||||||
|
RGBToHSL: function RGBToHSL(rgb) {
|
||||||
|
var r = rgb[0];
|
||||||
|
var g = rgb[1];
|
||||||
|
var b = rgb[2];
|
||||||
|
var min = Math.min(r, g, b);
|
||||||
|
var max = Math.max(r, g, b);
|
||||||
|
var delta = max - min;
|
||||||
|
var h = 0;
|
||||||
|
var s = 0;
|
||||||
|
var l = (min + max) / 2;
|
||||||
|
if (l > 0 && l < 1) {
|
||||||
|
s = delta / (l < 0.5 ? 2 * l : 2 - 2 * l);
|
||||||
|
}
|
||||||
|
if (delta > 0) {
|
||||||
|
if (max === r && max !== g) { h += (g - b) / delta; }
|
||||||
|
if (max === g && max !== b) { h += 2 + (b - r) / delta; }
|
||||||
|
if (max === b && max !== r) { h += 4 + (r - g) / delta; }
|
||||||
|
h /= 6;
|
||||||
|
}
|
||||||
|
return [h, s, l];
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Helper for returning coordinates relative to the center with touch event
|
||||||
|
*/
|
||||||
|
widgetCoordsTouch: function widgetCoordsTouch(event) {
|
||||||
|
return {
|
||||||
|
x: event.targetTouches[0].clientX - this.offset.left - this.mid,
|
||||||
|
y: event.targetTouches[0].clientY - this.offset.top - this.mid
|
||||||
|
};
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Handle the touchstart events
|
||||||
|
*/
|
||||||
|
touchHandleStart: function touchHandleStart(event) {
|
||||||
|
// Ignore the event if another is already being handled
|
||||||
|
if (this.touchHandled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set the flag to prevent others from inheriting the touch event
|
||||||
|
this.touchHandled = true;
|
||||||
|
|
||||||
|
// Track movement to determine if interaction was a click
|
||||||
|
this._touchMoved = false;
|
||||||
|
|
||||||
|
// Update the stored offset for the widget.
|
||||||
|
this.offset = {
|
||||||
|
left: this.$refs['color-wheel'].getBoundingClientRect().left,
|
||||||
|
top: this.$refs['color-wheel'].getBoundingClientRect().top
|
||||||
|
};
|
||||||
|
|
||||||
|
// Check which area is being dragged
|
||||||
|
var pos = this.widgetCoordsTouch(event);
|
||||||
|
this.circleDrag =
|
||||||
|
Math.max(Math.abs(pos.x), Math.abs(pos.y)) > this.square + 2;
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Handle the touchstart events
|
||||||
|
*/
|
||||||
|
touchHandleMove: function touchHandleMove(event) {
|
||||||
|
// Ignore event if not handled
|
||||||
|
if (!this.touchHandled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
event.preventDefault();
|
||||||
|
|
||||||
|
// Interaction was not a click
|
||||||
|
this._touchMoved = true;
|
||||||
|
|
||||||
|
// Get coordinates relative to color picker center
|
||||||
|
var pos = this.widgetCoordsTouch(event);
|
||||||
|
|
||||||
|
// Set new HSL parameters
|
||||||
|
if (this.circleDrag) {
|
||||||
|
var hue = Math.atan2(pos.x, -pos.y) / 6.28;
|
||||||
|
this.setHSL([(hue + 1) % 1, this.hsl[1], this.hsl[2]]);
|
||||||
|
} else {
|
||||||
|
var sat = Math.max(0, Math.min(1, -(pos.x / this.square / 2) + 0.5));
|
||||||
|
var lum = Math.max(0, Math.min(1, -(pos.y / this.square / 2) + 0.5));
|
||||||
|
this.setHSL([this.hsl[0], sat, lum]);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Handle the touchstart events
|
||||||
|
*/
|
||||||
|
touchHandleEnd: function touchHandleEnd() {
|
||||||
|
// Ignore event if not handled
|
||||||
|
if (!this.touchHandled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// Unset the flag to allow other widgets to inherit the touch event
|
||||||
|
this.touchHandled = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/* script */
|
||||||
|
var __vue_script__ = script;
|
||||||
|
|
||||||
|
/* template */
|
||||||
|
var __vue_render__ = function () {var _vm=this;var _h=_vm.$createElement;var _c=_vm._self._c||_h;return _c('div',{ref:"color-wheel",staticClass:"cpw_container",class:{s_disabled: _vm.disabled},style:({width: (_vm.width + "px"), height: (_vm.height + "px"), position: 'relative'}),attrs:{"id":"color-wheel"}},[_c('div',{ref:"farbtastic-solid",staticClass:"farbtastic-solid",staticStyle:{"position":"absolute"},style:(_vm.solidStyle)}),_vm._v(" "),_c('canvas',{ref:"farbtastic-mask",staticClass:"farbtastic-mask",style:({width: _vm.width, height: _vm.height}),attrs:{"width":_vm.width,"height":_vm.height}}),_vm._v(" "),_c('canvas',{ref:"farbtastic-overlay",staticClass:"farbtastic-overlay",style:({width: _vm.width, height: _vm.height}),attrs:{"width":_vm.width,"height":_vm.height},on:{"mousedown":_vm.mousedown,"touchstart":_vm.touchHandleStart,"touchmove":_vm.touchHandleMove,"touchend":_vm.touchHandleEnd}})])};
|
||||||
|
var __vue_staticRenderFns__ = [];
|
||||||
|
|
||||||
|
/* style */
|
||||||
|
var __vue_inject_styles__ = function (inject) {
|
||||||
|
if (!inject) { return }
|
||||||
|
inject("data-v-58e7c5c8_0", { source: "\n.s_disabled[data-v-58e7c5c8]{opacity:.5\n}\n.cpw_container[data-v-58e7c5c8]{-webkit-touch-callout:none;-webkit-text-size-adjust:none;-moz-text-size-adjust:none;-ms-text-size-adjust:none;text-size-adjust:none;tap-highlight-color:transparent;tap-highlight-color:transparent;-webkit-user-select:none;-moz-user-select:none;-ms-user-select:none;user-select:none\n}\n.cpw_container .farbtastic-mask[data-v-58e7c5c8]{position:absolute;left:0\n}\n.cpw_container .farbtastic-overlay[data-v-58e7c5c8]{position:absolute;left:0\n}", map: undefined, media: undefined });
|
||||||
|
|
||||||
|
};
|
||||||
|
/* scoped */
|
||||||
|
var __vue_scope_id__ = "data-v-58e7c5c8";
|
||||||
|
/* module identifier */
|
||||||
|
var __vue_module_identifier__ = undefined;
|
||||||
|
/* functional template */
|
||||||
|
var __vue_is_functional_template__ = false;
|
||||||
|
/* component normalizer */
|
||||||
|
function __vue_normalize__(
|
||||||
|
template, style, script$$1,
|
||||||
|
scope, functional, moduleIdentifier,
|
||||||
|
createInjector, createInjectorSSR
|
||||||
|
) {
|
||||||
|
var component = (typeof script$$1 === 'function' ? script$$1.options : script$$1) || {};
|
||||||
|
|
||||||
|
// For security concerns, we use only base name in production mode.
|
||||||
|
component.__file = "color-picker.vue";
|
||||||
|
|
||||||
|
if (!component.render) {
|
||||||
|
component.render = template.render;
|
||||||
|
component.staticRenderFns = template.staticRenderFns;
|
||||||
|
component._compiled = true;
|
||||||
|
|
||||||
|
if (functional) { component.functional = true; }
|
||||||
|
}
|
||||||
|
|
||||||
|
component._scopeId = scope;
|
||||||
|
|
||||||
|
{
|
||||||
|
var hook;
|
||||||
|
if (style) {
|
||||||
|
hook = function(context) {
|
||||||
|
style.call(this, createInjector(context));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hook !== undefined) {
|
||||||
|
if (component.functional) {
|
||||||
|
// register for functional component in vue file
|
||||||
|
var originalRender = component.render;
|
||||||
|
component.render = function renderWithStyleInjection(h, context) {
|
||||||
|
hook.call(context);
|
||||||
|
return originalRender(h, context)
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// inject component registration as beforeCreate hook
|
||||||
|
var existing = component.beforeCreate;
|
||||||
|
component.beforeCreate = existing ? [].concat(existing, hook) : [hook];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return component
|
||||||
|
}
|
||||||
|
/* style inject */
|
||||||
|
function __vue_create_injector__() {
|
||||||
|
var head = document.head || document.getElementsByTagName('head')[0];
|
||||||
|
var styles = __vue_create_injector__.styles || (__vue_create_injector__.styles = {});
|
||||||
|
var isOldIE =
|
||||||
|
typeof navigator !== 'undefined' &&
|
||||||
|
/msie [6-9]\\b/.test(navigator.userAgent.toLowerCase());
|
||||||
|
|
||||||
|
return function addStyle(id, css) {
|
||||||
|
if (document.querySelector('style[data-vue-ssr-id~="' + id + '"]')) { return } // SSR styles are present.
|
||||||
|
|
||||||
|
var group = isOldIE ? css.media || 'default' : id;
|
||||||
|
var style = styles[group] || (styles[group] = { ids: [], parts: [], element: undefined });
|
||||||
|
|
||||||
|
if (!style.ids.includes(id)) {
|
||||||
|
var code = css.source;
|
||||||
|
var index = style.ids.length;
|
||||||
|
|
||||||
|
style.ids.push(id);
|
||||||
|
|
||||||
|
if (css.map) {
|
||||||
|
// https://developer.chrome.com/devtools/docs/javascript-debugging
|
||||||
|
// this makes source maps inside style tags work properly in Chrome
|
||||||
|
code += '\n/*# sourceURL=' + css.map.sources[0] + ' */';
|
||||||
|
// http://stackoverflow.com/a/26603875
|
||||||
|
code +=
|
||||||
|
'\n/*# sourceMappingURL=data:application/json;base64,' +
|
||||||
|
btoa(unescape(encodeURIComponent(JSON.stringify(css.map)))) +
|
||||||
|
' */';
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOldIE) {
|
||||||
|
style.element = style.element || document.querySelector('style[data-group=' + group + ']');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!style.element) {
|
||||||
|
var el = style.element = document.createElement('style');
|
||||||
|
el.type = 'text/css';
|
||||||
|
|
||||||
|
if (css.media) { el.setAttribute('media', css.media); }
|
||||||
|
if (isOldIE) {
|
||||||
|
el.setAttribute('data-group', group);
|
||||||
|
el.setAttribute('data-next-index', '0');
|
||||||
|
}
|
||||||
|
|
||||||
|
head.appendChild(el);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isOldIE) {
|
||||||
|
index = parseInt(style.element.getAttribute('data-next-index'));
|
||||||
|
style.element.setAttribute('data-next-index', index + 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (style.element.styleSheet) {
|
||||||
|
style.parts.push(code);
|
||||||
|
style.element.styleSheet.cssText = style.parts
|
||||||
|
.filter(Boolean)
|
||||||
|
.join('\n');
|
||||||
|
} else {
|
||||||
|
var textNode = document.createTextNode(code);
|
||||||
|
var nodes = style.element.childNodes;
|
||||||
|
if (nodes[index]) { style.element.removeChild(nodes[index]); }
|
||||||
|
if (nodes.length) { style.element.insertBefore(textNode, nodes[index]); }
|
||||||
|
else { style.element.appendChild(textNode); }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/* style inject SSR */
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
var component = __vue_normalize__(
|
||||||
|
{ render: __vue_render__, staticRenderFns: __vue_staticRenderFns__ },
|
||||||
|
__vue_inject_styles__,
|
||||||
|
__vue_script__,
|
||||||
|
__vue_scope_id__,
|
||||||
|
__vue_is_functional_template__,
|
||||||
|
__vue_module_identifier__,
|
||||||
|
__vue_create_injector__,
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
|
// Import vue component
|
||||||
|
|
||||||
|
// Declare install function executed by Vue.use()
|
||||||
|
function install(Vue) {
|
||||||
|
if (install.installed) { return; }
|
||||||
|
install.installed = true;
|
||||||
|
Vue.component('ColorPicker', component);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create module definition for Vue.use()
|
||||||
|
var plugin = {
|
||||||
|
install: install,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Auto-install when vue is found (eg. in browser via <script> tag)
|
||||||
|
var GlobalVue = null;
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
GlobalVue = window.Vue;
|
||||||
|
} else if (typeof global !== 'undefined') {
|
||||||
|
GlobalVue = global.Vue;
|
||||||
|
}
|
||||||
|
if (GlobalVue) {
|
||||||
|
GlobalVue.use(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default component;
|
||||||
|
export { install };
|
407
classes/period.php
Normal file
407
classes/period.php
Normal file
|
@ -0,0 +1,407 @@
|
||||||
|
<?php
|
||||||
|
namespace local_treestudyplan;
|
||||||
|
|
||||||
|
require_once($CFG->libdir.'/externallib.php');
|
||||||
|
|
||||||
|
class period {
|
||||||
|
const TABLE = "local_treestudyplan_period";
|
||||||
|
private static $CACHE = [];
|
||||||
|
|
||||||
|
private $r; // Holds database record
|
||||||
|
private $id;
|
||||||
|
private $page;
|
||||||
|
|
||||||
|
public function aggregator(){
|
||||||
|
return $this->studyplan->aggregator();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cache constructors to avoid multiple creation events in one session.
|
||||||
|
public static function findById($id): self {
|
||||||
|
if(!array_key_exists($id,self::$CACHE)){
|
||||||
|
self::$CACHE[$id] = new self($id);
|
||||||
|
}
|
||||||
|
return self::$CACHE[$id];
|
||||||
|
}
|
||||||
|
|
||||||
|
private function __construct($id) {
|
||||||
|
global $DB;
|
||||||
|
$this->id = $id;
|
||||||
|
$this->r = $DB->get_record(self::TABLE,['id' => $id]);
|
||||||
|
$this->page = studyplanpage::findById($this->r->page_id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function id(){
|
||||||
|
return $this->id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function studyplan() : studyplan {
|
||||||
|
return $this->page->studyplan();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function page(){
|
||||||
|
return $this->page;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function shortname(){
|
||||||
|
return $this->r->shortname;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function fullname(){
|
||||||
|
return $this->r->fullname;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function period(){
|
||||||
|
return $this->r->period;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function startdate(){
|
||||||
|
return new \DateTime($this->r->startdate);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function enddate(){
|
||||||
|
if($this->r->enddate && strlen($this->r->enddate) > 0){
|
||||||
|
return new \DateTime($this->r->enddate);
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
// return a date 100 years into the future
|
||||||
|
return (new \DateTime($this->r->startdate))->add(new \DateInterval("P100Y"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function simple_structure($value=VALUE_REQUIRED){
|
||||||
|
return new \external_single_structure([
|
||||||
|
"id" => new \external_value(PARAM_INT, 'id of studyplan page'),
|
||||||
|
"fullname" => new \external_value(PARAM_TEXT, 'name of studyplan page'),
|
||||||
|
"shortname"=> new \external_value(PARAM_TEXT, 'shortname of studyplan page'),
|
||||||
|
"period" => new \external_value(PARAM_INT, 'period sequence'),
|
||||||
|
"startdate" => new \external_value(PARAM_TEXT, 'start date of studyplan'),
|
||||||
|
"enddate" => new \external_value(PARAM_TEXT, 'end date of studyplan'),
|
||||||
|
],'Studyplan page basic info',$value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function simple_model(){
|
||||||
|
return [
|
||||||
|
'id' => $this->r->id,
|
||||||
|
'fullname' => $this->r->fullname,
|
||||||
|
'shortname' => $this->r->shortname,
|
||||||
|
'period' => $this->r->period,
|
||||||
|
'startdate' => $this->r->startdate,
|
||||||
|
'enddate' => $this->r->enddate,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function add($fields){
|
||||||
|
global $CFG, $DB;
|
||||||
|
|
||||||
|
if(!isset($fields['page_id'])){
|
||||||
|
throw new \InvalidArgumentException("parameter 'studyplan_id' missing");
|
||||||
|
}
|
||||||
|
|
||||||
|
$addable = ['page_id','fullname','shortname','description','periods','startdate','enddate'];
|
||||||
|
$info = ['enddate' => null ];
|
||||||
|
foreach($addable as $f){
|
||||||
|
if(array_key_exists($f,$fields)){
|
||||||
|
$info[$f] = $fields[$f];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$id = $DB->insert_record(self::TABLE, $info);
|
||||||
|
return self::findById($id); // make sure the new page is immediately cached
|
||||||
|
}
|
||||||
|
|
||||||
|
public function edit($fields){
|
||||||
|
global $DB;
|
||||||
|
$editable = ['fullname','shortname','description','periods','startdate','enddate'];
|
||||||
|
$info = ['id' => $this->id,];
|
||||||
|
foreach($editable as $f){
|
||||||
|
if(array_key_exists($f,$fields)){
|
||||||
|
$info[$f] = $fields[$f];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$DB->update_record(self::TABLE, $info);
|
||||||
|
//reload record after edit
|
||||||
|
$this->r = $DB->get_record(self::TABLE,['id' => $this->id],"*",MUST_EXIST);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function delete($force=false){
|
||||||
|
global $DB;
|
||||||
|
$DB->delete_records(self::TABLE, ['id' => $this->id]);
|
||||||
|
return success::success();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function user_structure($value=VALUE_REQUIRED){
|
||||||
|
return new \external_single_structure([
|
||||||
|
"id" => new \external_value(PARAM_INT, 'id of studyplan page'),
|
||||||
|
"fullname" => new \external_value(PARAM_TEXT, 'name of studyplan page'),
|
||||||
|
"shortname"=> new \external_value(PARAM_TEXT, 'shortname of studyplan page'),
|
||||||
|
"description"=> new \external_value(PARAM_TEXT, 'description of studyplan page'),
|
||||||
|
"periods" => new \external_value(PARAM_INT, 'number of slots in studyplan page'),
|
||||||
|
"startdate" => new \external_value(PARAM_TEXT, 'start date of studyplan page'),
|
||||||
|
"enddate" => new \external_value(PARAM_TEXT, 'end date of studyplan page'),
|
||||||
|
"studylines" => new \external_multiple_structure(studyline::user_structure()),
|
||||||
|
],'Studyplan page with user info',$value);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function user_model($userid){
|
||||||
|
|
||||||
|
$model = [
|
||||||
|
'id' => $this->r->id,
|
||||||
|
'fullname' => $this->r->fullname,
|
||||||
|
'shortname' => $this->r->shortname,
|
||||||
|
'description' => $this->r->description,
|
||||||
|
'periods' => $this->r->periods,
|
||||||
|
'startdate' => $this->r->startdate,
|
||||||
|
'enddate' => $this->r->enddate,
|
||||||
|
'studylines' => [],
|
||||||
|
];
|
||||||
|
|
||||||
|
$children = studyline::find_page_children($this);
|
||||||
|
foreach($children as $c)
|
||||||
|
{
|
||||||
|
$model['studylines'][] = $c->user_model($userid);
|
||||||
|
}
|
||||||
|
return $model;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function find_studyplan_children(studyplan $plan)
|
||||||
|
{
|
||||||
|
global $DB;
|
||||||
|
$list = [];
|
||||||
|
$ids = $DB->get_fieldset_select(self::TABLE,"id","studyplan_id = :plan_id ORDER BY startdate",
|
||||||
|
['plan_id' => $plan->id()]);
|
||||||
|
foreach($ids as $id) {
|
||||||
|
$list[] = self::findById($id);
|
||||||
|
}
|
||||||
|
return $list;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function duplicate_page($page_id,$name,$shortname)
|
||||||
|
{
|
||||||
|
$ori = self::findById($page_id);
|
||||||
|
$new = $ori->duplicate($name,$shortname);
|
||||||
|
return $new->simple_model();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function duplicate($name,$shortname)
|
||||||
|
{
|
||||||
|
// First duplicate the studyplan structure
|
||||||
|
$new = studyplanpage::add([
|
||||||
|
'fullname' => $name,
|
||||||
|
'shortname' => $shortname,
|
||||||
|
'description' => $this->r->description,
|
||||||
|
'pages' => $this->r->pages,
|
||||||
|
'startdate' => $this->r->startdate,
|
||||||
|
'enddate' => empty($this->r->enddate)?null:$this->r->enddate,
|
||||||
|
]);
|
||||||
|
|
||||||
|
// next, copy the studylines
|
||||||
|
|
||||||
|
$children = studyline::find_page_children($this);
|
||||||
|
$itemtranslation = [];
|
||||||
|
$linetranslation = [];
|
||||||
|
foreach($children as $c){
|
||||||
|
$newchild = $c->duplicate($this,$itemtranslation);
|
||||||
|
$linetranslation[$c->id()] = $newchild->id();
|
||||||
|
}
|
||||||
|
|
||||||
|
// now the itemtranslation array contains all of the old child id's as keys and all of the related new ids as values
|
||||||
|
// (feature of the studyline::duplicate function)
|
||||||
|
// use this to recreate the lines in the new plan
|
||||||
|
foreach(array_keys($itemtranslation) as $item_id){
|
||||||
|
// copy based on the outgoing connections of each item, to avoid duplicates
|
||||||
|
$connections = studyitemconnection::find_outgoing($item_id);
|
||||||
|
foreach($connections as $conn){
|
||||||
|
studyitemconnection::connect($itemtranslation[$conn->from_id],$itemtranslation[$conn->to_id]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return $new;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static function export_structure()
|
||||||
|
{
|
||||||
|
return new \external_single_structure([
|
||||||
|
"format" => new \external_value(PARAM_TEXT, 'format of studyplan export'),
|
||||||
|
"content"=> new \external_value(PARAM_TEXT, 'exported studyplan content'),
|
||||||
|
],'Exported studyplan');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function export_page()
|
||||||
|
{
|
||||||
|
$model = $this->export_model();
|
||||||
|
$json = json_encode([
|
||||||
|
"type"=>"studyplanpage",
|
||||||
|
"version"=>2.0,
|
||||||
|
"page"=>$model
|
||||||
|
],\JSON_PRETTY_PRINT);
|
||||||
|
return [ "format" => "application/json", "content" => $json];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function export_page_csv()
|
||||||
|
{
|
||||||
|
//TODO: Period shortnames instead of just P1, P2, P3 etc
|
||||||
|
|
||||||
|
$model = $this->editor_model();
|
||||||
|
|
||||||
|
$slots = intval($model["slots"]);
|
||||||
|
// First line
|
||||||
|
$csv = "\"Studyline[{$slots}]\"";
|
||||||
|
for($i = 1; $i <= $slots; $i++){
|
||||||
|
$csv .= ",\"P{$i}\"";
|
||||||
|
}
|
||||||
|
$csv .= "\r\n";
|
||||||
|
// next, make one line per studyline
|
||||||
|
foreach($model["studylines"] as $line){
|
||||||
|
// determine how many fields are simultaneous in the line at maximum
|
||||||
|
$maxlines = 1;
|
||||||
|
for($i = 1; $i <= $slots; $i++){
|
||||||
|
if(count($line["slots"]) > $i){
|
||||||
|
$ct = 0;
|
||||||
|
foreach($line["slots"][$i][studyline::SLOTSET_COMPETENCY] as $itm){
|
||||||
|
if($itm["type"] == "course"){
|
||||||
|
$ct += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if($ct > $maxlines){
|
||||||
|
$maxlines = $ct;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for($lct = 0; $lct < $maxlines; $lct++){
|
||||||
|
$csv .= "\"{$line["name"]}\"";
|
||||||
|
for($i = 1; $i <= $slots; $i++){
|
||||||
|
$filled = false;
|
||||||
|
if(count($line["slots"]) > $i){
|
||||||
|
$ct = 0;
|
||||||
|
foreach($line["slots"][$i][studyline::SLOTSET_COMPETENCY] as $itm){
|
||||||
|
if($itm["type"] == "course"){
|
||||||
|
if($ct == $lct){
|
||||||
|
$csv .= ",\"";
|
||||||
|
$csv .= $itm["course"]["fullname"];
|
||||||
|
$csv .= "\r\n";
|
||||||
|
$first = true;
|
||||||
|
foreach($itm["course"]["grades"] as $g){
|
||||||
|
if($g["selected"]){
|
||||||
|
if($first){
|
||||||
|
$first = false;
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
$csv .= "\r\n";
|
||||||
|
}
|
||||||
|
$csv .= "- ".str_replace('"', '\'', $g["name"]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$csv .= "\"";
|
||||||
|
$filled = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
$ct++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if(!$filled) {
|
||||||
|
$csv .= ",\"\"";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$csv .= "\r\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [ "format" => "text/csv", "content" => $csv];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function export_studylines(){
|
||||||
|
$model = $this->export_studylines_model();
|
||||||
|
$json = json_encode([
|
||||||
|
"type"=>"studylines",
|
||||||
|
"version"=>1.0,
|
||||||
|
"studylines"=>$model,
|
||||||
|
],\JSON_PRETTY_PRINT);
|
||||||
|
return [ "format" => "application/json", "content" => $json];
|
||||||
|
}
|
||||||
|
|
||||||
|
public function export_model()
|
||||||
|
{
|
||||||
|
$model = [
|
||||||
|
'fullname' => $this->r->name,
|
||||||
|
'shortname' => $this->r->shortname,
|
||||||
|
'description' => $this->r->description,
|
||||||
|
'periods' => $this->r->periods,
|
||||||
|
'startdate' => $this->r->startdate,
|
||||||
|
'enddate' => $this->r->enddate,
|
||||||
|
'studylines' => $this->export_studylines_model(),
|
||||||
|
];
|
||||||
|
return $model;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function export_studylines_model()
|
||||||
|
{
|
||||||
|
$children = studyline::find_page_children($this);
|
||||||
|
$lines = [];
|
||||||
|
foreach($children as $c)
|
||||||
|
{
|
||||||
|
$lines[] = $c->export_model();
|
||||||
|
}
|
||||||
|
return $lines;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function import_studylines($content,$format="application/json")
|
||||||
|
{
|
||||||
|
if($format != "application/json") { return false;}
|
||||||
|
$content = json_decode($content,true);
|
||||||
|
if($content["type"] == "studylines" && $content["version"] >= 2.0){
|
||||||
|
return $this->import_studylines_model($content["studylines"]);
|
||||||
|
}
|
||||||
|
else if($content["type"] == "studyplanpage" && $content["version"] >= 2.0){
|
||||||
|
return $this->import_studylines_model($content["page"]["studylines"]);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function find_studyline_by_shortname($shortname){
|
||||||
|
$children = studyline::find_page_children($this);
|
||||||
|
foreach($children as $l){
|
||||||
|
if($shortname == $l->shortname()){
|
||||||
|
return $l;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function import_studylines_model($model)
|
||||||
|
{
|
||||||
|
// First attempt to map each studyline model to an existing or new line
|
||||||
|
$line_map = [];
|
||||||
|
foreach($model as $ix => $linemodel){
|
||||||
|
$line = $this->find_studyline_by_shortname($linemodel["shortname"]);
|
||||||
|
if(empty($line)){
|
||||||
|
$linemodel["studyplan_id"] = $this->id;
|
||||||
|
$line = studyline::add($linemodel);
|
||||||
|
} else {
|
||||||
|
//$line->edit($linemodel); // Update the line with the settings from the imported file
|
||||||
|
}
|
||||||
|
$line_map[$ix] = $line;
|
||||||
|
}
|
||||||
|
|
||||||
|
// next, let each study line import the study items
|
||||||
|
$itemtranslation = [];
|
||||||
|
$connections = [];
|
||||||
|
foreach($model as $ix => $linemodel){
|
||||||
|
$line_map[$ix]->import_studyitems($linemodel["slots"],$itemtranslation,$connections);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, create the links between the study items
|
||||||
|
foreach($connections as $from => $dests){
|
||||||
|
foreach($dests as $to){
|
||||||
|
studyitemconnection::connect($from,$itemtranslation[$to]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -176,7 +176,7 @@ class studyplanpage {
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
$DB->delete_records('local_treestudyplan_page', ['id' => $this->id]);
|
$DB->delete_records(self::TABLE, ['id' => $this->id]);
|
||||||
return success::success();
|
return success::success();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue