diff --git a/Asset/Javascript/wiki.js b/Asset/Javascript/wiki.js index 6b6fd57..868478c 100644 --- a/Asset/Javascript/wiki.js +++ b/Asset/Javascript/wiki.js @@ -1,134 +1,51 @@ jQuery(document).ready(function () { - var dragSrcEl = null; - - function handleDragStart(e) { - // Target (this) element is the source node. - dragSrcEl = this; - - e.dataTransfer.effectAllowed = 'move'; - e.dataTransfer.setData('text/html', this.outerHTML); - - this.classList.add('dragElem'); - } - function handleDragOver(e) { - if (e.preventDefault) { - e.preventDefault(); // Necessary. Allows us to drop. - } - this.classList.add('over'); - - e.dataTransfer.dropEffect = 'move'; // See the section on the DataTransfer object. - - return false; - } - - function handleDragEnter(e) { - // this / e.target is the current hover target. - } - - function handleDragLeave(e) { - this.classList.remove('over'); // this / e.target is previous target element. - } - - function handleDrop(e) { - // this/e.target is current target element. - - if (e.stopPropagation) { - e.stopPropagation(); // Stops some browsers from redirecting. - } - - // Don't do anything if dropping the same column we're dragging. - if (dragSrcEl != this) { - - let targetRoute; - if(e.target.localName == "a"){ - targetRoute = e.target.href - } else { - targetRoute = e.target.querySelector("a").href - } - let targetParams = new URL(targetRoute) - var targetProperties = {} - for (const [key, value] of targetParams.searchParams.entries()) { - targetProperties[key] = value + /* + page reorder and nesting support using jquery sorting plugin + */ + if($("#columns").data("reorder-url")){ + jQuery('#columns').sortable({ + nested: true, + onDrop: function ($item, container, _super) { + // console.log("onDrop", $item, container, _super) + container.el.removeClass("active"); + var srcProperties = { + ...$item[0].dataset } - // console.log("targetProperties", targetProperties) - - var srcParams = new URL(dragSrcEl.querySelector("a").href) - var srcProperties = {} - - for (const [key, value] of srcParams.searchParams.entries()) { - srcProperties[key] = value - } - // console.log("srcProperties", srcProperties) - - let project_id = srcProperties["project_id"] - - // console.log("project_id", project_id) + var containerProperties = {...container.el[0].dataset} let request = { - "src_wiki_id": srcProperties["wiki_id"], - "target_wiki_id": targetProperties["wiki_id"] + "src_wiki_id": srcProperties["pageId"], + "index": $item.index(), + "parent_id": containerProperties["parentId"] } - console.log("request", request) + // console.log("request", request) + $.ajax({ - cache: false, - url: $("#columns").data("reorder-url"), - contentType: "application/json", - type: "POST", - processData: false, - data: JSON.stringify(request), - success: function(data) { - // self.refresh(data); - // self.savingInProgress = false; - }, - error: function() { - // self.app.hideLoadingIcon(); - // self.savingInProgress = false; - }, - statusCode: { - 403: function(data) { - window.alert(data.responseJSON.message); - document.location.reload(true); + cache: false, + url: $("#columns").data("reorder-url"), + contentType: "application/json", + type: "POST", + processData: false, + data: JSON.stringify(request), + success: function(data) { + // self.refresh(data); + // self.savingInProgress = false; + }, + error: function() { + // self.app.hideLoadingIcon(); + // self.savingInProgress = false; + }, + statusCode: { + 403: function(data) { + window.alert(data.responseJSON.message); + document.location.reload(true); + } } - } - }); - - // Set the source column's HTML to the HTML of the column we dropped on. - //alert(this.outerHTML); - //dragSrcEl.innerHTML = this.innerHTML; - //this.innerHTML = e.dataTransfer.getData('text/html'); - this.parentNode.removeChild(dragSrcEl); - var dropHTML = e.dataTransfer.getData('text/html'); - this.insertAdjacentHTML('beforebegin', dropHTML); - var dropElem = this.previousSibling; - addDnDHandlers(dropElem); - - } - this.classList.remove('over'); - return false; + }); + _super($item, container); + }, + }) } - - function handleDragEnd(e) { - // this/e.target is the source node. - this.classList.remove('over'); - - /*[].forEach.call(cols, function (col) { - col.classList.remove('over'); - });*/ - } - - function addDnDHandlers(elem) { - elem.addEventListener('dragstart', handleDragStart, false); - elem.addEventListener('dragenter', handleDragEnter, false) - elem.addEventListener('dragover', handleDragOver, false); - elem.addEventListener('dragleave', handleDragLeave, false); - elem.addEventListener('drop', handleDrop, false); - elem.addEventListener('dragend', handleDragEnd, false); - - } - - var cols = document.querySelectorAll('#columns .wikipage'); - [].forEach.call(cols, addDnDHandlers); - }); \ No newline at end of file diff --git a/Asset/css/wiki.css b/Asset/css/wiki.css index fade2ce..2f521ce 100644 --- a/Asset/css/wiki.css +++ b/Asset/css/wiki.css @@ -1,11 +1,20 @@ -[draggable] { +/* [draggable] { -moz-user-select: none; -khtml-user-select: none; -webkit-user-select: none; user-select: none; -/* Required to make elements draggable in old WebKit */ -khtml-user-drag: element; -webkit-user-drag: element; +} */ + +body.dragging, body.dragging * { +cursor: move !important; +} + +.dragged { + position: absolute; + opacity: 0.5; + z-index: 2000; } #columns { @@ -17,7 +26,7 @@ width: 162px; padding-bottom: 5px; padding-top: 5px; text-align: left; -cursor: move; +/* cursor: move; */ } .wikipage header { height: 20px; @@ -52,4 +61,8 @@ width: 25%; } .content { width: 75%; +} + +.sidebar>ul li:last-child { + margin-bottom: 0px; } \ No newline at end of file diff --git a/Asset/vendor/jquery-sortable/jquery-sortable.js b/Asset/vendor/jquery-sortable/jquery-sortable.js new file mode 100644 index 0000000..8390b45 --- /dev/null +++ b/Asset/vendor/jquery-sortable/jquery-sortable.js @@ -0,0 +1,693 @@ +/* =================================================== + * jquery-sortable.js v0.9.13 + * http://johnny.github.com/jquery-sortable/ + * =================================================== + * Copyright (c) 2012 Jonas von Andrian + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * * The name of the author may not be used to endorse or promote products + * derived from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND + * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND + * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * ========================================================== */ + + +!function ( $, window, pluginName, undefined){ + var containerDefaults = { + // If true, items can be dragged from this container + drag: true, + // If true, items can be droped onto this container + drop: true, + // Exclude items from being draggable, if the + // selector matches the item + exclude: "", + // If true, search for nested containers within an item.If you nest containers, + // either the original selector with which you call the plugin must only match the top containers, + // or you need to specify a group (see the bootstrap nav example) + nested: true, + // If true, the items are assumed to be arranged vertically + vertical: true + }, // end container defaults + groupDefaults = { + // This is executed after the placeholder has been moved. + // $closestItemOrContainer contains the closest item, the placeholder + // has been put at or the closest empty Container, the placeholder has + // been appended to. + afterMove: function ($placeholder, container, $closestItemOrContainer) { + }, + // The exact css path between the container and its items, e.g. "> tbody" + containerPath: "", + // The css selector of the containers + containerSelector: "ol, ul", + // Distance the mouse has to travel to start dragging + distance: 0, + // Time in milliseconds after mousedown until dragging should start. + // This option can be used to prevent unwanted drags when clicking on an element. + delay: 0, + // The css selector of the drag handle + handle: "", + // The exact css path between the item and its subcontainers. + // It should only match the immediate items of a container. + // No item of a subcontainer should be matched. E.g. for ol>div>li the itemPath is "> div" + itemPath: "", + // The css selector of the items + itemSelector: "li", + // The class given to "body" while an item is being dragged + bodyClass: "dragging", + // The class giving to an item while being dragged + draggedClass: "dragged", + // Check if the dragged item may be inside the container. + // Use with care, since the search for a valid container entails a depth first search + // and may be quite expensive. + isValidTarget: function ($item, container) { + return true + }, + // Executed before onDrop if placeholder is detached. + // This happens if pullPlaceholder is set to false and the drop occurs outside a container. + onCancel: function ($item, container, _super, event) { + }, + // Executed at the beginning of a mouse move event. + // The Placeholder has not been moved yet. + onDrag: function ($item, position, _super, event) { + $item.css(position) + }, + // Called after the drag has been started, + // that is the mouse button is being held down and + // the mouse is moving. + // The container is the closest initialized container. + // Therefore it might not be the container, that actually contains the item. + onDragStart: function ($item, container, _super, event) { + $item.css({ + height: $item.outerHeight(), + width: $item.outerWidth() + }) + $item.addClass(container.group.options.draggedClass) + $("body").addClass(container.group.options.bodyClass) + }, + // Called when the mouse button is being released + onDrop: function ($item, container, _super, event) { + $item.removeClass(container.group.options.draggedClass).removeAttr("style") + $("body").removeClass(container.group.options.bodyClass) + }, + // Called on mousedown. If falsy value is returned, the dragging will not start. + // Ignore if element clicked is input, select or textarea + onMousedown: function ($item, _super, event) { + if (!event.target.nodeName.match(/^(input|select|textarea)$/i)) { + event.preventDefault() + return true + } + }, + // The class of the placeholder (must match placeholder option markup) + placeholderClass: "placeholder", + // Template for the placeholder. Can be any valid jQuery input + // e.g. a string, a DOM element. + // The placeholder must have the class "placeholder" + placeholder: '
  • ', + // If true, the position of the placeholder is calculated on every mousemove. + // If false, it is only calculated when the mouse is above a container. + pullPlaceholder: true, + // Specifies serialization of the container group. + // The pair $parent/$children is either container/items or item/subcontainers. + serialize: function ($parent, $children, parentIsContainer) { + var result = $.extend({}, $parent.data()) + + if(parentIsContainer) + return [$children] + else if ($children[0]){ + result.children = $children + } + + delete result.subContainers + delete result.sortable + + return result + }, + // Set tolerance while dragging. Positive values decrease sensitivity, + // negative values increase it. + tolerance: 0 + }, // end group defaults + containerGroups = {}, + groupCounter = 0, + emptyBox = { + left: 0, + top: 0, + bottom: 0, + right:0 + }, + eventNames = { + start: "touchstart.sortable mousedown.sortable", + drop: "touchend.sortable touchcancel.sortable mouseup.sortable", + drag: "touchmove.sortable mousemove.sortable", + scroll: "scroll.sortable" + }, + subContainerKey = "subContainers" + + /* + * a is Array [left, right, top, bottom] + * b is array [left, top] + */ + function d(a,b) { + var x = Math.max(0, a[0] - b[0], b[0] - a[1]), + y = Math.max(0, a[2] - b[1], b[1] - a[3]) + return x+y; + } + + function setDimensions(array, dimensions, tolerance, useOffset) { + var i = array.length, + offsetMethod = useOffset ? "offset" : "position" + tolerance = tolerance || 0 + + while(i--){ + var el = array[i].el ? array[i].el : $(array[i]), + // use fitting method + pos = el[offsetMethod]() + pos.left += parseInt(el.css('margin-left'), 10) + pos.top += parseInt(el.css('margin-top'),10) + dimensions[i] = [ + pos.left - tolerance, + pos.left + el.outerWidth() + tolerance, + pos.top - tolerance, + pos.top + el.outerHeight() + tolerance + ] + } + } + + function getRelativePosition(pointer, element) { + var offset = element.offset() + return { + left: pointer.left - offset.left, + top: pointer.top - offset.top + } + } + + function sortByDistanceDesc(dimensions, pointer, lastPointer) { + pointer = [pointer.left, pointer.top] + lastPointer = lastPointer && [lastPointer.left, lastPointer.top] + + var dim, + i = dimensions.length, + distances = [] + + while(i--){ + dim = dimensions[i] + distances[i] = [i,d(dim,pointer), lastPointer && d(dim, lastPointer)] + } + distances = distances.sort(function (a,b) { + return b[1] - a[1] || b[2] - a[2] || b[0] - a[0] + }) + + // last entry is the closest + return distances + } + + function ContainerGroup(options) { + this.options = $.extend({}, groupDefaults, options) + this.containers = [] + + if(!this.options.rootGroup){ + this.scrollProxy = $.proxy(this.scroll, this) + this.dragProxy = $.proxy(this.drag, this) + this.dropProxy = $.proxy(this.drop, this) + this.placeholder = $(this.options.placeholder) + + if(!options.isValidTarget) + this.options.isValidTarget = undefined + } + } + + ContainerGroup.get = function (options) { + if(!containerGroups[options.group]) { + if(options.group === undefined) + options.group = groupCounter ++ + + containerGroups[options.group] = new ContainerGroup(options) + } + + return containerGroups[options.group] + } + + ContainerGroup.prototype = { + dragInit: function (e, itemContainer) { + this.$document = $(itemContainer.el[0].ownerDocument) + + // get item to drag + var closestItem = $(e.target).closest(this.options.itemSelector); + // using the length of this item, prevents the plugin from being started if there is no handle being clicked on. + // this may also be helpful in instantiating multidrag. + if (closestItem.length) { + this.item = closestItem; + this.itemContainer = itemContainer; + if (this.item.is(this.options.exclude) || !this.options.onMousedown(this.item, groupDefaults.onMousedown, e)) { + return; + } + this.setPointer(e); + this.toggleListeners('on'); + this.setupDelayTimer(); + this.dragInitDone = true; + } + }, + drag: function (e) { + if(!this.dragging){ + if(!this.distanceMet(e) || !this.delayMet) + return + + this.options.onDragStart(this.item, this.itemContainer, groupDefaults.onDragStart, e) + this.item.before(this.placeholder) + this.dragging = true + } + + this.setPointer(e) + // place item under the cursor + this.options.onDrag(this.item, + getRelativePosition(this.pointer, this.item.offsetParent()), + groupDefaults.onDrag, + e) + + var p = this.getPointer(e), + box = this.sameResultBox, + t = this.options.tolerance + + if(!box || box.top - t > p.top || box.bottom + t < p.top || box.left - t > p.left || box.right + t < p.left) + if(!this.searchValidTarget()){ + this.placeholder.detach() + this.lastAppendedItem = undefined + } + }, + drop: function (e) { + this.toggleListeners('off') + + this.dragInitDone = false + + if(this.dragging){ + // processing Drop, check if placeholder is detached + if(this.placeholder.closest("html")[0]){ + this.placeholder.before(this.item).detach() + } else { + this.options.onCancel(this.item, this.itemContainer, groupDefaults.onCancel, e) + } + this.options.onDrop(this.item, this.getContainer(this.item), groupDefaults.onDrop, e) + + // cleanup + this.clearDimensions() + this.clearOffsetParent() + this.lastAppendedItem = this.sameResultBox = undefined + this.dragging = false + } + }, + searchValidTarget: function (pointer, lastPointer) { + if(!pointer){ + pointer = this.relativePointer || this.pointer + lastPointer = this.lastRelativePointer || this.lastPointer + } + + var distances = sortByDistanceDesc(this.getContainerDimensions(), + pointer, + lastPointer), + i = distances.length + + while(i--){ + var index = distances[i][0], + distance = distances[i][1] + + if(!distance || this.options.pullPlaceholder){ + var container = this.containers[index] + if(!container.disabled){ + if(!this.$getOffsetParent()){ + var offsetParent = container.getItemOffsetParent() + pointer = getRelativePosition(pointer, offsetParent) + lastPointer = getRelativePosition(lastPointer, offsetParent) + } + if(container.searchValidTarget(pointer, lastPointer)) + return true + } + } + } + if(this.sameResultBox) + this.sameResultBox = undefined + }, + movePlaceholder: function (container, item, method, sameResultBox) { + var lastAppendedItem = this.lastAppendedItem + if(!sameResultBox && lastAppendedItem && lastAppendedItem[0] === item[0]) + return; + + item[method](this.placeholder) + this.lastAppendedItem = item + this.sameResultBox = sameResultBox + this.options.afterMove(this.placeholder, container, item) + }, + getContainerDimensions: function () { + if(!this.containerDimensions) + setDimensions(this.containers, this.containerDimensions = [], this.options.tolerance, !this.$getOffsetParent()) + return this.containerDimensions + }, + getContainer: function (element) { + return element.closest(this.options.containerSelector).data(pluginName) + }, + $getOffsetParent: function () { + if(this.offsetParent === undefined){ + var i = this.containers.length - 1, + offsetParent = this.containers[i].getItemOffsetParent() + + if(!this.options.rootGroup){ + while(i--){ + if(offsetParent[0] != this.containers[i].getItemOffsetParent()[0]){ + // If every container has the same offset parent, + // use position() which is relative to this parent, + // otherwise use offset() + // compare #setDimensions + offsetParent = false + break; + } + } + } + + this.offsetParent = offsetParent + } + return this.offsetParent + }, + setPointer: function (e) { + var pointer = this.getPointer(e) + + if(this.$getOffsetParent()){ + var relativePointer = getRelativePosition(pointer, this.$getOffsetParent()) + this.lastRelativePointer = this.relativePointer + this.relativePointer = relativePointer + } + + this.lastPointer = this.pointer + this.pointer = pointer + }, + distanceMet: function (e) { + var currentPointer = this.getPointer(e) + return (Math.max( + Math.abs(this.pointer.left - currentPointer.left), + Math.abs(this.pointer.top - currentPointer.top) + ) >= this.options.distance) + }, + getPointer: function(e) { + var o = e.originalEvent || e.originalEvent.touches && e.originalEvent.touches[0] + return { + left: e.pageX || o.pageX, + top: e.pageY || o.pageY + } + }, + setupDelayTimer: function () { + var that = this + this.delayMet = !this.options.delay + + // init delay timer if needed + if (!this.delayMet) { + clearTimeout(this._mouseDelayTimer); + this._mouseDelayTimer = setTimeout(function() { + that.delayMet = true + }, this.options.delay) + } + }, + scroll: function (e) { + this.clearDimensions() + this.clearOffsetParent() // TODO is this needed? + }, + toggleListeners: function (method) { + var that = this, + events = ['drag','drop','scroll'] + + $.each(events,function (i,event) { + that.$document[method](eventNames[event], that[event + 'Proxy']) + }) + }, + clearOffsetParent: function () { + this.offsetParent = undefined + }, + // Recursively clear container and item dimensions + clearDimensions: function () { + this.traverse(function(object){ + object._clearDimensions() + }) + }, + traverse: function(callback) { + callback(this) + var i = this.containers.length + while(i--){ + this.containers[i].traverse(callback) + } + }, + _clearDimensions: function(){ + this.containerDimensions = undefined + }, + _destroy: function () { + containerGroups[this.options.group] = undefined + } + } + + function Container(element, options) { + this.el = element + this.options = $.extend( {}, containerDefaults, options) + + this.group = ContainerGroup.get(this.options) + this.rootGroup = this.options.rootGroup || this.group + this.handle = this.rootGroup.options.handle || this.rootGroup.options.itemSelector + + var itemPath = this.rootGroup.options.itemPath + this.target = itemPath ? this.el.find(itemPath) : this.el + + this.target.on(eventNames.start, this.handle, $.proxy(this.dragInit, this)) + + if(this.options.drop) + this.group.containers.push(this) + } + + Container.prototype = { + dragInit: function (e) { + var rootGroup = this.rootGroup + + if( !this.disabled && + !rootGroup.dragInitDone && + this.options.drag && + this.isValidDrag(e)) { + rootGroup.dragInit(e, this) + } + }, + isValidDrag: function(e) { + return e.which == 1 || + e.type == "touchstart" && e.originalEvent.touches.length == 1 + }, + searchValidTarget: function (pointer, lastPointer) { + var distances = sortByDistanceDesc(this.getItemDimensions(), + pointer, + lastPointer), + i = distances.length, + rootGroup = this.rootGroup, + validTarget = !rootGroup.options.isValidTarget || + rootGroup.options.isValidTarget(rootGroup.item, this) + + if(!i && validTarget){ + rootGroup.movePlaceholder(this, this.target, "append") + return true + } else + while(i--){ + var index = distances[i][0], + distance = distances[i][1] + if(!distance && this.hasChildGroup(index)){ + var found = this.getContainerGroup(index).searchValidTarget(pointer, lastPointer) + if(found) + return true + } + else if(validTarget){ + this.movePlaceholder(index, pointer) + return true + } + } + }, + movePlaceholder: function (index, pointer) { + var item = $(this.items[index]), + dim = this.itemDimensions[index], + method = "after", + width = item.outerWidth(), + height = item.outerHeight(), + offset = item.offset(), + sameResultBox = { + left: offset.left, + right: offset.left + width, + top: offset.top, + bottom: offset.top + height + } + if(this.options.vertical){ + var yCenter = (dim[2] + dim[3]) / 2, + inUpperHalf = pointer.top <= yCenter + if(inUpperHalf){ + method = "before" + sameResultBox.bottom -= height / 2 + } else + sameResultBox.top += height / 2 + } else { + var xCenter = (dim[0] + dim[1]) / 2, + inLeftHalf = pointer.left <= xCenter + if(inLeftHalf){ + method = "before" + sameResultBox.right -= width / 2 + } else + sameResultBox.left += width / 2 + } + if(this.hasChildGroup(index)) + sameResultBox = emptyBox + this.rootGroup.movePlaceholder(this, item, method, sameResultBox) + }, + getItemDimensions: function () { + if(!this.itemDimensions){ + this.items = this.$getChildren(this.el, "item").filter( + ":not(." + this.group.options.placeholderClass + ", ." + this.group.options.draggedClass + ")" + ).get() + setDimensions(this.items, this.itemDimensions = [], this.options.tolerance) + } + return this.itemDimensions + }, + getItemOffsetParent: function () { + var offsetParent, + el = this.el + // Since el might be empty we have to check el itself and + // can not do something like el.children().first().offsetParent() + if(el.css("position") === "relative" || el.css("position") === "absolute" || el.css("position") === "fixed") + offsetParent = el + else + offsetParent = el.offsetParent() + return offsetParent + }, + hasChildGroup: function (index) { + return this.options.nested && this.getContainerGroup(index) + }, + getContainerGroup: function (index) { + var childGroup = $.data(this.items[index], subContainerKey) + if( childGroup === undefined){ + var childContainers = this.$getChildren(this.items[index], "container") + childGroup = false + + if(childContainers[0]){ + var options = $.extend({}, this.options, { + rootGroup: this.rootGroup, + group: groupCounter ++ + }) + childGroup = childContainers[pluginName](options).data(pluginName).group + } + $.data(this.items[index], subContainerKey, childGroup) + } + return childGroup + }, + $getChildren: function (parent, type) { + var options = this.rootGroup.options, + path = options[type + "Path"], + selector = options[type + "Selector"] + + parent = $(parent) + if(path) + parent = parent.find(path) + + return parent.children(selector) + }, + _serialize: function (parent, isContainer) { + var that = this, + childType = isContainer ? "item" : "container", + + children = this.$getChildren(parent, childType).not(this.options.exclude).map(function () { + return that._serialize($(this), !isContainer) + }).get() + + return this.rootGroup.options.serialize(parent, children, isContainer) + }, + traverse: function(callback) { + $.each(this.items || [], function(item){ + var group = $.data(this, subContainerKey) + if(group) + group.traverse(callback) + }); + + callback(this) + }, + _clearDimensions: function () { + this.itemDimensions = undefined + }, + _destroy: function() { + var that = this; + + this.target.off(eventNames.start, this.handle); + this.el.removeData(pluginName) + + if(this.options.drop) + this.group.containers = $.grep(this.group.containers, function(val){ + return val != that + }) + + $.each(this.items || [], function(){ + $.removeData(this, subContainerKey) + }) + } + } + + var API = { + enable: function() { + this.traverse(function(object){ + object.disabled = false + }) + }, + disable: function (){ + this.traverse(function(object){ + object.disabled = true + }) + }, + serialize: function () { + return this._serialize(this.el, true) + }, + refresh: function() { + this.traverse(function(object){ + object._clearDimensions() + }) + }, + destroy: function () { + this.traverse(function(object){ + object._destroy(); + }) + } + } + + $.extend(Container.prototype, API) + + /** + * jQuery API + * + * Parameters are + * either options on init + * or a method name followed by arguments to pass to the method + */ + $.fn[pluginName] = function(methodOrOptions) { + var args = Array.prototype.slice.call(arguments, 1) + + return this.map(function(){ + var $t = $(this), + object = $t.data(pluginName) + + if(object && API[methodOrOptions]) + return API[methodOrOptions].apply(object, args) || this + else if(!object && (methodOrOptions === undefined || + typeof methodOrOptions === "object")) + $t.data(pluginName, new Container($t, methodOrOptions)) + + return this + }); + }; + + }(jQuery, window, 'sortable'); \ No newline at end of file diff --git a/ChangeLog b/ChangeLog index 005b4fd..984d1d2 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,3 +1,12 @@ +Version 0.3.5 +Improvements: + +* ability to organize pages into sub pages + +Bug fixes: + +* Fix https://github.com/funktechno/kanboard-plugin-wiki/issues/11 + Version 0.3.4 Improvements: diff --git a/Controller/WikiAjaxController.php b/Controller/WikiAjaxController.php index 543778e..b0e0bf7 100644 --- a/Controller/WikiAjaxController.php +++ b/Controller/WikiAjaxController.php @@ -15,6 +15,38 @@ */ class WikiAjaxController extends BaseController { + /** + * reorder wiki page list by index and parent id + * @throws \Kanboard\Core\Controller\AccessForbiddenException + * @return void + */ + public function reorder_by_index(){ + $this->checkReusableGETCSRFParam(); + $project_id = $this->request->getIntegerParam('project_id'); + + if (! $project_id || ! $this->request->isAjax()) { + throw new AccessForbiddenException(); + } + + $values = $this->request->getJson(); + + if(!isset($values['src_wiki_id']) || !isset($values['index'])) { + throw new AccessForbiddenException(); + } + + try { + $result = $this->wiki->reorderPagesByIndex($project_id, $values['src_wiki_id'], $values['index'], $values['parent_id'] ?? null); + + if (!$result) { + $this->response->status(400); + } else { + $this->response->status(200); + } + } catch (Exception $e) { + $this->response->html('
    '.$e->getMessage().'
    '); + } + + } /** * reorder for wikipages using src and target page moving src before target */ diff --git a/Controller/WikiController.php b/Controller/WikiController.php index a678395..5845c1c 100755 --- a/Controller/WikiController.php +++ b/Controller/WikiController.php @@ -127,11 +127,22 @@ public function edit(array $values = array(), array $errors = array()) // $values['date_modification'] = date('Y-m-d'); // } + $wikipages = $this->wiki->getWikipages($editwiki['project_id']); + + $wiki_list = array('' => t('None')); + + foreach ($wikipages as $page) { + if (t($wiki_id) != t($page['id'])) { + $wiki_list[$page['id']] = $page['title']; + } + } + // $values['wikipage'] $this->response->html($this->helper->layout->app('wiki:wiki/edit', array( 'wiki_id' => $wiki_id, 'values' => $editwiki, 'errors' => $errors, + 'wiki_list' => $wiki_list, 'title' => t('Edit Wikipage'), ), 'wiki:wiki/sidebar')); } diff --git a/Helper/WikiHelper.php b/Helper/WikiHelper.php index 965dc2e..f8cd6ab 100644 --- a/Helper/WikiHelper.php +++ b/Helper/WikiHelper.php @@ -1,6 +1,6 @@ wiki->getWikipages($project['id']); // return $this->db->table(self::WIKITABLE)->eq('project_id', $project_id)->desc('order')->findAll(); } + /** + * render wiki page html children recursively + * @param mixed $children + * @param mixed $parent_id + * @param mixed $project + * @param mixed $not_editable + * @return string + */ + public function renderChildren($children, $parent_id, $project, $not_editable){ + $html = ''; + return $html; + } // public function doSomething() // { diff --git a/Makefile b/Makefile index d214150..123eb44 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,5 @@ plugin=Wiki -version=0.3.4 +version=0.3.5 all: @ echo "Build archive for plugin ${plugin} version=${version}" @ git archive HEAD --prefix=${plugin}/ --format=zip -o ${plugin}-${version}.zip diff --git a/Model/Wiki.php b/Model/Wiki.php index 719e6bb..d9c2b0c 100755 --- a/Model/Wiki.php +++ b/Model/Wiki.php @@ -52,6 +52,76 @@ public function getEditions($wiki_id) const EVENT_UPDATE = 'wikipage.update'; const EVENT_CREATE = 'wikipage.create'; const EVENT_DELETE = 'wikipage.delete'; + + + /** + * retrieve wikipages by parent id + * @param mixed $project_id + * @param mixed $parent_id + * @return mixed + */ + function getWikiPagesByParentId($project_id, $parent_id){ + if(isset ($parent_id)){ + return $this->db + ->table(self::WIKITABLE) + ->columns( + 'c.name as creator_name', + 'c.username as creator_username', + 'mod.name as modifier_name', + 'mod.username as modifier_username', + self::WIKITABLE . '.id', + self::WIKITABLE . '.title', + self::WIKITABLE . '.parent_id', + self::WIKITABLE . '.content', + self::WIKITABLE . '.project_id', + self::WIKITABLE . '.is_active', + self::WIKITABLE . '.ordercolumn', + self::WIKITABLE . '.creator_id', + self::WIKITABLE . '.date_creation', + self::WIKITABLE . '.date_modification', + self::WIKITABLE . '.editions', + self::WIKITABLE . '.current_edition', + self::WIKITABLE . '.modifier_id' + ) + ->left(UserModel::TABLE, 'c', 'id', self::WIKITABLE, 'creator_id') + ->left(UserModel::TABLE, 'mod', 'id', self::WIKITABLE, 'modifier_id') + ->eq('project_id', $project_id) + ->eq('parent_id', $parent_id) + ->asc('parent_id') + ->asc('ordercolumn') + ->findAll(); + } + else { + return $this->db + ->table(self::WIKITABLE) + ->columns( + 'c.name as creator_name', + 'c.username as creator_username', + 'mod.name as modifier_name', + 'mod.username as modifier_username', + self::WIKITABLE . '.id', + self::WIKITABLE . '.title', + self::WIKITABLE . '.parent_id', + self::WIKITABLE . '.content', + self::WIKITABLE . '.project_id', + self::WIKITABLE . '.is_active', + self::WIKITABLE . '.ordercolumn', + self::WIKITABLE . '.creator_id', + self::WIKITABLE . '.date_creation', + self::WIKITABLE . '.date_modification', + self::WIKITABLE . '.editions', + self::WIKITABLE . '.current_edition', + self::WIKITABLE . '.modifier_id' + ) + ->left(UserModel::TABLE, 'c', 'id', self::WIKITABLE, 'creator_id') + ->left(UserModel::TABLE, 'mod', 'id', self::WIKITABLE, 'modifier_id') + ->eq('project_id', $project_id) + ->isNull('parent_id') + ->asc('parent_id') + ->asc('ordercolumn') + ->findAll(); + } + } /** * Get all Wiki Pages by order for a project * @@ -73,6 +143,7 @@ public function getWikipages($project_id) // UserModel::TABLE . '.username as modifier_username', self::WIKITABLE . '.id', self::WIKITABLE . '.title', + self::WIKITABLE . '.parent_id', self::WIKITABLE . '.content', self::WIKITABLE . '.project_id', self::WIKITABLE . '.is_active', @@ -89,7 +160,9 @@ public function getWikipages($project_id) ->left(UserModel::TABLE, 'c', 'id', self::WIKITABLE, 'creator_id') ->left(UserModel::TABLE, 'mod', 'id', self::WIKITABLE, 'modifier_id') ->eq('project_id', $project_id) - ->asc('ordercolumn')->findAll(); + ->asc('parent_id') + ->asc('ordercolumn') + ->findAll(); // return $this->db->table(self::TABLE) // ->columns(self::TABLE.'.*', UserModel::TABLE.'.username AS owner_username', UserModel::TABLE.'.name AS owner_name') @@ -98,6 +171,62 @@ public function getWikipages($project_id) // ->findOne(); } + public function reorderPagesByIndex($project_id, $src_wiki_id, $index, $parent_id){ + // retrieve wiki pages + $wikiPages = $this->getWikiPagesByParentId($project_id, $parent_id); + + // echo json_encode($wikiPages), true; + + // echo "project_id: " . $project_id . " src_wiki_id: " . $src_wiki_id . " index: " . + $index . " parent_id: " . $parent_id ." count list: " . count($wikiPages) . "
    "; + // change order of each in for loop, move matching id to one before target + $orderColumn = 0; + $oldSourceColumn = 1; + $oldParentId = 0; + // disable save if list is empty + if(empty($wikiPages)){ + return false; + } + for ($i=0; $i < count($wikiPages); $i++) { + $oldOrderColumn = $wikiPages[$i]['ordercolumn']; + $id = $wikiPages[$i]['id']; + // increment by 1 if index matches + if($orderColumn == $index && $id != $src_wiki_id){ + // add additional order column + $orderColumn++; + } + // echo " id: " . $id . " oldOrderColumn: " . $oldOrderColumn . " orderColumn: " . $orderColumn . "
    "; + + if ($id == $src_wiki_id) { + $oldSourceColumn = $oldOrderColumn; + $oldParentId = $wikiPages[$i]['parent_id']; + } else { + if ($oldOrderColumn != $orderColumn) { + // echo "updating ". $id ." column to ". $orderColumn . "
    "; + $result = $this->savePagePosition($id, $orderColumn, $wikiPages[$i]['parent_id'] ?? null); + if(!$result){ + // echo "Error: ". $id ." column not updated to ". $orderColumn . "
    "; + return false; + } + } + } + $orderColumn++; + } + + // update moved src + // echo "oldSourceColumn: " . $oldSourceColumn . " index: " . $index . "
    "; + if($oldSourceColumn != $index || $oldParentId != $parent_id){ + // echo "updating src ". $src_wiki_id . " column to ". $targetColumn -1 . "
    "; + $result = $this->savePagePosition($src_wiki_id, $index, $parent_id ?? null); + if(!$result){ + // echo "Error: ". $src_wiki_id ." column not updated to ". $index . "
    "; + return false; + } + } + + return true; + } + public function reorderPages($project_id, $src_wiki_id, $target_wiki_id){ // retrieve wiki pages $wikiPages = $this->getWikipages($project_id); @@ -109,6 +238,7 @@ public function reorderPages($project_id, $src_wiki_id, $target_wiki_id){ $orderColumn = 1; $targetColumn = 1; $oldSourceColumn = 1; + $oldParentId = 0; for ($i=0; $i < count($wikiPages); $i++) { $oldOrderColumn = $wikiPages[$i]['ordercolumn']; $id = $wikiPages[$i]['id']; @@ -121,10 +251,11 @@ public function reorderPages($project_id, $src_wiki_id, $target_wiki_id){ if ($id == $src_wiki_id) { $oldSourceColumn = $oldOrderColumn; + $oldParentId = $wikiPages[$i]['parent_id']; } else { if ($oldOrderColumn != $orderColumn) { // echo "updating ". $id ." column to ". $orderColumn . "
    "; - $result = $this->savePagePosition($id, $orderColumn); + $result = $this->savePagePosition($id, $orderColumn, $wikiPages[$i]['parent_id']); if(!$result){ return false; } @@ -138,7 +269,7 @@ public function reorderPages($project_id, $src_wiki_id, $target_wiki_id){ // echo "oldSourceColumn: " . $oldSourceColumn . " targetColumn: " . $targetColumn . "
    "; if($oldSourceColumn != $targetColumn){ // echo "updating src ". $src_wiki_id . " column to ". $targetColumn -1 . "
    "; - $result = $this->savePagePosition($src_wiki_id, $targetColumn); + $result = $this->savePagePosition($src_wiki_id, $targetColumn, $oldParentId); if(!$result){ return false; } @@ -146,10 +277,17 @@ public function reorderPages($project_id, $src_wiki_id, $target_wiki_id){ return true; } - - public function savePagePosition($wiki_id, $orderColumn) { + /** + * update page position and parent id + * @param mixed $wiki_id + * @param mixed $orderColumn + * @param mixed $parent_id + * @return bool + */ + public function savePagePosition($wiki_id, $orderColumn, $parent_id) { $result = $this->db->table(self::WIKITABLE)->eq('id', $wiki_id)->update(array( - 'ordercolumn' => $orderColumn + 'ordercolumn' => $orderColumn, + 'parent_id' => $parent_id )); if (! $result) { @@ -221,6 +359,7 @@ public function getWikipage($wiki_id) // UserModel::TABLE . '.username as modifier_username', self::WIKITABLE . '.id', self::WIKITABLE . '.title', + self::WIKITABLE . '.parent_id', self::WIKITABLE . '.content', self::WIKITABLE . '.project_id', self::WIKITABLE . '.ordercolumn', @@ -284,8 +423,15 @@ public function updatepage($paramvalues, $editions, $date = '') 'content' => $paramvalues['content'], 'current_edition' => $editions, 'date_modification' => $date ?: date('Y-m-d'), + // 'parent_id' => $paramvalues['parent_id'] ]; + if(isset($paramvalues['parent_id']) && $paramvalues['parent_id'] != '') { + $values['parent_id'] = $paramvalues['parent_id']; + } else { + $values['parent_id'] = null; + } + if ($this->userSession->isLogged()) { $values['modifier_id'] = $this->userSession->getId(); } diff --git a/Plugin.php b/Plugin.php index 1911310..52636c2 100755 --- a/Plugin.php +++ b/Plugin.php @@ -48,6 +48,7 @@ public function initialize() $this->template->setTemplateOverride('file_viewer/show', 'wiki:file_viewer/show'); $this->hook->on('template:layout:css', array('template' => 'plugins/Wiki/Asset/css/wiki.css')); + $this->hook->on('template:layout:js', array('template' => 'plugins/Wiki/Asset/vendor/jquery-sortable/jquery-sortable.js')); $this->hook->on('template:layout:js', array('template' => 'plugins/Wiki/Asset/Javascript/wiki.js')); @@ -56,7 +57,7 @@ public function initialize() // $this->layout->register('wiki', '\Kanboard\Plugin\Wiki\Helper\layout'); // $this->helper->register('wiki', '\Kanboard\Plugin\Wiki\Helper\layout'); - // $this->helper->register('wikiHelper', '\Kanboard\Plugin\Wiki\Helper\WikiHelper'); + $this->helper->register('wikiHelper', '\Kanboard\Plugin\Wiki\Helper\WikiHelper'); } @@ -96,7 +97,7 @@ public function getPluginAuthor() public function getPluginVersion() { - return '0.3.4'; + return '0.3.5'; } public function getPluginHomepage() diff --git a/README.md b/README.md index a08467a..402583e 100644 --- a/README.md +++ b/README.md @@ -73,12 +73,12 @@ Note that you can only restore **saved** editions. So you if you have the global - [x] ordering - [x] drop down to switch - [x] drag to move, require css magic -- [] subpages and pagination +- [x] subpages and pagination - [x] fix wiki sidebar - use html template render properly to list wiki pages - still having difficulty getting template helper working, manually added for each page - [x] get rid of additional old budget plugin code -- [] kanboard rest api support +- [] kanboard rest api support - by request - [] translations, maybe buttons, won't be translating "Wiki" for most languages - Related issues: [#13](https://github.com/kanboard/kanboard/issues/13), [#12](https://github.com/kanboard/kanboard/issues/12), [#10](https://github.com/kanboard/kanboard/issues/10) - [] active, archived wikipages? diff --git a/Schema/Mysql.php b/Schema/Mysql.php index 33b10c6..77383a4 100644 --- a/Schema/Mysql.php +++ b/Schema/Mysql.php @@ -4,7 +4,14 @@ use PDO; -const VERSION = 9; +const VERSION = 10; + +function version_10(PDO $pdo) +{ + $pdo->exec("ALTER TABLE wikipage ADD COLUMN parent_id int(11) NULL"); + // FK + $pdo->exec("ALTER TABLE wikipage ADD FOREIGN KEY (parent_id) REFERENCES wikipage (id)"); +} /** * Allow unicode emojis in wikipages diff --git a/Schema/Postgres.php b/Schema/Postgres.php index 18955ce..b5dbf6c 100644 --- a/Schema/Postgres.php +++ b/Schema/Postgres.php @@ -4,7 +4,14 @@ use PDO; -const VERSION = 3; +const VERSION = 4; + +function version_4(PDO $pdo) +{ + $pdo->exec("ALTER TABLE wikipage ADD COLUMN parent_id INTEGER NULL"); + // FK + $pdo->exec("ALTER TABLE wikipage ADD FOREIGN KEY (parent_id) REFERENCES wikipage (id)"); +} function version_3(PDO $pdo) { diff --git a/Schema/Sqlite.php b/Schema/Sqlite.php index c292915..d63710f 100644 --- a/Schema/Sqlite.php +++ b/Schema/Sqlite.php @@ -4,7 +4,70 @@ use PDO; -const VERSION = 3; +const VERSION = 4; + +function version_4(PDO $pdo) +{ + $pdo->exec("ALTER TABLE wikipage ADD COLUMN parent_id INTEGER NULL"); + // FK + $pdo->exec(' + PRAGMA foreign_keys = 0; + + CREATE TABLE wikipage_temp_table AS SELECT * FROM wikipage; + DROP TABLE wikipage; + + CREATE TABLE wikipage ( + id INTEGER, + project_id INTEGER NOT NULL, + title VARCHAR (255) NOT NULL, + content TEXT DEFAULT 1, + is_active INT (4) DEFAULT 1, + creator_id INT (11) DEFAULT 0, + modifier_id INT (11), + date_creation INTEGER, + date_modification INTEGER, + ordercolumn INTEGER DEFAULT 1, + editions INTEGER DEFAULT 1, + current_edition INTEGER DEFAULT 1, + parent_id INTEGER REFERENCES wikipage (id), + FOREIGN KEY (project_id) REFERENCES projects (id), + PRIMARY KEY (id AUTOINCREMENT) + ); + + INSERT INTO wikipage ( + id, + project_id, + title, + content, + is_active, + creator_id, + modifier_id, + date_creation, + date_modification, + ordercolumn, + editions, + current_edition, + parent_id + ) + SELECT id, + project_id, + title, + content, + is_active, + creator_id, + modifier_id, + date_creation, + date_modification, + ordercolumn, + editions, + current_edition, + parent_id + FROM wikipage_temp_table; + + DROP TABLE wikipage_temp_table; + + PRAGMA foreign_keys = 1;'); +} function version_3(PDO $pdo) { diff --git a/Template/wiki/detail.php b/Template/wiki/detail.php index 6406c63..6f8e68e 100755 --- a/Template/wiki/detail.php +++ b/Template/wiki/detail.php @@ -33,31 +33,39 @@