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" /> |