",options:{disabled:!1,create:null},_createWidget:function(e,i){i=t(i||this.defaultElement||this)[0],this.element=t(i),this.uuid=s++,this.eventNamespace="."+this.widgetName+this.uuid,this.bindings=t(),this.hoverable=t(),this.focusable=t(),i!==this&&(t.data(i,this.widgetFullName,this),this._on(!0,this.element,{remove:function(t){t.target===i&&this.destroy()}}),this.document=t(i.style?i.ownerDocument:i.document||i),this.window=t(this.document[0].defaultView||this.document[0].parentWindow)),this.options=t.widget.extend({},this.options,this._getCreateOptions(),e),this._create(),this._trigger("create",null,this._getCreateEventData()),this._init()},_getCreateOptions:t.noop,_getCreateEventData:t.noop,_create:t.noop,_init:t.noop,destroy:function(){this._destroy(),this.element.unbind(this.eventNamespace).removeData(this.widgetFullName).removeData(t.camelCase(this.widgetFullName)),this.widget().unbind(this.eventNamespace).removeAttr("aria-disabled").removeClass(this.widgetFullName+"-disabled "+"ui-state-disabled"),this.bindings.unbind(this.eventNamespace),this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus")},_destroy:t.noop,widget:function(){return this.element},option:function(e,i){var s,n,a,o=e;if(0===arguments.length)return t.widget.extend({},this.options);if("string"==typeof e)if(o={},s=e.split("."),e=s.shift(),s.length){for(n=o[e]=t.widget.extend({},this.options[e]),a=0;s.length-1>a;a++)n[s[a]]=n[s[a]]||{},n=n[s[a]];if(e=s.pop(),1===arguments.length)return void 0===n[e]?null:n[e];n[e]=i}else{if(1===arguments.length)return void 0===this.options[e]?null:this.options[e];o[e]=i}return this._setOptions(o),this},_setOptions:function(t){var e;for(e in t)this._setOption(e,t[e]);return this},_setOption:function(t,e){return this.options[t]=e,"disabled"===t&&(this.widget().toggleClass(this.widgetFullName+"-disabled",!!e),e&&(this.hoverable.removeClass("ui-state-hover"),this.focusable.removeClass("ui-state-focus"))),this},enable:function(){return this._setOptions({disabled:!1})},disable:function(){return this._setOptions({disabled:!0})},_on:function(e,i,s){var n,a=this;"boolean"!=typeof e&&(s=i,i=e,e=!1),s?(i=n=t(i),this.bindings=this.bindings.add(i)):(s=i,i=this.element,n=this.widget()),t.each(s,function(s,o){function r(){return e||a.options.disabled!==!0&&!t(this).hasClass("ui-state-disabled")?("string"==typeof o?a[o]:o).apply(a,arguments):void 0}"string"!=typeof o&&(r.guid=o.guid=o.guid||r.guid||t.guid++);var h=s.match(/^([\w:-]*)\s*(.*)$/),l=h[1]+a.eventNamespace,u=h[2];u?n.delegate(u,l,r):i.bind(l,r)})},_off:function(e,i){i=(i||"").split(" ").join(this.eventNamespace+" ")+this.eventNamespace,e.unbind(i).undelegate(i),this.bindings=t(this.bindings.not(e).get()),this.focusable=t(this.focusable.not(e).get()),this.hoverable=t(this.hoverable.not(e).get())},_delay:function(t,e){function i(){return("string"==typeof t?s[t]:t).apply(s,arguments)}var s=this;return setTimeout(i,e||0)},_hoverable:function(e){this.hoverable=this.hoverable.add(e),this._on(e,{mouseenter:function(e){t(e.currentTarget).addClass("ui-state-hover")},mouseleave:function(e){t(e.currentTarget).removeClass("ui-state-hover")}})},_focusable:function(e){this.focusable=this.focusable.add(e),this._on(e,{focusin:function(e){t(e.currentTarget).addClass("ui-state-focus")},focusout:function(e){t(e.currentTarget).removeClass("ui-state-focus")}})},_trigger:function(e,i,s){var n,a,o=this.options[e];if(s=s||{},i=t.Event(i),i.type=(e===this.widgetEventPrefix?e:this.widgetEventPrefix+e).toLowerCase(),i.target=this.element[0],a=i.originalEvent)for(n in a)n in i||(i[n]=a[n]);return this.element.trigger(i,s),!(t.isFunction(o)&&o.apply(this.element[0],[i].concat(s))===!1||i.isDefaultPrevented())}},t.each({show:"fadeIn",hide:"fadeOut"},function(e,i){t.Widget.prototype["_"+e]=function(s,n,a){"string"==typeof n&&(n={effect:n});var o,r=n?n===!0||"number"==typeof n?i:n.effect||i:e;n=n||{},"number"==typeof n&&(n={duration:n}),o=!t.isEmptyObject(n),n.complete=a,n.delay&&s.delay(n.delay),o&&t.effects&&t.effects.effect[r]?s[e](n):r!==e&&s[r]?s[r](n.duration,n.easing,a):s.queue(function(i){t(this)[e](),a&&a.call(s[0]),i()})}}),t.widget;var a=!1;t(document).mouseup(function(){a=!1}),t.widget("ui.mouse",{version:"1.11.4",options:{cancel:"input,textarea,button,select,option",distance:1,delay:0},_mouseInit:function(){var e=this;this.element.bind("mousedown."+this.widgetName,function(t){return e._mouseDown(t)}).bind("click."+this.widgetName,function(i){return!0===t.data(i.target,e.widgetName+".preventClickEvent")?(t.removeData(i.target,e.widgetName+".preventClickEvent"),i.stopImmediatePropagation(),!1):void 0}),this.started=!1},_mouseDestroy:function(){this.element.unbind("."+this.widgetName),this._mouseMoveDelegate&&this.document.unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate)},_mouseDown:function(e){if(!a){this._mouseMoved=!1,this._mouseStarted&&this._mouseUp(e),this._mouseDownEvent=e;var i=this,s=1===e.which,n="string"==typeof this.options.cancel&&e.target.nodeName?t(e.target).closest(this.options.cancel).length:!1;return s&&!n&&this._mouseCapture(e)?(this.mouseDelayMet=!this.options.delay,this.mouseDelayMet||(this._mouseDelayTimer=setTimeout(function(){i.mouseDelayMet=!0},this.options.delay)),this._mouseDistanceMet(e)&&this._mouseDelayMet(e)&&(this._mouseStarted=this._mouseStart(e)!==!1,!this._mouseStarted)?(e.preventDefault(),!0):(!0===t.data(e.target,this.widgetName+".preventClickEvent")&&t.removeData(e.target,this.widgetName+".preventClickEvent"),this._mouseMoveDelegate=function(t){return i._mouseMove(t)},this._mouseUpDelegate=function(t){return i._mouseUp(t)},this.document.bind("mousemove."+this.widgetName,this._mouseMoveDelegate).bind("mouseup."+this.widgetName,this._mouseUpDelegate),e.preventDefault(),a=!0,!0)):!0}},_mouseMove:function(e){if(this._mouseMoved){if(t.ui.ie&&(!document.documentMode||9>document.documentMode)&&!e.button)return this._mouseUp(e);if(!e.which)return this._mouseUp(e)}return(e.which||e.button)&&(this._mouseMoved=!0),this._mouseStarted?(this._mouseDrag(e),e.preventDefault()):(this._mouseDistanceMet(e)&&this._mouseDelayMet(e)&&(this._mouseStarted=this._mouseStart(this._mouseDownEvent,e)!==!1,this._mouseStarted?this._mouseDrag(e):this._mouseUp(e)),!this._mouseStarted)},_mouseUp:function(e){return this.document.unbind("mousemove."+this.widgetName,this._mouseMoveDelegate).unbind("mouseup."+this.widgetName,this._mouseUpDelegate),this._mouseStarted&&(this._mouseStarted=!1,e.target===this._mouseDownEvent.target&&t.data(e.target,this.widgetName+".preventClickEvent",!0),this._mouseStop(e)),a=!1,!1},_mouseDistanceMet:function(t){return Math.max(Math.abs(this._mouseDownEvent.pageX-t.pageX),Math.abs(this._mouseDownEvent.pageY-t.pageY))>=this.options.distance},_mouseDelayMet:function(){return this.mouseDelayMet},_mouseStart:function(){},_mouseDrag:function(){},_mouseStop:function(){},_mouseCapture:function(){return!0}}),t.widget("ui.sortable",t.ui.mouse,{version:"1.11.4",widgetEventPrefix:"sort",ready:!1,options:{appendTo:"parent",axis:!1,connectWith:!1,containment:!1,cursor:"auto",cursorAt:!1,dropOnEmpty:!0,forcePlaceholderSize:!1,forceHelperSize:!1,grid:!1,handle:!1,helper:"original",items:"> *",opacity:!1,placeholder:!1,revert:!1,scroll:!0,scrollSensitivity:20,scrollSpeed:20,scope:"default",tolerance:"intersect",zIndex:1e3,activate:null,beforeStop:null,change:null,deactivate:null,out:null,over:null,receive:null,remove:null,sort:null,start:null,stop:null,update:null},_isOverAxis:function(t,e,i){return t>=e&&e+i>t},_isFloating:function(t){return/left|right/.test(t.css("float"))||/inline|table-cell/.test(t.css("display"))},_create:function(){this.containerCache={},this.element.addClass("ui-sortable"),this.refresh(),this.offset=this.element.offset(),this._mouseInit(),this._setHandleClassName(),this.ready=!0},_setOption:function(t,e){this._super(t,e),"handle"===t&&this._setHandleClassName()},_setHandleClassName:function(){this.element.find(".ui-sortable-handle").removeClass("ui-sortable-handle"),t.each(this.items,function(){(this.instance.options.handle?this.item.find(this.instance.options.handle):this.item).addClass("ui-sortable-handle")})},_destroy:function(){this.element.removeClass("ui-sortable ui-sortable-disabled").find(".ui-sortable-handle").removeClass("ui-sortable-handle"),this._mouseDestroy();for(var t=this.items.length-1;t>=0;t--)this.items[t].item.removeData(this.widgetName+"-item");return this},_mouseCapture:function(e,i){var s=null,n=!1,a=this;return this.reverting?!1:this.options.disabled||"static"===this.options.type?!1:(this._refreshItems(e),t(e.target).parents().each(function(){return t.data(this,a.widgetName+"-item")===a?(s=t(this),!1):void 0}),t.data(e.target,a.widgetName+"-item")===a&&(s=t(e.target)),s?!this.options.handle||i||(t(this.options.handle,s).find("*").addBack().each(function(){this===e.target&&(n=!0)}),n)?(this.currentItem=s,this._removeCurrentsFromItems(),!0):!1:!1)},_mouseStart:function(e,i,s){var n,a,o=this.options;if(this.currentContainer=this,this.refreshPositions(),this.helper=this._createHelper(e),this._cacheHelperProportions(),this._cacheMargins(),this.scrollParent=this.helper.scrollParent(),this.offset=this.currentItem.offset(),this.offset={top:this.offset.top-this.margins.top,left:this.offset.left-this.margins.left},t.extend(this.offset,{click:{left:e.pageX-this.offset.left,top:e.pageY-this.offset.top},parent:this._getParentOffset(),relative:this._getRelativeOffset()}),this.helper.css("position","absolute"),this.cssPosition=this.helper.css("position"),this.originalPosition=this._generatePosition(e),this.originalPageX=e.pageX,this.originalPageY=e.pageY,o.cursorAt&&this._adjustOffsetFromHelper(o.cursorAt),this.domPosition={prev:this.currentItem.prev()[0],parent:this.currentItem.parent()[0]},this.helper[0]!==this.currentItem[0]&&this.currentItem.hide(),this._createPlaceholder(),o.containment&&this._setContainment(),o.cursor&&"auto"!==o.cursor&&(a=this.document.find("body"),this.storedCursor=a.css("cursor"),a.css("cursor",o.cursor),this.storedStylesheet=t("").appendTo(a)),o.opacity&&(this.helper.css("opacity")&&(this._storedOpacity=this.helper.css("opacity")),this.helper.css("opacity",o.opacity)),o.zIndex&&(this.helper.css("zIndex")&&(this._storedZIndex=this.helper.css("zIndex")),this.helper.css("zIndex",o.zIndex)),this.scrollParent[0]!==this.document[0]&&"HTML"!==this.scrollParent[0].tagName&&(this.overflowOffset=this.scrollParent.offset()),this._trigger("start",e,this._uiHash()),this._preserveHelperProportions||this._cacheHelperProportions(),!s)for(n=this.containers.length-1;n>=0;n--)this.containers[n]._trigger("activate",e,this._uiHash(this));return t.ui.ddmanager&&(t.ui.ddmanager.current=this),t.ui.ddmanager&&!o.dropBehaviour&&t.ui.ddmanager.prepareOffsets(this,e),this.dragging=!0,this.helper.addClass("ui-sortable-helper"),this._mouseDrag(e),!0},_mouseDrag:function(e){var i,s,n,a,o=this.options,r=!1;for(this.position=this._generatePosition(e),this.positionAbs=this._convertPositionTo("absolute"),this.lastPositionAbs||(this.lastPositionAbs=this.positionAbs),this.options.scroll&&(this.scrollParent[0]!==this.document[0]&&"HTML"!==this.scrollParent[0].tagName?(this.overflowOffset.top+this.scrollParent[0].offsetHeight-e.pageY
=0;i--)if(s=this.items[i],n=s.item[0],a=this._intersectsWithPointer(s),a&&s.instance===this.currentContainer&&n!==this.currentItem[0]&&this.placeholder[1===a?"next":"prev"]()[0]!==n&&!t.contains(this.placeholder[0],n)&&("semi-dynamic"===this.options.type?!t.contains(this.element[0],n):!0)){if(this.direction=1===a?"down":"up","pointer"!==this.options.tolerance&&!this._intersectsWithSides(s))break;this._rearrange(e,s),this._trigger("change",e,this._uiHash());break}return this._contactContainers(e),t.ui.ddmanager&&t.ui.ddmanager.drag(this,e),this._trigger("sort",e,this._uiHash()),this.lastPositionAbs=this.positionAbs,!1},_mouseStop:function(e,i){if(e){if(t.ui.ddmanager&&!this.options.dropBehaviour&&t.ui.ddmanager.drop(this,e),this.options.revert){var s=this,n=this.placeholder.offset(),a=this.options.axis,o={};a&&"x"!==a||(o.left=n.left-this.offset.parent.left-this.margins.left+(this.offsetParent[0]===this.document[0].body?0:this.offsetParent[0].scrollLeft)),a&&"y"!==a||(o.top=n.top-this.offset.parent.top-this.margins.top+(this.offsetParent[0]===this.document[0].body?0:this.offsetParent[0].scrollTop)),this.reverting=!0,t(this.helper).animate(o,parseInt(this.options.revert,10)||500,function(){s._clear(e)})}else this._clear(e,i);return!1}},cancel:function(){if(this.dragging){this._mouseUp({target:null}),"original"===this.options.helper?this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper"):this.currentItem.show();for(var e=this.containers.length-1;e>=0;e--)this.containers[e]._trigger("deactivate",null,this._uiHash(this)),this.containers[e].containerCache.over&&(this.containers[e]._trigger("out",null,this._uiHash(this)),this.containers[e].containerCache.over=0)}return this.placeholder&&(this.placeholder[0].parentNode&&this.placeholder[0].parentNode.removeChild(this.placeholder[0]),"original"!==this.options.helper&&this.helper&&this.helper[0].parentNode&&this.helper.remove(),t.extend(this,{helper:null,dragging:!1,reverting:!1,_noFinalSort:null}),this.domPosition.prev?t(this.domPosition.prev).after(this.currentItem):t(this.domPosition.parent).prepend(this.currentItem)),this},serialize:function(e){var i=this._getItemsAsjQuery(e&&e.connected),s=[];return e=e||{},t(i).each(function(){var i=(t(e.item||this).attr(e.attribute||"id")||"").match(e.expression||/(.+)[\-=_](.+)/);i&&s.push((e.key||i[1]+"[]")+"="+(e.key&&e.expression?i[1]:i[2]))}),!s.length&&e.key&&s.push(e.key+"="),s.join("&")},toArray:function(e){var i=this._getItemsAsjQuery(e&&e.connected),s=[];return e=e||{},i.each(function(){s.push(t(e.item||this).attr(e.attribute||"id")||"")}),s},_intersectsWith:function(t){var e=this.positionAbs.left,i=e+this.helperProportions.width,s=this.positionAbs.top,n=s+this.helperProportions.height,a=t.left,o=a+t.width,r=t.top,h=r+t.height,l=this.offset.click.top,u=this.offset.click.left,c="x"===this.options.axis||s+l>r&&h>s+l,d="y"===this.options.axis||e+u>a&&o>e+u,p=c&&d;return"pointer"===this.options.tolerance||this.options.forcePointerForContainers||"pointer"!==this.options.tolerance&&this.helperProportions[this.floating?"width":"height"]>t[this.floating?"width":"height"]?p:e+this.helperProportions.width/2>a&&o>i-this.helperProportions.width/2&&s+this.helperProportions.height/2>r&&h>n-this.helperProportions.height/2},_intersectsWithPointer:function(t){var e="x"===this.options.axis||this._isOverAxis(this.positionAbs.top+this.offset.click.top,t.top,t.height),i="y"===this.options.axis||this._isOverAxis(this.positionAbs.left+this.offset.click.left,t.left,t.width),s=e&&i,n=this._getDragVerticalDirection(),a=this._getDragHorizontalDirection();return s?this.floating?a&&"right"===a||"down"===n?2:1:n&&("down"===n?2:1):!1},_intersectsWithSides:function(t){var e=this._isOverAxis(this.positionAbs.top+this.offset.click.top,t.top+t.height/2,t.height),i=this._isOverAxis(this.positionAbs.left+this.offset.click.left,t.left+t.width/2,t.width),s=this._getDragVerticalDirection(),n=this._getDragHorizontalDirection();return this.floating&&n?"right"===n&&i||"left"===n&&!i:s&&("down"===s&&e||"up"===s&&!e)},_getDragVerticalDirection:function(){var t=this.positionAbs.top-this.lastPositionAbs.top;return 0!==t&&(t>0?"down":"up")},_getDragHorizontalDirection:function(){var t=this.positionAbs.left-this.lastPositionAbs.left;return 0!==t&&(t>0?"right":"left")},refresh:function(t){return this._refreshItems(t),this._setHandleClassName(),this.refreshPositions(),this},_connectWith:function(){var t=this.options;return t.connectWith.constructor===String?[t.connectWith]:t.connectWith},_getItemsAsjQuery:function(e){function i(){r.push(this)}var s,n,a,o,r=[],h=[],l=this._connectWith();if(l&&e)for(s=l.length-1;s>=0;s--)for(a=t(l[s],this.document[0]),n=a.length-1;n>=0;n--)o=t.data(a[n],this.widgetFullName),o&&o!==this&&!o.options.disabled&&h.push([t.isFunction(o.options.items)?o.options.items.call(o.element):t(o.options.items,o.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),o]);for(h.push([t.isFunction(this.options.items)?this.options.items.call(this.element,null,{options:this.options,item:this.currentItem}):t(this.options.items,this.element).not(".ui-sortable-helper").not(".ui-sortable-placeholder"),this]),s=h.length-1;s>=0;s--)h[s][0].each(i);return t(r)},_removeCurrentsFromItems:function(){var e=this.currentItem.find(":data("+this.widgetName+"-item)");this.items=t.grep(this.items,function(t){for(var i=0;e.length>i;i++)if(e[i]===t.item[0])return!1;return!0})},_refreshItems:function(e){this.items=[],this.containers=[this];var i,s,n,a,o,r,h,l,u=this.items,c=[[t.isFunction(this.options.items)?this.options.items.call(this.element[0],e,{item:this.currentItem}):t(this.options.items,this.element),this]],d=this._connectWith();if(d&&this.ready)for(i=d.length-1;i>=0;i--)for(n=t(d[i],this.document[0]),s=n.length-1;s>=0;s--)a=t.data(n[s],this.widgetFullName),a&&a!==this&&!a.options.disabled&&(c.push([t.isFunction(a.options.items)?a.options.items.call(a.element[0],e,{item:this.currentItem}):t(a.options.items,a.element),a]),this.containers.push(a));for(i=c.length-1;i>=0;i--)for(o=c[i][1],r=c[i][0],s=0,l=r.length;l>s;s++)h=t(r[s]),h.data(this.widgetName+"-item",o),u.push({item:h,instance:o,width:0,height:0,left:0,top:0})},refreshPositions:function(e){this.floating=this.items.length?"x"===this.options.axis||this._isFloating(this.items[0].item):!1,this.offsetParent&&this.helper&&(this.offset.parent=this._getParentOffset());var i,s,n,a;for(i=this.items.length-1;i>=0;i--)s=this.items[i],s.instance!==this.currentContainer&&this.currentContainer&&s.item[0]!==this.currentItem[0]||(n=this.options.toleranceElement?t(this.options.toleranceElement,s.item):s.item,e||(s.width=n.outerWidth(),s.height=n.outerHeight()),a=n.offset(),s.left=a.left,s.top=a.top);if(this.options.custom&&this.options.custom.refreshContainers)this.options.custom.refreshContainers.call(this);else for(i=this.containers.length-1;i>=0;i--)a=this.containers[i].element.offset(),this.containers[i].containerCache.left=a.left,this.containers[i].containerCache.top=a.top,this.containers[i].containerCache.width=this.containers[i].element.outerWidth(),this.containers[i].containerCache.height=this.containers[i].element.outerHeight();return this},_createPlaceholder:function(e){e=e||this;var i,s=e.options;s.placeholder&&s.placeholder.constructor!==String||(i=s.placeholder,s.placeholder={element:function(){var s=e.currentItem[0].nodeName.toLowerCase(),n=t("<"+s+">",e.document[0]).addClass(i||e.currentItem[0].className+" ui-sortable-placeholder").removeClass("ui-sortable-helper");return"tbody"===s?e._createTrPlaceholder(e.currentItem.find("tr").eq(0),t("",e.document[0]).appendTo(n)):"tr"===s?e._createTrPlaceholder(e.currentItem,n):"img"===s&&n.attr("src",e.currentItem.attr("src")),i||n.css("visibility","hidden"),n},update:function(t,n){(!i||s.forcePlaceholderSize)&&(n.height()||n.height(e.currentItem.innerHeight()-parseInt(e.currentItem.css("paddingTop")||0,10)-parseInt(e.currentItem.css("paddingBottom")||0,10)),n.width()||n.width(e.currentItem.innerWidth()-parseInt(e.currentItem.css("paddingLeft")||0,10)-parseInt(e.currentItem.css("paddingRight")||0,10)))}}),e.placeholder=t(s.placeholder.element.call(e.element,e.currentItem)),e.currentItem.after(e.placeholder),s.placeholder.update(e,e.placeholder)},_createTrPlaceholder:function(e,i){var s=this;e.children().each(function(){t(" | ",s.document[0]).attr("colspan",t(this).attr("colspan")||1).appendTo(i)})},_contactContainers:function(e){var i,s,n,a,o,r,h,l,u,c,d=null,p=null;for(i=this.containers.length-1;i>=0;i--)if(!t.contains(this.currentItem[0],this.containers[i].element[0]))if(this._intersectsWith(this.containers[i].containerCache)){if(d&&t.contains(this.containers[i].element[0],d.element[0]))continue;d=this.containers[i],p=i}else this.containers[i].containerCache.over&&(this.containers[i]._trigger("out",e,this._uiHash(this)),this.containers[i].containerCache.over=0);if(d)if(1===this.containers.length)this.containers[p].containerCache.over||(this.containers[p]._trigger("over",e,this._uiHash(this)),this.containers[p].containerCache.over=1);else{for(n=1e4,a=null,u=d.floating||this._isFloating(this.currentItem),o=u?"left":"top",r=u?"width":"height",c=u?"clientX":"clientY",s=this.items.length-1;s>=0;s--)t.contains(this.containers[p].element[0],this.items[s].item[0])&&this.items[s].item[0]!==this.currentItem[0]&&(h=this.items[s].item.offset()[o],l=!1,e[c]-h>this.items[s][r]/2&&(l=!0),n>Math.abs(e[c]-h)&&(n=Math.abs(e[c]-h),a=this.items[s],this.direction=l?"up":"down"));if(!a&&!this.options.dropOnEmpty)return;if(this.currentContainer===this.containers[p])return this.currentContainer.containerCache.over||(this.containers[p]._trigger("over",e,this._uiHash()),this.currentContainer.containerCache.over=1),void 0;a?this._rearrange(e,a,null,!0):this._rearrange(e,null,this.containers[p].element,!0),this._trigger("change",e,this._uiHash()),this.containers[p]._trigger("change",e,this._uiHash(this)),this.currentContainer=this.containers[p],this.options.placeholder.update(this.currentContainer,this.placeholder),this.containers[p]._trigger("over",e,this._uiHash(this)),this.containers[p].containerCache.over=1}},_createHelper:function(e){var i=this.options,s=t.isFunction(i.helper)?t(i.helper.apply(this.element[0],[e,this.currentItem])):"clone"===i.helper?this.currentItem.clone():this.currentItem;return s.parents("body").length||t("parent"!==i.appendTo?i.appendTo:this.currentItem[0].parentNode)[0].appendChild(s[0]),s[0]===this.currentItem[0]&&(this._storedCSS={width:this.currentItem[0].style.width,height:this.currentItem[0].style.height,position:this.currentItem.css("position"),top:this.currentItem.css("top"),left:this.currentItem.css("left")}),(!s[0].style.width||i.forceHelperSize)&&s.width(this.currentItem.width()),(!s[0].style.height||i.forceHelperSize)&&s.height(this.currentItem.height()),s},_adjustOffsetFromHelper:function(e){"string"==typeof e&&(e=e.split(" ")),t.isArray(e)&&(e={left:+e[0],top:+e[1]||0}),"left"in e&&(this.offset.click.left=e.left+this.margins.left),"right"in e&&(this.offset.click.left=this.helperProportions.width-e.right+this.margins.left),"top"in e&&(this.offset.click.top=e.top+this.margins.top),"bottom"in e&&(this.offset.click.top=this.helperProportions.height-e.bottom+this.margins.top)},_getParentOffset:function(){this.offsetParent=this.helper.offsetParent();var e=this.offsetParent.offset();return"absolute"===this.cssPosition&&this.scrollParent[0]!==this.document[0]&&t.contains(this.scrollParent[0],this.offsetParent[0])&&(e.left+=this.scrollParent.scrollLeft(),e.top+=this.scrollParent.scrollTop()),(this.offsetParent[0]===this.document[0].body||this.offsetParent[0].tagName&&"html"===this.offsetParent[0].tagName.toLowerCase()&&t.ui.ie)&&(e={top:0,left:0}),{top:e.top+(parseInt(this.offsetParent.css("borderTopWidth"),10)||0),left:e.left+(parseInt(this.offsetParent.css("borderLeftWidth"),10)||0)}},_getRelativeOffset:function(){if("relative"===this.cssPosition){var t=this.currentItem.position();return{top:t.top-(parseInt(this.helper.css("top"),10)||0)+this.scrollParent.scrollTop(),left:t.left-(parseInt(this.helper.css("left"),10)||0)+this.scrollParent.scrollLeft()}}return{top:0,left:0}},_cacheMargins:function(){this.margins={left:parseInt(this.currentItem.css("marginLeft"),10)||0,top:parseInt(this.currentItem.css("marginTop"),10)||0}},_cacheHelperProportions:function(){this.helperProportions={width:this.helper.outerWidth(),height:this.helper.outerHeight()}},_setContainment:function(){var e,i,s,n=this.options;"parent"===n.containment&&(n.containment=this.helper[0].parentNode),("document"===n.containment||"window"===n.containment)&&(this.containment=[0-this.offset.relative.left-this.offset.parent.left,0-this.offset.relative.top-this.offset.parent.top,"document"===n.containment?this.document.width():this.window.width()-this.helperProportions.width-this.margins.left,("document"===n.containment?this.document.width():this.window.height()||this.document[0].body.parentNode.scrollHeight)-this.helperProportions.height-this.margins.top]),/^(document|window|parent)$/.test(n.containment)||(e=t(n.containment)[0],i=t(n.containment).offset(),s="hidden"!==t(e).css("overflow"),this.containment=[i.left+(parseInt(t(e).css("borderLeftWidth"),10)||0)+(parseInt(t(e).css("paddingLeft"),10)||0)-this.margins.left,i.top+(parseInt(t(e).css("borderTopWidth"),10)||0)+(parseInt(t(e).css("paddingTop"),10)||0)-this.margins.top,i.left+(s?Math.max(e.scrollWidth,e.offsetWidth):e.offsetWidth)-(parseInt(t(e).css("borderLeftWidth"),10)||0)-(parseInt(t(e).css("paddingRight"),10)||0)-this.helperProportions.width-this.margins.left,i.top+(s?Math.max(e.scrollHeight,e.offsetHeight):e.offsetHeight)-(parseInt(t(e).css("borderTopWidth"),10)||0)-(parseInt(t(e).css("paddingBottom"),10)||0)-this.helperProportions.height-this.margins.top])
+},_convertPositionTo:function(e,i){i||(i=this.position);var s="absolute"===e?1:-1,n="absolute"!==this.cssPosition||this.scrollParent[0]!==this.document[0]&&t.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,a=/(html|body)/i.test(n[0].tagName);return{top:i.top+this.offset.relative.top*s+this.offset.parent.top*s-("fixed"===this.cssPosition?-this.scrollParent.scrollTop():a?0:n.scrollTop())*s,left:i.left+this.offset.relative.left*s+this.offset.parent.left*s-("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():a?0:n.scrollLeft())*s}},_generatePosition:function(e){var i,s,n=this.options,a=e.pageX,o=e.pageY,r="absolute"!==this.cssPosition||this.scrollParent[0]!==this.document[0]&&t.contains(this.scrollParent[0],this.offsetParent[0])?this.scrollParent:this.offsetParent,h=/(html|body)/i.test(r[0].tagName);return"relative"!==this.cssPosition||this.scrollParent[0]!==this.document[0]&&this.scrollParent[0]!==this.offsetParent[0]||(this.offset.relative=this._getRelativeOffset()),this.originalPosition&&(this.containment&&(e.pageX-this.offset.click.leftthis.containment[2]&&(a=this.containment[2]+this.offset.click.left),e.pageY-this.offset.click.top>this.containment[3]&&(o=this.containment[3]+this.offset.click.top)),n.grid&&(i=this.originalPageY+Math.round((o-this.originalPageY)/n.grid[1])*n.grid[1],o=this.containment?i-this.offset.click.top>=this.containment[1]&&i-this.offset.click.top<=this.containment[3]?i:i-this.offset.click.top>=this.containment[1]?i-n.grid[1]:i+n.grid[1]:i,s=this.originalPageX+Math.round((a-this.originalPageX)/n.grid[0])*n.grid[0],a=this.containment?s-this.offset.click.left>=this.containment[0]&&s-this.offset.click.left<=this.containment[2]?s:s-this.offset.click.left>=this.containment[0]?s-n.grid[0]:s+n.grid[0]:s)),{top:o-this.offset.click.top-this.offset.relative.top-this.offset.parent.top+("fixed"===this.cssPosition?-this.scrollParent.scrollTop():h?0:r.scrollTop()),left:a-this.offset.click.left-this.offset.relative.left-this.offset.parent.left+("fixed"===this.cssPosition?-this.scrollParent.scrollLeft():h?0:r.scrollLeft())}},_rearrange:function(t,e,i,s){i?i[0].appendChild(this.placeholder[0]):e.item[0].parentNode.insertBefore(this.placeholder[0],"down"===this.direction?e.item[0]:e.item[0].nextSibling),this.counter=this.counter?++this.counter:1;var n=this.counter;this._delay(function(){n===this.counter&&this.refreshPositions(!s)})},_clear:function(t,e){function i(t,e,i){return function(s){i._trigger(t,s,e._uiHash(e))}}this.reverting=!1;var s,n=[];if(!this._noFinalSort&&this.currentItem.parent().length&&this.placeholder.before(this.currentItem),this._noFinalSort=null,this.helper[0]===this.currentItem[0]){for(s in this._storedCSS)("auto"===this._storedCSS[s]||"static"===this._storedCSS[s])&&(this._storedCSS[s]="");this.currentItem.css(this._storedCSS).removeClass("ui-sortable-helper")}else this.currentItem.show();for(this.fromOutside&&!e&&n.push(function(t){this._trigger("receive",t,this._uiHash(this.fromOutside))}),!this.fromOutside&&this.domPosition.prev===this.currentItem.prev().not(".ui-sortable-helper")[0]&&this.domPosition.parent===this.currentItem.parent()[0]||e||n.push(function(t){this._trigger("update",t,this._uiHash())}),this!==this.currentContainer&&(e||(n.push(function(t){this._trigger("remove",t,this._uiHash())}),n.push(function(t){return function(e){t._trigger("receive",e,this._uiHash(this))}}.call(this,this.currentContainer)),n.push(function(t){return function(e){t._trigger("update",e,this._uiHash(this))}}.call(this,this.currentContainer)))),s=this.containers.length-1;s>=0;s--)e||n.push(i("deactivate",this,this.containers[s])),this.containers[s].containerCache.over&&(n.push(i("out",this,this.containers[s])),this.containers[s].containerCache.over=0);if(this.storedCursor&&(this.document.find("body").css("cursor",this.storedCursor),this.storedStylesheet.remove()),this._storedOpacity&&this.helper.css("opacity",this._storedOpacity),this._storedZIndex&&this.helper.css("zIndex","auto"===this._storedZIndex?"":this._storedZIndex),this.dragging=!1,e||this._trigger("beforeStop",t,this._uiHash()),this.placeholder[0].parentNode.removeChild(this.placeholder[0]),this.cancelHelperRemoval||(this.helper[0]!==this.currentItem[0]&&this.helper.remove(),this.helper=null),!e){for(s=0;n.length>s;s++)n[s].call(this,t);this._trigger("stop",t,this._uiHash())}return this.fromOutside=!1,!this.cancelHelperRemoval},_trigger:function(){t.Widget.prototype._trigger.apply(this,arguments)===!1&&this.cancel()},_uiHash:function(e){var i=e||this;return{helper:i.helper,placeholder:i.placeholder||t([]),position:i.position,originalPosition:i.originalPosition,offset:i.positionAbs,item:i.currentItem,sender:e?e.element:null}}})});
\ No newline at end of file
diff --git a/resources/assets/js/controllers.js b/resources/assets/js/controllers.js
index 8b3d952be..8f434bf7e 100644
--- a/resources/assets/js/controllers.js
+++ b/resources/assets/js/controllers.js
@@ -400,4 +400,116 @@ module.exports = function (ngApp, events) {
}]);
-};
\ No newline at end of file
+ ngApp.controller('PageTagController', ['$scope', '$http', '$attrs',
+ function ($scope, $http, $attrs) {
+
+ const pageId = Number($attrs.pageId);
+ $scope.tags = [];
+
+ $scope.sortOptions = {
+ handle: '.handle',
+ items: '> tr',
+ containment: "parent",
+ axis: "y"
+ };
+
+ /**
+ * Push an empty tag to the end of the scope tags.
+ */
+ function addEmptyTag() {
+ $scope.tags.push({
+ name: '',
+ value: ''
+ });
+ }
+ $scope.addEmptyTag = addEmptyTag;
+
+ /**
+ * Get all tags for the current book and add into scope.
+ */
+ function getTags() {
+ $http.get('/ajax/tags/get/page/' + pageId).then((responseData) => {
+ $scope.tags = responseData.data;
+ addEmptyTag();
+ });
+ }
+ getTags();
+
+ /**
+ * Set the order property on all tags.
+ */
+ function setTagOrder() {
+ for (let i = 0; i < $scope.tags.length; i++) {
+ $scope.tags[i].order = i;
+ }
+ }
+
+ /**
+ * When an tag changes check if another empty editable
+ * field needs to be added onto the end.
+ * @param tag
+ */
+ $scope.tagChange = function(tag) {
+ let cPos = $scope.tags.indexOf(tag);
+ if (cPos !== $scope.tags.length-1) return;
+
+ if (tag.name !== '' || tag.value !== '') {
+ addEmptyTag();
+ }
+ };
+
+ /**
+ * When an tag field loses focus check the tag to see if its
+ * empty and therefore could be removed from the list.
+ * @param tag
+ */
+ $scope.tagBlur = function(tag) {
+ let isLast = $scope.tags.length - 1 === $scope.tags.indexOf(tag);
+ if (tag.name === '' && tag.value === '' && !isLast) {
+ let cPos = $scope.tags.indexOf(tag);
+ $scope.tags.splice(cPos, 1);
+ }
+ };
+
+ /**
+ * Save the tags to the current page.
+ */
+ $scope.saveTags = function() {
+ setTagOrder();
+ let postData = {tags: $scope.tags};
+ $http.post('/ajax/tags/update/page/' + pageId, postData).then((responseData) => {
+ $scope.tags = responseData.data.tags;
+ addEmptyTag();
+ events.emit('success', responseData.data.message);
+ })
+ };
+
+ /**
+ * Remove a tag from the current list.
+ * @param tag
+ */
+ $scope.removeTag = function(tag) {
+ let cIndex = $scope.tags.indexOf(tag);
+ $scope.tags.splice(cIndex, 1);
+ };
+
+ }]);
+
+};
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/resources/assets/js/directives.js b/resources/assets/js/directives.js
index 97d8a89e2..62557f976 100644
--- a/resources/assets/js/directives.js
+++ b/resources/assets/js/directives.js
@@ -301,6 +301,219 @@ module.exports = function (ngApp, events) {
}
}
- }])
+ }]);
+
+ ngApp.directive('toolbox', [function() {
+ return {
+ restrict: 'A',
+ link: function(scope, elem, attrs) {
+
+ // Get common elements
+ const $buttons = elem.find('[tab-button]');
+ const $content = elem.find('[tab-content]');
+ const $toggle = elem.find('[toolbox-toggle]');
+
+ // Handle toolbox toggle click
+ $toggle.click((e) => {
+ elem.toggleClass('open');
+ });
+
+ // Set an active tab/content by name
+ function setActive(tabName, openToolbox) {
+ $buttons.removeClass('active');
+ $content.hide();
+ $buttons.filter(`[tab-button="${tabName}"]`).addClass('active');
+ $content.filter(`[tab-content="${tabName}"]`).show();
+ if (openToolbox) elem.addClass('open');
+ }
+
+ // Set the first tab content active on load
+ setActive($content.first().attr('tab-content'), false);
+
+ // Handle tab button click
+ $buttons.click(function(e) {
+ let name = $(this).attr('tab-button');
+ setActive(name, true);
+ });
+ }
+ }
+ }]);
+
+ ngApp.directive('autosuggestions', ['$http', function($http) {
+ return {
+ restrict: 'A',
+ link: function(scope, elem, attrs) {
+
+ // Local storage for quick caching.
+ const localCache = {};
+
+ // Create suggestion element
+ const suggestionBox = document.createElement('ul');
+ suggestionBox.className = 'suggestion-box';
+ suggestionBox.style.position = 'absolute';
+ suggestionBox.style.display = 'none';
+ const $suggestionBox = $(suggestionBox);
+
+ // General state tracking
+ let isShowing = false;
+ let currentInput = false;
+ let active = 0;
+
+ // Listen to input events on autosuggest fields
+ elem.on('input', '[autosuggest]', function(event) {
+ let $input = $(this);
+ let val = $input.val();
+ let url = $input.attr('autosuggest');
+ // No suggestions until at least 3 chars
+ if (val.length < 3) {
+ if (isShowing) {
+ $suggestionBox.hide();
+ isShowing = false;
+ }
+ return;
+ };
+
+ let suggestionPromise = getSuggestions(val.slice(0, 3), url);
+ suggestionPromise.then((suggestions) => {
+ if (val.length > 2) {
+ suggestions = suggestions.filter((item) => {
+ return item.toLowerCase().indexOf(val.toLowerCase()) !== -1;
+ }).slice(0, 4);
+ displaySuggestions($input, suggestions);
+ }
+ });
+ });
+
+ // Hide autosuggestions when input loses focus.
+ // Slight delay to allow clicks.
+ elem.on('blur', '[autosuggest]', function(event) {
+ setTimeout(() => {
+ $suggestionBox.hide();
+ isShowing = false;
+ }, 200)
+ });
+
+ elem.on('keydown', '[autosuggest]', function (event) {
+ if (!isShowing) return;
+
+ let suggestionElems = suggestionBox.childNodes;
+ let suggestCount = suggestionElems.length;
+
+ // Down arrow
+ if (event.keyCode === 40) {
+ let newActive = (active === suggestCount-1) ? 0 : active + 1;
+ changeActiveTo(newActive, suggestionElems);
+ }
+ // Up arrow
+ else if (event.keyCode === 38) {
+ let newActive = (active === 0) ? suggestCount-1 : active - 1;
+ changeActiveTo(newActive, suggestionElems);
+ }
+ // Enter key
+ else if (event.keyCode === 13) {
+ let text = suggestionElems[active].textContent;
+ currentInput[0].value = text;
+ currentInput.focus();
+ $suggestionBox.hide();
+ isShowing = false;
+ event.preventDefault();
+ return false;
+ }
+ });
+
+ // Change the active suggestion to the given index
+ function changeActiveTo(index, suggestionElems) {
+ suggestionElems[active].className = '';
+ active = index;
+ suggestionElems[active].className = 'active';
+ }
+
+ // Display suggestions on a field
+ let prevSuggestions = [];
+ function displaySuggestions($input, suggestions) {
+
+ // Hide if no suggestions
+ if (suggestions.length === 0) {
+ $suggestionBox.hide();
+ isShowing = false;
+ prevSuggestions = suggestions;
+ return;
+ }
+
+ // Otherwise show and attach to input
+ if (!isShowing) {
+ $suggestionBox.show();
+ isShowing = true;
+ }
+ if ($input !== currentInput) {
+ $suggestionBox.detach();
+ $input.after($suggestionBox);
+ currentInput = $input;
+ }
+
+ // Return if no change
+ if (prevSuggestions.join() === suggestions.join()) {
+ prevSuggestions = suggestions;
+ return;
+ }
+
+ // Build suggestions
+ $suggestionBox[0].innerHTML = '';
+ for (let i = 0; i < suggestions.length; i++) {
+ var suggestion = document.createElement('li');
+ suggestion.textContent = suggestions[i];
+ suggestion.onclick = suggestionClick;
+ if (i === 0) {
+ suggestion.className = 'active'
+ active = 0;
+ };
+ $suggestionBox[0].appendChild(suggestion);
+ }
+
+ prevSuggestions = suggestions;
+ }
+
+ // Suggestion click event
+ function suggestionClick(event) {
+ let text = this.textContent;
+ currentInput[0].value = text;
+ currentInput.focus();
+ $suggestionBox.hide();
+ isShowing = false;
+ };
+
+ // Get suggestions & cache
+ function getSuggestions(input, url) {
+ let searchUrl = url + '?search=' + encodeURIComponent(input);
+
+ // Get from local cache if exists
+ if (localCache[searchUrl]) {
+ return new Promise((resolve, reject) => {
+ resolve(localCache[input]);
+ });
+ }
+
+ return $http.get(searchUrl).then((response) => {
+ localCache[input] = response.data;
+ return response.data;
+ });
+ }
+
+ }
+ }
+ }]);
+};
+
+
+
+
+
+
+
+
+
+
+
+
+
-};
\ No newline at end of file
diff --git a/resources/assets/js/global.js b/resources/assets/js/global.js
index 9e2b3b8ea..d4fe7020b 100644
--- a/resources/assets/js/global.js
+++ b/resources/assets/js/global.js
@@ -5,9 +5,9 @@ var angular = require('angular');
var ngResource = require('angular-resource');
var ngAnimate = require('angular-animate');
var ngSanitize = require('angular-sanitize');
+require('angular-ui-sortable');
-var ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize']);
-
+var ngApp = angular.module('bookStack', ['ngResource', 'ngAnimate', 'ngSanitize', 'ui.sortable']);
// Global Event System
var Events = {
diff --git a/resources/assets/sass/_buttons.scss b/resources/assets/sass/_buttons.scss
index 9b5a498f6..5bdb0cf28 100644
--- a/resources/assets/sass/_buttons.scss
+++ b/resources/assets/sass/_buttons.scss
@@ -65,6 +65,9 @@ $button-border-radius: 2px;
&:focus, &:active {
outline: 0;
}
+ &:hover {
+ text-decoration: none;
+ }
&.neg {
color: $negative;
}
diff --git a/resources/assets/sass/_forms.scss b/resources/assets/sass/_forms.scss
index 482cf54bd..4b50f6022 100644
--- a/resources/assets/sass/_forms.scss
+++ b/resources/assets/sass/_forms.scss
@@ -239,6 +239,17 @@ div[editor-type="markdown"] .title-input.page-title input[type="text"] {
}
}
+input.outline {
+ border: 0;
+ border-bottom: 2px solid #DDD;
+ border-radius: 0;
+ &:focus, &:active {
+ border: 0;
+ border-bottom: 2px solid #AAA;
+ outline: 0;
+ }
+}
+
#login-form label[for="remember"] {
margin: 0;
}
diff --git a/resources/assets/sass/_pages.scss b/resources/assets/sass/_pages.scss
index e1feccb64..ff1b47cd7 100644
--- a/resources/assets/sass/_pages.scss
+++ b/resources/assets/sass/_pages.scss
@@ -122,9 +122,176 @@
}
}
-h1, h2, h3, h4, h5, h6 {
- &:hover a.link-hook {
- opacity: 1;
- transform: translate3d(0, 0, 0);
+// Attribute form
+.floating-toolbox {
+ background-color: #FFF;
+ border: 1px solid #DDD;
+ right: $-xl*2;
+ z-index: 99;
+ width: 48px;
+ overflow: hidden;
+ align-items: stretch;
+ flex-direction: row;
+ display: flex;
+ transition: width ease-in-out 180ms;
+ margin-top: -1px;
+ &.open {
+ width: 480px;
+ }
+ [toolbox-toggle] i {
+ transition: transform ease-in-out 180ms;
+ }
+ [toolbox-toggle] {
+ transition: background-color ease-in-out 180ms;
+ }
+ &.open [toolbox-toggle] {
+ background-color: rgba(255, 0, 0, 0.29);
+ }
+ &.open [toolbox-toggle] i {
+ transform: rotate(180deg);
+ }
+ > div {
+ flex: 1;
+ position: relative;
+ }
+ .tabs {
+ display: block;
+ border-right: 1px solid #DDD;
+ width: 54px;
+ flex: 0;
+ }
+ .tabs i {
+ color: rgba(0, 0, 0, 0.5);
+ padding: 0;
+ margin: 0;
+ }
+ .tabs > span {
+ display: block;
+ cursor: pointer;
+ padding: $-s $-m;
+ font-size: 13.5px;
+ line-height: 1.6;
+ border-bottom: 1px solid rgba(255, 255, 255, 0.3);
+ }
+ &.open .tabs > span.active {
+ color: #444;
+ background-color: rgba(0, 0, 0, 0.1);
+ }
+ div[tab-content] {
+ padding-bottom: 45px;
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ }
+ div[tab-content] .padded {
+ flex: 1;
+ padding-top: 0;
+ }
+ h4 {
+ font-size: 24px;
+ margin: $-m 0 0 0;
+ padding: 0 $-l $-s $-l;
+ }
+ .tags input {
+ max-width: 100%;
+ width: 100%;
+ min-width: 50px;
+ }
+ .tags td {
+ padding-right: $-s;
+ padding-top: $-s;
+ position: relative;
+ }
+ button.pos {
+ position: absolute;
+ bottom: 0;
+ display: block;
+ width: 100%;
+ padding: $-s;
+ height: 45px;
+ border: 0;
+ margin: 0;
+ box-shadow: none;
+ border-radius: 0;
+ &:hover{
+ box-shadow: none;
+ }
+ }
+ .handle {
+ user-select: none;
+ cursor: move;
+ color: #999;
+ }
+ form {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ overflow-y: scroll;
}
}
+
+[tab-content] {
+ display: none;
+}
+
+.tag-display {
+ margin: $-xl $-xs;
+ border: 1px solid #DDD;
+ min-width: 180px;
+ max-width: 320px;
+ opacity: 0.7;
+ table {
+ width: 100%;
+ margin: 0;
+ padding: 0;
+ }
+ span {
+ color: #666;
+ margin-left: $-s;
+ }
+ .heading {
+ padding: $-xs $-s;
+ color: #444;
+ }
+ td {
+ border: 0;
+ border-bottom: 1px solid #DDD;
+ padding: $-xs $-s;
+ color: #444;
+ }
+ .tag-value {
+ color: #888;
+ }
+ td i {
+ color: #888;
+ }
+ tr:last-child td {
+ border-bottom: none;
+ }
+ .tag {
+ padding: $-s;
+ }
+}
+
+.suggestion-box {
+ position: absolute;
+ background-color: #FFF;
+ border: 1px solid #BBB;
+ box-shadow: $bs-light;
+ list-style: none;
+ z-index: 100;
+ padding: 0;
+ margin: 0;
+ border-radius: 3px;
+ li {
+ display: block;
+ padding: $-xs $-s;
+ border-bottom: 1px solid #DDD;
+ &:last-child {
+ border-bottom: 0;
+ }
+ &.active {
+ background-color: #EEE;
+ }
+ }
+}
\ No newline at end of file
diff --git a/resources/assets/sass/_tables.scss b/resources/assets/sass/_tables.scss
index e6ec76b38..b23db436a 100644
--- a/resources/assets/sass/_tables.scss
+++ b/resources/assets/sass/_tables.scss
@@ -26,6 +26,13 @@ table {
}
}
+table.no-style {
+ td {
+ border: 0;
+ padding: 0;
+ }
+}
+
table.list-table {
margin: 0 -$-xs;
td {
diff --git a/resources/assets/sass/styles.scss b/resources/assets/sass/styles.scss
index d8453b9ed..0a7da179b 100644
--- a/resources/assets/sass/styles.scss
+++ b/resources/assets/sass/styles.scss
@@ -21,6 +21,11 @@
[ng\:cloak], [ng-cloak], .ng-cloak {
display: none !important;
+ user-select: none;
+}
+
+[ng-click] {
+ cursor: pointer;
}
// Jquery Sortable Styles
@@ -201,4 +206,4 @@ $btt-size: 40px;
background-color: $negative;
color: #EEE;
}
-}
\ No newline at end of file
+}
diff --git a/resources/views/base.blade.php b/resources/views/base.blade.php
index 96bc20936..34c64c581 100644
--- a/resources/views/base.blade.php
+++ b/resources/views/base.blade.php
@@ -15,6 +15,7 @@
+
@yield('head')
diff --git a/resources/views/pages/create.blade.php b/resources/views/pages/create.blade.php
deleted file mode 100644
index 2c6403e48..000000000
--- a/resources/views/pages/create.blade.php
+++ /dev/null
@@ -1,17 +0,0 @@
-@extends('base')
-
-@section('head')
-
-@stop
-
-@section('body-class', 'flexbox')
-
-@section('content')
-
-
-
-
- @include('partials/image-manager', ['imageType' => 'gallery', 'uploaded_to' => $draft->id])
-@stop
\ No newline at end of file
diff --git a/resources/views/pages/edit.blade.php b/resources/views/pages/edit.blade.php
index 0ad06fc53..de6051118 100644
--- a/resources/views/pages/edit.blade.php
+++ b/resources/views/pages/edit.blade.php
@@ -9,10 +9,15 @@
@section('content')
-
+
+
@include('partials/image-manager', ['imageType' => 'gallery', 'uploaded_to' => $page->id])
diff --git a/resources/views/pages/form-toolbox.blade.php b/resources/views/pages/form-toolbox.blade.php
new file mode 100644
index 000000000..ae17045d1
--- /dev/null
+++ b/resources/views/pages/form-toolbox.blade.php
@@ -0,0 +1,37 @@
+
+
\ No newline at end of file
diff --git a/resources/views/pages/form.blade.php b/resources/views/pages/form.blade.php
index 7ce9dbfe5..aa05a9014 100644
--- a/resources/views/pages/form.blade.php
+++ b/resources/views/pages/form.blade.php
@@ -41,6 +41,7 @@
@include('form/text', ['name' => 'name', 'placeholder' => 'Page Title'])
+
@if(setting('app-editor') === 'wysiwyg')
\ No newline at end of file
diff --git a/resources/views/partials/custom-styles.blade.php b/resources/views/partials/custom-styles.blade.php
index de324d284..4d3d0a6ef 100644
--- a/resources/views/partials/custom-styles.blade.php
+++ b/resources/views/partials/custom-styles.blade.php
@@ -1,9 +1,9 @@
@if(Setting::get('app-color'))
diff --git a/tests/Auth/AuthTest.php b/tests/Auth/AuthTest.php
index 067840841..306771ed5 100644
--- a/tests/Auth/AuthTest.php
+++ b/tests/Auth/AuthTest.php
@@ -181,7 +181,7 @@ class AuthTest extends TestCase
public function test_user_deletion()
{
$userDetails = factory(\BookStack\User::class)->make();
- $user = $this->getNewUser($userDetails->toArray());
+ $user = $this->getEditor($userDetails->toArray());
$this->asAdmin()
->visit('/settings/users/' . $user->id)
diff --git a/tests/Entity/EntityTest.php b/tests/Entity/EntityTest.php
index eebb0dc36..3bf6a3f2a 100644
--- a/tests/Entity/EntityTest.php
+++ b/tests/Entity/EntityTest.php
@@ -161,8 +161,8 @@ class EntityTest extends TestCase
public function test_entities_viewable_after_creator_deletion()
{
// Create required assets and revisions
- $creator = $this->getNewUser();
- $updater = $this->getNewUser();
+ $creator = $this->getEditor();
+ $updater = $this->getEditor();
$entities = $this->createEntityChainBelongingToUser($creator, $updater);
$this->actingAs($creator);
app('BookStack\Repos\UserRepo')->destroy($creator);
@@ -174,8 +174,8 @@ class EntityTest extends TestCase
public function test_entities_viewable_after_updater_deletion()
{
// Create required assets and revisions
- $creator = $this->getNewUser();
- $updater = $this->getNewUser();
+ $creator = $this->getEditor();
+ $updater = $this->getEditor();
$entities = $this->createEntityChainBelongingToUser($creator, $updater);
$this->actingAs($updater);
app('BookStack\Repos\UserRepo')->destroy($updater);
@@ -198,7 +198,7 @@ class EntityTest extends TestCase
public function test_recently_created_pages_view()
{
- $user = $this->getNewUser();
+ $user = $this->getEditor();
$content = $this->createEntityChainBelongingToUser($user);
$this->asAdmin()->visit('/pages/recently-created')
@@ -207,7 +207,7 @@ class EntityTest extends TestCase
public function test_recently_updated_pages_view()
{
- $user = $this->getNewUser();
+ $user = $this->getEditor();
$content = $this->createEntityChainBelongingToUser($user);
$this->asAdmin()->visit('/pages/recently-updated')
@@ -241,7 +241,7 @@ class EntityTest extends TestCase
public function test_recently_created_pages_on_home()
{
- $entityChain = $this->createEntityChainBelongingToUser($this->getNewUser());
+ $entityChain = $this->createEntityChainBelongingToUser($this->getEditor());
$this->asAdmin()->visit('/')
->seeInElement('#recently-created-pages', $entityChain['page']->name);
}
diff --git a/tests/Entity/PageDraftTest.php b/tests/Entity/PageDraftTest.php
index 2c9a28814..108b7459f 100644
--- a/tests/Entity/PageDraftTest.php
+++ b/tests/Entity/PageDraftTest.php
@@ -32,7 +32,7 @@ class PageDraftTest extends TestCase
->dontSeeInField('html', $addedContent);
$newContent = $this->page->html . $addedContent;
- $newUser = $this->getNewUser();
+ $newUser = $this->getEditor();
$this->pageRepo->saveUpdateDraft($this->page, ['html' => $newContent]);
$this->actingAs($newUser)->visit($this->page->getUrl() . '/edit')
->dontSeeInField('html', $newContent);
@@ -54,7 +54,7 @@ class PageDraftTest extends TestCase
->dontSeeInField('html', $addedContent);
$newContent = $this->page->html . $addedContent;
- $newUser = $this->getNewUser();
+ $newUser = $this->getEditor();
$this->pageRepo->saveUpdateDraft($this->page, ['html' => $newContent]);
$this->actingAs($newUser)
@@ -79,7 +79,7 @@ class PageDraftTest extends TestCase
{
$book = \BookStack\Book::first();
$chapter = $book->chapters->first();
- $newUser = $this->getNewUser();
+ $newUser = $this->getEditor();
$this->actingAs($newUser)->visit('/')
->visit($book->getUrl() . '/page/create')
diff --git a/tests/Entity/TagTests.php b/tests/Entity/TagTests.php
new file mode 100644
index 000000000..0520e1a00
--- /dev/null
+++ b/tests/Entity/TagTests.php
@@ -0,0 +1,146 @@
+defaultTagCount)->make();
+ }
+
+ $page->tags()->saveMany($tags);
+ return $page;
+ }
+
+ public function test_get_page_tags()
+ {
+ $page = $this->getPageWithTags();
+
+ // Add some other tags to check they don't interfere
+ factory(Tag::class, $this->defaultTagCount)->create();
+
+ $this->asAdmin()->get("/ajax/tags/get/page/" . $page->id)
+ ->shouldReturnJson();
+
+ $json = json_decode($this->response->getContent());
+ $this->assertTrue(count($json) === $this->defaultTagCount, "Returned JSON item count is not as expected");
+ }
+
+ public function test_tag_name_suggestions()
+ {
+ // Create some tags with similar names to test with
+ $attrs = collect();
+ $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'country']));
+ $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'color']));
+ $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'city']));
+ $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'county']));
+ $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'planet']));
+ $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'plans']));
+ $page = $this->getPageWithTags($attrs);
+
+ $this->asAdmin()->get('/ajax/tags/suggest/names?search=dog')->seeJsonEquals([]);
+ $this->get('/ajax/tags/suggest/names?search=co')->seeJsonEquals(['color', 'country', 'county']);
+ $this->get('/ajax/tags/suggest/names?search=cou')->seeJsonEquals(['country', 'county']);
+ $this->get('/ajax/tags/suggest/names?search=pla')->seeJsonEquals(['planet', 'plans']);
+ }
+
+ public function test_tag_value_suggestions()
+ {
+ // Create some tags with similar values to test with
+ $attrs = collect();
+ $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'country', 'value' => 'cats']));
+ $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'color', 'value' => 'cattery']));
+ $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'city', 'value' => 'castle']));
+ $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'county', 'value' => 'dog']));
+ $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'planet', 'value' => 'catapult']));
+ $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'plans', 'value' => 'dodgy']));
+ $page = $this->getPageWithTags($attrs);
+
+ $this->asAdmin()->get('/ajax/tags/suggest/values?search=ora')->seeJsonEquals([]);
+ $this->get('/ajax/tags/suggest/values?search=cat')->seeJsonEquals(['cats', 'cattery', 'catapult']);
+ $this->get('/ajax/tags/suggest/values?search=do')->seeJsonEquals(['dog', 'dodgy']);
+ $this->get('/ajax/tags/suggest/values?search=cas')->seeJsonEquals(['castle']);
+ }
+
+ public function test_entity_permissions_effect_tag_suggestions()
+ {
+ $permissionService = $this->app->make(PermissionService::class);
+
+ // Create some tags with similar names to test with and save to a page
+ $attrs = collect();
+ $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'country']));
+ $attrs = $attrs->merge(factory(Tag::class, 5)->make(['name' => 'color']));
+ $page = $this->getPageWithTags($attrs);
+
+ $this->asAdmin()->get('/ajax/tags/suggest?search=co')->seeJsonEquals(['color', 'country']);
+ $this->asEditor()->get('/ajax/tags/suggest?search=co')->seeJsonEquals(['color', 'country']);
+
+ // Set restricted permission the page
+ $page->restricted = true;
+ $page->save();
+ $permissionService->buildJointPermissionsForEntity($page);
+
+ $this->asAdmin()->get('/ajax/tags/suggest?search=co')->seeJsonEquals(['color', 'country']);
+ $this->asEditor()->get('/ajax/tags/suggest?search=co')->seeJsonEquals([]);
+ }
+
+ public function test_entity_tag_updating()
+ {
+ $page = $this->getPageWithTags();
+
+ $testJsonData = [
+ ['name' => 'color', 'value' => 'red'],
+ ['name' => 'color', 'value' => ' blue '],
+ ['name' => 'city', 'value' => 'London '],
+ ['name' => 'country', 'value' => ' England'],
+ ];
+ $testResponseJsonData = [
+ ['name' => 'color', 'value' => 'red'],
+ ['name' => 'color', 'value' => 'blue'],
+ ['name' => 'city', 'value' => 'London'],
+ ['name' => 'country', 'value' => 'England'],
+ ];
+
+ // Do update request
+ $this->asAdmin()->json("POST", "/ajax/tags/update/page/" . $page->id, ['tags' => $testJsonData]);
+ $updateData = json_decode($this->response->getContent());
+ // Check data is correct
+ $testDataCorrect = true;
+ foreach ($updateData->tags as $data) {
+ $testItem = ['name' => $data->name, 'value' => $data->value];
+ if (!in_array($testItem, $testResponseJsonData)) $testDataCorrect = false;
+ }
+ $testMessage = "Expected data was not found in the response.\nExpected Data: %s\nRecieved Data: %s";
+ $this->assertTrue($testDataCorrect, sprintf($testMessage, json_encode($testResponseJsonData), json_encode($updateData)));
+ $this->assertTrue(isset($updateData->message), "No message returned in tag update response");
+
+ // Do get request
+ $this->asAdmin()->get("/ajax/tags/get/page/" . $page->id);
+ $getResponseData = json_decode($this->response->getContent());
+ // Check counts
+ $this->assertTrue(count($getResponseData) === count($testJsonData), "The received tag count is incorrect");
+ // Check data is correct
+ $testDataCorrect = true;
+ foreach ($getResponseData as $data) {
+ $testItem = ['name' => $data->name, 'value' => $data->value];
+ if (!in_array($testItem, $testResponseJsonData)) $testDataCorrect = false;
+ }
+ $testMessage = "Expected data was not found in the response.\nExpected Data: %s\nRecieved Data: %s";
+ $this->assertTrue($testDataCorrect, sprintf($testMessage, json_encode($testResponseJsonData), json_encode($getResponseData)));
+ }
+
+}
diff --git a/tests/Permissions/RestrictionsTest.php b/tests/Permissions/RestrictionsTest.php
index 75d83cbfc..d3830cff7 100644
--- a/tests/Permissions/RestrictionsTest.php
+++ b/tests/Permissions/RestrictionsTest.php
@@ -9,7 +9,7 @@ class RestrictionsTest extends TestCase
public function setUp()
{
parent::setUp();
- $this->user = $this->getNewUser();
+ $this->user = $this->getEditor();
$this->viewer = $this->getViewer();
$this->restrictionService = $this->app[\BookStack\Services\PermissionService::class];
}
diff --git a/tests/TestCase.php b/tests/TestCase.php
index 5d0545b66..4c2893f4e 100644
--- a/tests/TestCase.php
+++ b/tests/TestCase.php
@@ -14,7 +14,10 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
* @var string
*/
protected $baseUrl = 'http://localhost';
+
+ // Local user instances
private $admin;
+ private $editor;
/**
* Creates the application.
@@ -30,6 +33,10 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
return $app;
}
+ /**
+ * Set the current user context to be an admin.
+ * @return $this
+ */
public function asAdmin()
{
if($this->admin === null) {
@@ -39,6 +46,18 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
return $this->actingAs($this->admin);
}
+ /**
+ * Set the current editor context to be an editor.
+ * @return $this
+ */
+ public function asEditor()
+ {
+ if($this->editor === null) {
+ $this->editor = $this->getEditor();
+ }
+ return $this->actingAs($this->editor);
+ }
+
/**
* Quickly sets an array of settings.
* @param $settingsArray
@@ -79,7 +98,7 @@ class TestCase extends Illuminate\Foundation\Testing\TestCase
* @param array $attributes
* @return mixed
*/
- protected function getNewUser($attributes = [])
+ protected function getEditor($attributes = [])
{
$user = factory(\BookStack\User::class)->create($attributes);
$role = \BookStack\Role::getRole('editor');
diff --git a/tests/UserProfileTest.php b/tests/UserProfileTest.php
index 170e7eed1..40ae004e9 100644
--- a/tests/UserProfileTest.php
+++ b/tests/UserProfileTest.php
@@ -33,7 +33,7 @@ class UserProfileTest extends TestCase
public function test_profile_page_shows_created_content_counts()
{
- $newUser = $this->getNewUser();
+ $newUser = $this->getEditor();
$this->asAdmin()->visit('/user/' . $newUser->id)
->see($newUser->name)
@@ -52,7 +52,7 @@ class UserProfileTest extends TestCase
public function test_profile_page_shows_recent_activity()
{
- $newUser = $this->getNewUser();
+ $newUser = $this->getEditor();
$this->actingAs($newUser);
$entities = $this->createEntityChainBelongingToUser($newUser, $newUser);
Activity::add($entities['book'], 'book_update', $entities['book']->id);
@@ -66,7 +66,7 @@ class UserProfileTest extends TestCase
public function test_clicking_user_name_in_activity_leads_to_profile_page()
{
- $newUser = $this->getNewUser();
+ $newUser = $this->getEditor();
$this->actingAs($newUser);
$entities = $this->createEntityChainBelongingToUser($newUser, $newUser);
Activity::add($entities['book'], 'book_update', $entities['book']->id);