Fixes a number of issues with drag-n-drop on the Afform GUI palette & canvas.
Forces panels to take up full vertical space so palette never scrolls offscreen.
Compresses tabs above palette to save space.
#afGuiEditor #afGuiEditor-palette {
margin-right: 5px;
+ height: 100%;
}
#afGuiEditor #afGuiEditor-canvas {
margin-left: 5px;
-}
-
-#afGuiEditor .panel-body {
- padding: 5px 12px;
- position: relative;
+ height: 100%;
}
#afGuiEditor fieldset legend {
margin-bottom: 10px;
}
-#afGuiEditor #afGuiEditor-palette-tabs li {
+#afGuiEditor .panel {
+ height: 100%;
+}
+#afGuiEditor .panel-heading {
+ height: 44px;
+ padding: 10px;
+}
+#afGuiEditor .panel-heading ul.nav-tabs {
+ border-bottom: 0 none;
+}
+#afGuiEditor .panel-heading ul.nav-tabs li {
top: 1px;
}
-
-#afGuiEditor #afGuiEditor-palette-tabs li > a {
- padding: 10px 15px;
+#afGuiEditor .panel-heading ul.nav-tabs li.fluid-width-tab {
+ white-space: nowrap;
+ overflow: hidden;
+}
+#afGuiEditor .panel-heading ul.nav-tabs li.active {
+ max-width: 50%;
+}
+#afGuiEditor .panel-heading ul.nav-tabs li > a {
+ padding: 5px 3px 5px 8px;
+ height: 33px;
font-size: 12px;
+ margin: 0;
+}
+
+#afGuiEditor .panel-body {
+ padding: 5px 12px;
+ position: relative;
+ height: calc(100% - 44px);
+ overflow-y: scroll;
+ overflow-x: hidden;
}
#afGuiEditor .af-gui-columns {
}
#afGuiEditor .crm-editable-enabled,
-#afGuiEditor-palette-tabs > li > a > span {
+#afGuiEditor .panel-heading ul.nav-tabs li > a > span {
display: inline-block;
padding: 0 4px !important;
border: 2px solid transparent !important;
left: 0;
padding-left: 15px;
}
-#afGuiEditor:not(.af-gui-dragging) #afGuiEditor-canvas:hover .af-gui-bar {
+#afGuiEditor:not(.af-gui-dragging *) #afGuiEditor-canvas:hover .af-gui-bar {
opacity: 1;
transition: opacity .2s;
}
transition: opacity .1s;
}
+/* Disable menu while dragging */
+body.af-gui-dragging #civicrm-menu {
+ pointer-events: none;
+}
+/* Disable scrollbars while dragging */
+body.af-gui-dragging {
+ overflow-x: hidden;
+ overflow-y: hidden;
+}
+
#afGuiEditor .af-gui-bar .btn.active {
background-color: #b3b3b3;
}
padding: 22px 3px 3px;
min-height: 40px;
display: block;
+ margin-bottom: 10px;
+ margin-top: 10px;
}
#afGuiEditor af-gui-markup,
#afGuiEditor .af-gui-container-type-fieldset {
box-shadow: 0 0 5px #bbbbbb;
+ margin-top: 20px;
+ margin-bottom: 20px;
}
#afGuiEditor .af-gui-container:hover,
-#afGuiEditor.af-gui-dragging .af-gui-container {
+.af-gui-dragging #afGuiEditor .af-gui-container {
border: 2px dashed #757575;
}
#afGuiEditor .af-gui-container.af-gui-dragtarget {
margin-top: 10px;
}
+#afGuiEditor .ui-sortable-helper {
+ height: 20px !important;
+ opacity: .5;
+ overflow: visible;
+}
+#afGuiEditor .ui-sortable-helper > * {
+ background-color: #d5d5d5;
+}
+#afGuiEditor .ui-sortable-helper .af-gui-palette-item {
+ height: 30px;
+ width: 300px;
+ border: 2px dashed #0071bd;
+}
+
#afGuiEditor .af-gui-entity-palette-select-list {
max-height: 400px;
overflow-y: auto;
$(this).removeClass('af-gui-dragtarget');
})
.on('sortstart', '#afGuiEditor', function() {
- $('#afGuiEditor').addClass('af-gui-dragging');
+ $('body').addClass('af-gui-dragging');
})
.on('sortstop', function() {
- $('.af-gui-dragging').removeClass('af-gui-dragging');
+ $('body').removeClass('af-gui-dragging');
$('.af-gui-dragtarget').removeClass('af-gui-dragtarget');
});
});
$scope.saving = false;
$scope.selectedEntityName = null;
this.meta = afGui.meta;
- var editor = this;
+ var editor = this,
+ sortableOptions = {};
this.$onInit = function() {
// Load the current form plus blocks & fields
afGui.resetMeta();
afGui.addMeta(this.data);
initializeForm();
+
+ $timeout(fixEditorHeight);
+ $timeout(editor.adjustTabWidths);
+ $(window)
+ .on('resize.afGuiEditor', fixEditorHeight)
+ .on('resize.afGuiEditor', editor.adjustTabWidths);
+ };
+
+ this.$onDestroy = function() {
+ $(window).off('.afGuiEditor');
};
// Initialize the current form
if (selectTab) {
editor.selectEntity(type + num);
}
+ $timeout(editor.adjustTabWidths);
}
if (meta.fields) {
this.selectEntity = function(entityName) {
$scope.selectedEntityName = entityName;
+ $timeout(editor.adjustTabWidths);
};
this.getEntity = function(entityName) {
return options;
}
+ // Options for ui-sortable in field palette
+ this.getSortableOptions = function(entityName) {
+ if (!sortableOptions[entityName + '']) {
+ sortableOptions[entityName + ''] = {
+ helper: 'clone',
+ appendTo: '#afGuiEditor-canvas-body > af-gui-container',
+ containment: '#afGuiEditor-canvas-body',
+ update: editor.onDrop,
+ items: '> div:not(.disabled)',
+ connectWith: '#afGuiEditor-canvas ' + (entityName ? '[data-entity="' + entityName + '"] > ' : '') + '[ui-sortable]',
+ placeholder: 'af-gui-dropzone',
+ tolerance: 'pointer',
+ scrollSpeed: 8
+ };
+ }
+ return sortableOptions[entityName + ''];
+ };
+
// Validates that a drag-n-drop action is allowed
this.onDrop = function(event, ui) {
var sort = ui.item.sortable;
});
}
});
+
+ // Force editor panels to a fixed height, to avoid palette scrolling offscreen
+ function fixEditorHeight() {
+ var height = $(window).height() - $('#afGuiEditor').offset().top;
+ $('#afGuiEditor').height(Math.floor(height));
+ }
+
+ // Compress tabs on small screens
+ this.adjustTabWidths = function() {
+ $('#afGuiEditor .panel-heading ul.nav-tabs li.active').css('max-width', '');
+ $('#afGuiEditor .panel-heading ul.nav-tabs').each(function() {
+ var remainingSpace = Math.floor($(this).width()) - 1,
+ inactiveTabs = $(this).children('li.fluid-width-tab').not('.active');
+ $(this).children('.active,:not(.fluid-width-tab)').each(function() {
+ remainingSpace -= $(this).width();
+ });
+ if (inactiveTabs.length) {
+ inactiveTabs.css('max-width', Math.floor(remainingSpace / inactiveTabs.length) + 'px');
+ }
+ });
+ };
}
});
<ul class="nav nav-tabs">
<li role="presentation" ng-class="{active: canvasTab === 'layout'}">
<a href ng-click="canvasTab = 'layout'">
+ <i class="crm-i fa-list-alt"></i>
<span>{{:: ts('Form Layout') }}</span>
</a>
</li>
<li role="presentation" ng-class="{active: canvasTab === 'markup'}">
<a href ng-click="canvasTab = 'markup'; updateLayoutHtml()">
+ <i class="crm-i fa-code"></i>
<span>{{:: ts('Markup') }}</span>
</a>
</li>
<div id="afGuiEditor-palette-config" class="panel panel-default">
- <ul id="afGuiEditor-palette-tabs" class="panel-heading nav nav-tabs">
- <li role="presentation" ng-class="{active: selectedEntityName === null}">
+ <div class="panel-heading">
+ <ul id="afGuiEditor-palette-tabs" class="nav nav-tabs">
+ <li role="presentation" class="fluid-width-tab" ng-class="{active: selectedEntityName === null}" title="{{:: ts('Form Settings') }}">
<a href ng-click="editor.selectEntity(null)">
+ <i class="crm-i fa-gear"></i>
<span>{{:: ts('Form Settings') }}</span>
</a>
</li>
- <li role="presentation" ng-repeat="entity in entities" ng-class="{active: selectedEntityName === entity.name}">
+ <li role="presentation" ng-repeat="entity in entities" class="fluid-width-tab" ng-class="{active: selectedEntityName === entity.name}" title="{{ entity.label }}">
<a href ng-click="editor.selectEntity(entity.name)">
- <span ng-if="!entity.loading && editor.allowEntityConfig" crm-ui-editable ng-model="entity.label">{{ entity.label }}</span>
- <span ng-if="!entity.loading && !editor.allowEntityConfig">{{ entity.label }}</span>
+ <i class="crm-i {{:: editor.meta.entities[entity.type].icon }}"></i>
+ <span ng-if="!entity.loading && editor.allowEntityConfig && selectedEntityName === entity.name" crm-ui-editable ng-model="entity.label" ng-change="editor.adjustTabWidths()">{{ entity.label }}</span>
+ <span ng-if="!entity.loading && !(editor.allowEntityConfig && selectedEntityName === entity.name)">{{ entity.label }}</span>
<i ng-if="entity.loading" class="crm-i fa-spin fa-spinner"></i>
</a>
</li>
- <li role="presentation" ng-repeat="(key, searchDisplay) in editor.meta.searchDisplays" ng-class="{active: selectedEntityName === key}">
+ <li role="presentation" ng-repeat="(key, searchDisplay) in editor.meta.searchDisplays" class="fluid-width-tab" ng-class="{active: selectedEntityName === key}" title="{{ searchDisplay.label }}">
<a href ng-click="editor.selectEntity(key)">
+ <i class="crm-i {{:: searchDisplay['type:icon'] }}"></i>
<span>{{ searchDisplay.label }}</span>
</a>
</li>
- <li role="presentation" class="dropdown" ng-if="editor.allowEntityConfig">
- <a href class="dropdown-toggle" data-toggle="dropdown" title="{{ ts('Add Entity') }}">
- <span><i class="crm-i fa-plus"></i></span>
+ <li role="presentation" class="dropdown" ng-if="editor.allowEntityConfig" title="{{:: ts('Add Entity') }}">
+ <a href class="dropdown-toggle" data-toggle="dropdown">
+ <i class="crm-i fa-plus"></i>
</a>
- <ul class="dropdown-menu">
+ <ul class="dropdown-menu dropdown-menu-right">
<li ng-repeat="(entityName, entity) in editor.meta.entities" ng-if="entity.defaults">
<a href ng-click="editor.addEntity(entityName, true)">
<i class="crm-i {{:: entity.icon }}"></i>
</ul>
</li>
</ul>
+ </div>
<div class="panel-body" ng-include="'~/afGuiEditor/config-form.html'" ng-if="selectedEntityName === null"></div>
<div class="panel-body" ng-repeat="entity in entities" ng-if="selectedEntityName === entity.name">
<af-gui-entity entity="entity"></af-gui-entity>
<div class="af-gui-entity-palette-select-list">
<div ng-if="elementList.length">
<label>{{:: ts('Elements') }}</label>
- <div ui-sortable="{update: buildPaletteLists, items: '> div:not(.disabled)', connectWith: '[ui-sortable]', placeholder: 'af-gui-dropzone'}" ui-sortable-update="$ctrl.editor.onDrop" ng-model="elementList">
+ <div ui-sortable="$ctrl.editor.getSortableOptions()" ui-sortable-update="buildPaletteLists" ng-model="elementList">
<div ng-repeat="element in elementList" >
- {{:: elementTitles[$index] }}
+ <div class="af-gui-palette-item">{{:: elementTitles[$index] }}</div>
</div>
</div>
</div>
<div ng-if="blockList.length">
<label>{{:: ts('Blocks') }}</label>
- <div ui-sortable="{update: buildPaletteLists, items: '> div:not(.disabled)', connectWith: '[data-entity=\'' + $ctrl.entity.name + '\'] > [ui-sortable]', placeholder: 'af-gui-dropzone'}" ui-sortable-update="$ctrl.editor.onDrop" ng-model="blockList">
+ <div ui-sortable="$ctrl.editor.getSortableOptions($ctrl.entity.name)" ui-sortable-update="buildPaletteLists" ng-model="blockList">
<div ng-repeat="block in blockList" ng-class="{disabled: blockInUse(block)}">
- {{:: blockTitles[$index] }}
+ <div class="af-gui-palette-item">{{:: blockTitles[$index] }}</div>
</div>
</div>
</div>
<div ng-repeat="fieldGroup in fieldList">
<div ng-if="fieldGroup.fields.length">
<label>{{ fieldGroup.label }}</label>
- <div ui-sortable="{update: buildPaletteLists, items: '> div:not(.disabled)', connectWith: '[data-entity=\'' + fieldGroup.entityName + '\'] > [ui-sortable]', placeholder: 'af-gui-dropzone'}" ui-sortable-update="$ctrl.editor.onDrop" ng-model="fieldGroup.fields">
+ <div ui-sortable="$ctrl.editor.getSortableOptions(fieldGroup.entityName)" ui-sortable-update="buildPaletteLists" ng-model="fieldGroup.fields">
<div ng-repeat="field in fieldGroup.fields" ng-class="{disabled: fieldInUse(field.name)}">
- {{:: getField(fieldGroup.entityType, field.name).label }}
+ <div class="af-gui-palette-item">{{:: getField(fieldGroup.entityType, field.name).label }}</div>
</div>
</div>
</div>
<div class="af-gui-entity-palette-select-list">
<div ng-if="elementList.length">
<label>{{:: ts('Elements') }}</label>
- <div ui-sortable="{update: buildPaletteLists, items: '> div:not(.disabled)', connectWith: '[ui-sortable]', placeholder: 'af-gui-dropzone'}" ui-sortable-update="$ctrl.editor.onDrop" ng-model="elementList">
+ <div ui-sortable="$ctrl.editor.getSortableOptions()" ui-sortable-update="buildPaletteLists" ng-model="elementList">
<div ng-repeat="element in elementList" >
- {{:: elementTitles[$index] }}
+ <div class="af-gui-palette-item">{{:: elementTitles[$index] }}</div>
</div>
</div>
</div>
<div ng-if="blockList.length">
<label>{{:: ts('Blocks') }}</label>
- <div ui-sortable="{update: buildPaletteLists, items: '> div:not(.disabled)', connectWith: '[ui-sortable]', placeholder: 'af-gui-dropzone'}" ui-sortable-update="$ctrl.editor.onDrop" ng-model="blockList">
+ <div ui-sortable="$ctrl.editor.getSortableOptions()" ui-sortable-update="buildPaletteLists" ng-model="blockList">
<div ng-repeat="block in blockList" ng-class="{disabled: blockInUse(block)}">
- {{:: blockTitles[$index] }}
+ <div class="af-gui-palette-item">{{:: blockTitles[$index] }}</div>
</div>
</div>
</div>
<div ng-if="calcFieldList.length">
<label>{{:: ts('Calculated Fields') }}</label>
- <div ui-sortable="{update: buildPaletteLists, items: '> div:not(.disabled)', connectWith: '[ui-sortable]', placeholder: 'af-gui-dropzone'}" ui-sortable-update="$ctrl.editor.onDrop" ng-model="calcFieldList">
+ <div ui-sortable="$ctrl.editor.getSortableOptions()" ui-sortable-update="buildPaletteLists" ng-model="calcFieldList">
<div ng-repeat="field in calcFieldList" ng-class="{disabled: fieldInUse(field.name)}">
- {{:: field.defn.label }}
+ <div class="af-gui-palette-item">{{:: field.defn.label }}></div>
</div>
</div>
</div>
connectWith: '[ui-sortable]',
cancel: 'input,textarea,button,select,option,a,.dropdown-menu',
placeholder: 'af-gui-dropzone',
+ tolerance: 'pointer',
+ scrollSpeed: 8,
containment: '#afGuiEditor-canvas-body'
};