Basic CMS for creating a definition and then populating it with content
A Pen by Dominic Watson on CodePen.
Basic CMS for creating a definition and then populating it with content
A Pen by Dominic Watson on CodePen.
<body ng-app="backoffice" ng-controller="AppCtrl as app"> | |
<div class="container"> | |
<!-- Info --> | |
<h1>Angular CMS/Editor</h1> | |
<p><b>Problem:</b> Currently, we create PHP constants, spend some time to create a new space and then deploy it. It's just a block of HTML content and the frontend developer has to try and put that into different places on the page.</p> | |
<p><b>Solution:</b> Make templates within the CMS. The frontend developer can creates templates for the content team to populate, easily. No PHP constants, | |
deployments or dev time needed.</p> | |
<ol> | |
<li ng-class="{ strong: !app.space }">Frontend Developer creates a template</li> | |
<li ng-class="{ strong: app.space }">Content team create a space by populating space previously defined</li> | |
</ol> | |
<hr /> | |
<!-- List of templates --> | |
<div ng-if="!app.space.template"> | |
<h2>Templates</h2> | |
<table class="table table-bordered table-striped"> | |
<thead> | |
<tr> | |
<th>ID</th> | |
<th>Name (slug)</th> | |
<th>Created</th> | |
<th>Updated</th> | |
<th>Fields</th> | |
<th class="text-right">Actions</th> | |
</tr> | |
</thead> | |
<tbody> | |
<tr ng-if="!app.templates.length"> | |
<td colspan="6">There are no templates defined</td> | |
</tr> | |
<tr ng-repeat="template in app.templates"> | |
<td>{{ template.id }}</td> | |
<td>{{ template.name }} ({{template.slug}})</td> | |
<td>{{ template.createdOn | date }}</td> | |
<td>{{ template.updatedOn | date }}</td> | |
<td> | |
<span class="label label-default" title="{{field.type}} - {{field.description}}" ng-repeat="field in template.fields">{{ field.label }}</span> | |
</td> | |
<td class="text-right"> | |
<button ng-click="app.templates.splice($index, 1)" class="btn btn-danger"><i class="fa fa-remove"></i> Delete</button> | |
<button ng-click="app.template = template; app.creatingTemplate = true" class="btn btn-primary"><i class="fa fa-pencil"></i> Edit</button> | |
<button ng-click="app.createSpace(template)" class="btn btn-primary"><i class="fa fa-plus"></i> New space</button> | |
</td> | |
</tr> | |
</tbody> | |
<tfoot> | |
<tr> | |
<td colspan="6"> | |
<div ng-if="app.creatingTemplate"> | |
<form name="tForm" ng-submit="app.saveTemplate(tForm)" novalidate> | |
<!-- Mandatory identifiers --> | |
<fieldset> | |
<legend>Identifiers</legend> | |
<p class="fieldset-info">Fields required for identification and deep-linking within system</p> | |
<div class="field"> | |
<div class="inner"> | |
<div class="row"> | |
<div class="col-xs-3"> | |
<div class="form-group" ng-class="{ 'has-error': (tForm.name.$invalid && tForm.$submitted) || (tForm.name.$invalid && tForm.name.$dirty) }"> | |
<label class="control-label">Name</label> | |
<input type="text" class="form-control" name="name" placeholder="My Template" ng-model="app.template.name" required /> | |
</div> | |
</div> | |
<div class="col-xs-3"> | |
<div class="form-group" ng-class="{ 'has-error': (tForm.slug.$invalid && tForm.$submitted) || (tForm.slug.$invalid && tForm.slug.$dirty) }"> | |
<label class="control-label">Slug</label> | |
<input type="text" class="form-control" name="slug" placeholder="my-template" ng-model="app.template.slug" required /> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</fieldset> | |
<!-- Fields --> | |
<fieldset> | |
<legend>Fields</legend> | |
<div class="field" ng-repeat="field in app.template.fields"> | |
<div class="btn-tabs"> | |
<button type="button" class="btn-tab btn-danger" title="Remove" ng-if="app.template.fields.length > 1" ng-click="app.template.fields.splice($index, 1)"> | |
<i class="fa fa-remove"></i> Kill | |
</button> | |
<button type="button" class="btn-tab btn-primary" title="Add" ng-if="$last" ng-click="app.template.fields.push({ type: 'text', required: true, multiple: false })"> | |
<i class="fa fa-plus"></i> Add | |
</button> | |
</div> | |
<div class="inner"> | |
<div class="row"> | |
<div class="col-xs-3"> | |
<div class="form-group" ng-class="{ 'has-error': (tForm['field-label'].$invalid && tForm.$submitted) || (tForm['field-label'].$invalid && tForm['field-label'].$dirty) }"> | |
<label class="control-label">Label</label> | |
<input type="text" class="form-control" name="field-label" placeholder="My Field" ng-model="app.template.fields[$index].label" required /> | |
</div> | |
</div> | |
<div class="col-xs-3"> | |
<div class="form-group" ng-class="{ 'has-error': (tForm['field-key'].$invalid && tForm.$submitted) || (tForm['field-key'].$invalid && tForm['field-key'].$dirty) }"> | |
<label class="control-label">Key</label> | |
<input type="text" class="form-control" name="field-key" placeholder="my-field" ng-model="app.template.fields[$index].key" required /> | |
</div> | |
</div> | |
<div class="col-xs-2"> | |
<div class="form-group" ng-class="{ 'has-error': (tForm['field-type'].$invalid && tForm.$submitted) || (tForm['field-type'].$invalid && tForm['field-type'].$dirty) }"> | |
<label class="control-label">Type</label> | |
<select class="form-control" name="field-type" ng-model="app.template.fields[$index].type" ng-options="o.value as o.label for o in app.options.types" required></select> | |
</div> | |
</div> | |
<div class="col-xs-2"> | |
<div class="form-group" ng-class="{ 'has-error': (tForm['field-required'].$invalid && tForm.$submitted) || (tForm['field-required'].$invalid && tForm['field-required'].$dirty) }"> | |
<label class="control-label">Required</label> | |
<select class="form-control" name="field-required" ng-model="app.template.fields[$index].required" ng-options="o.value as o.label for o in app.options.booleans" required></select> | |
</div> | |
</div> | |
<div class="col-xs-2"> | |
<div class="form-group" ng-class="{ 'has-error': (tForm['field-multiple'].$invalid && tForm.$submitted) || (tForm['field-multiple'].$invalid && tForm['field-multiple'].$dirty) }"> | |
<label class="control-label">Multiple</label> | |
<select class="form-control" name="field-multiple" ng-model="app.template.fields[$index].multiple" ng-options="o.value as o.label for o in app.options.booleans" required></select> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</fieldset> | |
<hr /> | |
<div class="text-right"> | |
<button type="button" class="btn btn-danger" ng-click="app.creatingTemplate = false">Cancel</button> | |
<button type="submit" class="btn btn-primary" ng-disabled="dForm.$invalid"><i class="fa fa-check"></i> Save template</button> | |
</div> | |
</form> | |
</div> | |
<div class="text-right" ng-if="!app.creatingTemplate"> | |
<button type="button" class="btn btn-primary" ng-click="app.creatingTemplate = !app.creatingTemplate"><i class="fa fa-plus"></i> New template</button> | |
</div> | |
</td> | |
</tr> | |
</tfoot> | |
</table> | |
<!-- List of spaces --> | |
<div ng-if="app.spaces.length"> | |
<hr /> | |
<h2>Spaces</h2> | |
<table class="table table-bordered table-striped"> | |
<thead> | |
<tr> | |
<th>ID</th> | |
<th>Name (slug)</th> | |
<th>Template (slug)</th> | |
<th>Created</th> | |
<th>Updated</th> | |
<th class="text-right">Actions</th> | |
</tr> | |
</thead> | |
<tbody> | |
<tr ng-repeat="space in app.spaces"> | |
<td>{{ space.id }}</td> | |
<td>{{ space.name }} ({{ space.slug }})</td> | |
<td>{{ space.template.name }} ({{ space.template.slug }})</td> | |
<td>{{ space.createdOn | date }}</td> | |
<td>{{ space.updatedOn | date }}</td> | |
<td class="text-right"> | |
<button ng-click="app.spaces.splice($index, 1)" class="btn btn-danger"><i class="fa fa-remove"></i> Delete</button> | |
<button ng-click="app.space = space" class="btn btn-primary"><i class="fa fa-pencil"></i> Edit</button> | |
</td> | |
</tr> | |
</tbody> | |
</table> | |
</div> | |
</div> | |
<!-- Space creation/editing --> | |
<div class="row" ng-if="app.space.template"> | |
<div class="col-md-6"> | |
<h2>{{app.space.template.name}} ({{app.space.template.slug}})</h2> | |
<form name="sForm" ng-submit="app.saveSpace(sForm)" novalidate> | |
<!-- Mandatory fields for identification --> | |
<fieldset> | |
<legend> | |
Identifiers | |
<span class="text-danger" title="Required">*</span> | |
</legend> | |
<p class="fieldset-info">Fields required for identification and deep-linking within system</p> | |
<div class="field"> | |
<div class="inner"> | |
<div class="row"> | |
<div class="col-xs-6"> | |
<div class="form-group" ng-class="{ 'has-error': (sForm.name.$invalid && sForm.$submitted) || (sForm.name.$invalid && sForm.name.$dirty) }"> | |
<label class="control-label">Name</label> | |
<input type="text" class="form-control" name="name" placeholder="My Space" ng-model="app.space.name" required /> | |
</div> | |
</div> | |
<div class="col-xs-6"> | |
<div class="form-group" ng-class="{ 'has-error': (sForm.slug.$invalid && sForm.$submitted) || (sForm.slug.$invalid && sForm.slug.$dirty) }"> | |
<label class="control-label">Slug</label> | |
<input type="text" class="form-control" name="slug" placeholder="my-slug" ng-model="app.space.slug" required /> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</fieldset> | |
<!-- Template data fields --> | |
<fieldset ng-repeat="definition in app.space.template.fields" ng-class="{ 'has-error': (sForm['data-' + definition.key].$invalid && sForm.$submitted) || (sForm['data-' + definition.key].$invalid && sForm['data-' + definition.key].$dirty) }"> | |
<legend> | |
{{ definition.label }} | |
<span class="text-danger" title="Required" ng-if="definition.required !== false">*</span> | |
</legend> | |
<!-- Singular --> | |
<field-data class="field" | |
ng-if="!definition.multiple" | |
data="app.space.data[definition.key]" | |
definition="definition"></field-data> | |
<!-- Multiple: | |
have to pass in data like this or else ng-model doesn't trigger changes: | |
data="app.space.data[definition.key][$index]" | |
--> | |
<field-data class="field" | |
ng-if="definition.multiple" | |
ng-repeat="d in app.space.data[definition.key] track by $index" | |
data="app.space.data[definition.key][$index]" | |
definition="definition" | |
on-add="app.addData(definition, app.space.data[definition.key])" | |
on-remove="app.removeData(definition, $index)" | |
item-length="app.space.data[definition.key].length" | |
last="$last"></field-data> | |
</fieldset> | |
<div class="text-right"> | |
<button type="button" ng-click="app.space = null" class="btn btn-danger">Cancel</button> | |
<button type="submit" class="btn btn-primary"><i class="fa fa-check"></i> Save space</button> | |
</div> | |
</form> | |
<br /> | |
</div> | |
<!-- Debug --> | |
<div class="col-md-6"> | |
<h2>Live Preview</h2> | |
<p>Template HTML/CSS is shared between frontend and CMS for live-previews and only need to be coded once.</p> | |
<template-space data="app.space.data" slug="{{app.space.template.slug}}"></template-space> | |
<h2>Raw JSON</h2> | |
<pre>{{app.space | json}}</pre> | |
</div> | |
</div> | |
</div> | |
</body> | |
<!-- Imported from FE/Common --> | |
<script type="text/ng-template" id="components/template-space/banner.tpl.html"> | |
<div class="carousel banner"> | |
<div class="carousel-inner"> | |
<div class="item active"> | |
<img ng-src="{{tSpace.data.image}}"> | |
<h3 ng-repeat="title in tSpace.data.titles">{{title}}</h3> | |
<div class="bottom"> | |
<button ng-repeat="cta in tSpace.data.ctas" ng-href="{{cta.url}}" class="btn btn-{{cta.size}} btn-{{cta.style}}">{{cta.text}}</button> | |
{{tSpace.title}} | |
</div> | |
<div class="flip-content" ng-bind-html="tSpace.data.flipContent"></div> | |
</div> | |
</div> | |
</div> | |
</script> |
angular | |
.module('backoffice', [ | |
'ngAnimate', | |
'textAngular', | |
'ngFileUpload' | |
]) | |
.controller('AppCtrl', AppCtrl) | |
.directive('fieldData', fieldData) | |
// Imported from FE/Common | |
.directive('templateSpace', templateSpace); | |
function AppCtrl() { | |
var vm = this; | |
vm.creatingTemplate = false; | |
vm.template = { | |
slug: '', | |
name: '', | |
fields: [ | |
{ | |
key: '', | |
label: '', | |
type: 'text', | |
required: true, | |
multiple: false | |
} | |
] | |
}; | |
vm.templates = [ | |
{ | |
"id": 1, | |
"name": "HTML Space", | |
"slug": "html-space", | |
"createdOn": new Date("Sun Feb 07 2016 10:40:38 GMT+0100 (CET)"), | |
"updatedOn": new Date("Sun Feb 08 2016 10:40:38 GMT+0100 (CET)"), | |
"fields": [ | |
{ | |
"key": "title", | |
"type": "text", | |
"label": "Title", | |
"multiple": false, | |
"required": true | |
}, { | |
"key": "content", | |
"type": "html", | |
"label": "Content", | |
"multiple": false, | |
"required": true | |
}, { | |
"key": "ctas", | |
"type": "button", | |
"label": "Calls to action", | |
"multiple": true, | |
"required": true | |
} | |
] | |
}, | |
{ | |
"id": 2, | |
"name": "Banner", | |
"slug": "banner", | |
"createdOn": new Date("Friday Feb 12 2016 10:40:38 GMT+0100 (CET)"), | |
"updatedOn": new Date("Saturday Feb 13 2016 10:40:38 GMT+0100 (CET)"), | |
"fields": [ | |
{ | |
"key": "titles", | |
"type": "text", | |
"label": "Titles", | |
"multiple": true, | |
"required": true | |
}, { | |
"key": "image", | |
"type": "text", | |
"label": "Image URL", | |
"default": "https://i.ytimg.com/vi/tntOCGkgt98/maxresdefault.jpg", | |
"multiple": false, | |
"required": true | |
}, { | |
"key": "flipContent", | |
"type": "html", | |
"label": "Flip Content", | |
"multiple": false, | |
"required": true | |
}, { | |
"key": "ctas", | |
"type": "button", | |
"label": "Calls to action", | |
"multiple": true, | |
"required": true | |
} | |
] | |
} | |
]; | |
vm.options = { | |
types: [ | |
{ | |
value: 'text', | |
label: 'Text' | |
}, { | |
value: 'button', | |
label: 'Button' | |
}, { | |
value: 'html', | |
label: 'HTML' | |
}, { | |
value: 'file', | |
label: 'File' | |
} | |
], | |
booleans: [ | |
{ | |
value: true, | |
label: 'Yes' | |
}, { | |
value: false, | |
label: 'No' | |
} | |
] | |
} | |
vm.spaces = []; | |
vm.space = null; | |
vm.saveTemplate = saveTemplate; | |
vm.createSpace = createSpace; | |
vm.saveSpace = saveSpace; | |
// add/remove of field data | |
vm.addData = addData; | |
vm.removeData = removeData; | |
function saveTemplate(form) { | |
if (form.$valid) { | |
var now = new Date; | |
vm.template.updatedOn = now; | |
if (!vm.template.id) { | |
vm.template.id = vm.templates.length + 1; | |
vm.template.createdOn = new Date; | |
vm.templates.push(vm.template); | |
} | |
vm.creatingTemplate = false; | |
} | |
} | |
function createSpace(template) { | |
// init field data | |
var spaceData = {}; | |
template.fields.forEach(function(definition) { | |
var data = getDefaultData(definition); | |
if (definition.multiple) { | |
spaceData[definition.key] = [data]; | |
} else { | |
spaceData[definition.key] = data; | |
} | |
}); | |
vm.space = { | |
template: template, | |
data: spaceData | |
}; | |
} | |
function saveSpace(form) { | |
if (form.$valid) { | |
var now = new Date; | |
vm.space.updatedOn = now; | |
if (!vm.space.id) { | |
vm.space.id = vm.spaces.length + 1; | |
vm.space.createdOn = now; | |
vm.spaces.push(vm.space); | |
} | |
vm.space = null; | |
} | |
} | |
function getDefaultData(definition) { | |
if (definition.type === 'button') { | |
return { | |
text: definition.default || 'Submit', | |
href: '', | |
size: 'md', | |
style: 'default' | |
}; | |
} | |
return definition.default || ''; | |
} | |
function addData(definition, spaceData) { | |
var defaultData = getDefaultData(definition); | |
spaceData.push(defaultData); | |
} | |
function removeData(definition, index) { | |
vm.space.data[definition.key].splice(index, 1); | |
} | |
} | |
function fieldData() { | |
return { | |
restrict: 'E', | |
bindToController: true, | |
scope: { | |
definition: '=', | |
data: '=', | |
last: '=', | |
itemLength: '=', | |
onAdd: '&', | |
onRemove: '&' | |
}, | |
controllerAs: 'field', | |
controller: SpaceFieldController, | |
template: ` | |
<div class="btn-tabs"> | |
<button type="button" class="btn-tab btn-danger" title="Remove" ng-if="field.itemLength > 1" ng-click="field.onRemove()"> | |
<i class="fa fa-remove"></i> Kill | |
</button> | |
<button type="button" class="btn-tab btn-primary" title="Add" ng-if="field.last" ng-click="field.onAdd()"> | |
<i class="fa fa-plus"></i> Add | |
</button> | |
</div> | |
<div class="inner" ng-switch on="field.definition.type"> | |
<div class="form-group" ng-switch-when="text"> | |
<input type="text" class="form-control" name="data-{{field.definition.key}}" placeholder="Some text content" ng-required="field.definition.required !== false" ng-model="field.data" /> | |
</div> | |
<div class="form-group" ng-switch-when="file"> | |
<section class="draggable" ngf-max-size="{{field.maxUploadSize}}" ngf-pattern="'{{field.allowedFiletypes}}'" ng-model="field.data" ngf-drop="field.uploadFiles($files);" ngf-drag-over-class="'drag-over'"> | |
<div class="drop-txt off" translate>Drag file here to upload</div> | |
<div class="drop-txt on" translate>Drop file to upload</div> | |
<div class="or">or</div> | |
<div> | |
<button type="button" name="data-{{field.definition.key}}" class="btn btn-primary" ngf-select="field.uploadFiles($files);" ng-required="field.definition.required !== false">Select files on your computer</button> | |
</div> | |
</section> | |
<section class="uploading" ng-show="field.thinking"> | |
<div class="col-md-3"> | |
<i class="fa fa-2x fa-file-image-o"></i> | |
</div> | |
<div class="col-md-6 text-center"> | |
<i class="fa fa-2x fa-circle-o-notch fa-spin"></i> <span>uploading</span> | |
</div> | |
<div class="col-md-3"> | |
<button class="btn btn-sm btn-block btn-default">Cancel</button> | |
</div> | |
</section> | |
</div> | |
<div class="form-group" ng-switch-when="html"> | |
<text-angular name="data-{{field.definition.key}}" ng-model="field.data" ng-required="field.definition.required !== false"></text-angular> | |
</div> | |
<div ng-switch-when="button"> | |
<!-- Text and URL --> | |
<div class="row"> | |
<div class="col-xs-4"> | |
<div class="form-group"> | |
<label>Text</label> | |
<input type="text" class="form-control" name="data-{{field.definition.key}}" ng-model="field.data.text" ng-required="field.definition.required !== false"> | |
</div> | |
</div> | |
<div class="col-xs-8"> | |
<div class="form-group"> | |
<label>URL</label> | |
<input type="url" class="form-control" name="data-{{field.definition.key}}" ng-model="field.data.href" placeholder="https://www.url.com" ng-required="field.definition.required !== false"> | |
</div> | |
</div> | |
</div> | |
<!-- Style and size --> | |
<div class="row"> | |
<div class="col-xs-4"> | |
<div class="form-group"> | |
<label>Size</label> | |
<div> | |
<div class="btn-group"> | |
<button type="button" | |
ng-repeat="size in field.sizes" | |
ng-click="field.data.size = size.key" | |
ng-class="{ active: field.data.size == size.key }" | |
title="{{size.title}}" | |
class="btn btn-sm btn-default">{{size.name}}</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
<div class="col-xs-8"> | |
<div class="form-group"> | |
<label>Style</label> | |
<div> | |
<div class="btn-group"> | |
<button type="button" | |
ng-repeat="style in field.styles" | |
ng-click="field.data.style = style.key" | |
ng-class="{ active: field.data.style == style.key }" | |
title="{{style.title}}" | |
class="btn btn-sm btn-{{style.key}}">{{style.name}}</button> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
</div> | |
` | |
}; | |
} | |
function SpaceFieldController($scope, $timeout) { | |
var vm = this; | |
vm.maxUploadSize = 5000; | |
vm.allowedFiletypes = '.jpg, .jpeg, .png, .gif'; | |
vm.sizes = [ | |
{ | |
key: 'xs', | |
title: 'Extra Small', | |
name: 'XS' | |
}, { | |
key: 'sm', | |
title: 'Small', | |
name: 'SM' | |
}, { | |
key: 'md', | |
title: 'Medium', | |
name: 'MD' | |
}, { | |
key: 'lg', | |
title: 'Large', | |
name: 'LG' | |
} | |
]; | |
vm.styles = [ | |
{ | |
key: 'default', | |
name: 'Default', | |
title: 'Default' | |
}, { | |
key: 'primary', | |
name: 'Pri', | |
title: 'Primary' | |
}, { | |
key: 'success', | |
name: 'Suc', | |
title: 'Success' | |
}, { | |
key: 'info', | |
name: 'Info', | |
title: 'Info' | |
}, { | |
key: 'warning', | |
name: 'Warn', | |
title: 'Warning' | |
}, { | |
key: 'danger', | |
name: 'Dan', | |
title: 'Danger' | |
}, { | |
key: 'link', | |
name: 'Link' | |
} | |
]; | |
vm.uploadFiles = uploadFiles; | |
function uploadFiles($files) { | |
console.log('TODO: Upload', $files); | |
} | |
} | |
function templateSpace() { | |
return { | |
restrict: 'E', | |
templateUrl: function(elem, attrs) { | |
// Harcoded for now... attrs.slug doesn't get compiled yet :S | |
return 'components/template-space/banner.tpl.html'; | |
// return 'components/template-space/' + attrs.slug + '.tpl.html'; | |
}, | |
scope: { | |
slug: '@', | |
data: '=?' | |
}, | |
controller: TemplateSpaceCtrl, | |
controllerAs: 'tSpace', | |
bindToController: true | |
}; | |
} | |
/* @ngInject */ | |
function TemplateSpaceCtrl() { | |
var vm = this; | |
} |
<script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.14/angular.min.js"></script> | |
<script src="//cdnjs.cloudflare.com/ajax/libs/angular.js/1.3.14/angular-animate.min.js"></script> | |
<script src="//cdnjs.cloudflare.com/ajax/libs/textAngular/1.5.0/textAngular-rangy.min.js"></script> | |
<script src="//cdnjs.cloudflare.com/ajax/libs/textAngular/1.5.0/textAngular-sanitize.min.js"></script> | |
<script src="//cdnjs.cloudflare.com/ajax/libs/textAngular/1.5.0/textAngular.min.js"></script> | |
<script src="//cdnjs.cloudflare.com/ajax/libs/danialfarid-angular-file-upload/12.0.1/ng-file-upload-shim.min.js"></script> | |
<script src="//cdnjs.cloudflare.com/ajax/libs/danialfarid-angular-file-upload/12.0.1/ng-file-upload.min.js"></script> |
body { padding: 0 50px 50px; } | |
.table { | |
> tbody { | |
> tr { | |
> td { | |
line-height: 35px; | |
} | |
} | |
} | |
> tfoot { | |
form { | |
padding: 20px; | |
} | |
} | |
.label { | |
margin-right: 2px; | |
margin-bottom: 2px; | |
} | |
} | |
.btn-tabs { | |
z-index: 1; | |
position: absolute; | |
top: 10px; | |
right: 0; | |
transition: transform .1s; | |
transform: translateX(8px); | |
} | |
.btn-tab { | |
display: block; | |
width: 100%; | |
margin-bottom: 2px; | |
padding: 5px 8px; | |
border: 0; | |
border-radius: 0 3px 3px 0; | |
text-transform: uppercase; | |
transition: color .1s, transform .1s; | |
&:hover { | |
transform: translateX(5%); | |
} | |
} | |
.field { | |
position: relative; | |
margin: 0 0 3px; | |
transition: opacity .2s, transform .2s; | |
> .inner { | |
z-index: 2; | |
position: relative; | |
padding: 10px 10px 1px; | |
min-height: 80px; | |
border-radius: 0; | |
background: #f4f4f4; | |
} | |
&.ng-enter { | |
opacity: 0; | |
transform: translateY(-50px); | |
&.ng-enter-active { | |
opacity: 1; | |
transform: translateY(0); | |
} | |
} | |
&.ng-leave { | |
opacity: 1; | |
transform: translateY(0); | |
&.ng-leave-active { | |
opacity: 0; | |
transform: translateY(-50px); | |
} | |
} | |
&:hover { | |
.btn-tabs { | |
transform: translateX(95%); | |
} | |
} | |
} | |
.form-group { | |
margin-bottom: 10px; | |
} | |
fieldset { | |
margin-bottom: 10px; | |
} | |
.fieldset-info { | |
margin: -15px 0 20px; | |
color: #9c9c9c; | |
} | |
field-data { | |
display: block; | |
} | |
.strong { | |
font-weight: bold; | |
} | |
.btn-group { | |
box-shadow: 1px 0 5px rgba(0, 0, 0, 0.2); | |
border-radius: 5px; | |
} | |
// Imported from FE/Common | |
template-space { | |
display: block; | |
} | |
.carousel { | |
&.banner { | |
.item { | |
height: 250px; | |
padding: 10px; | |
background: #eee; | |
color: #fff; | |
} | |
h1, h2, h3, h4 { | |
margin: 0 0 5px; | |
} | |
img { | |
z-index: -1; | |
position: absolute; | |
top: 0; | |
left: 0; | |
width: 100%; | |
height: 100%; | |
} | |
.bottom { | |
position: absolute; | |
bottom: 0; | |
left: 0; | |
right: 0; | |
padding: 10px; | |
text-align: right; | |
background: rgba(0, 0, 0, 0.3); | |
} | |
} | |
.btn { | |
margin-left: 5px; | |
} | |
.flip-content { | |
position: absolute; | |
top: 0; | |
right: 0; | |
bottom: 54px; | |
left: 0; | |
padding: 10px; | |
background: rgba(#000, .8); | |
color: #fff; | |
opacity: 0; | |
transition: opacity .1s; | |
} | |
&:hover { | |
.flip-content { | |
opacity: 1; | |
} | |
} | |
} | |
.draggable { | |
border: 5px dashed #ccc; | |
padding: 15px; | |
text-align: center; | |
&.drag-over { | |
border-color: #000; | |
.drop-txt { | |
&.off { | |
display: none; | |
} | |
&.on { | |
display: block; | |
} | |
} | |
} | |
.drop-txt { | |
font-weight: bold; | |
&.on { | |
display: none; | |
} | |
} | |
.or { | |
margin: 5px 0; | |
color: #333; | |
} | |
} |
<link href="//maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css" rel="stylesheet" /> | |
<link href="//cdnjs.cloudflare.com/ajax/libs/textAngular/1.5.0/textAngular.css" rel="stylesheet" /> | |
<link href="//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.5.0/css/font-awesome.min.css" rel="stylesheet" /> |