├── user-bootstrap ├── .gitignore ├── views │ ├── README.md │ ├── clean.html │ └── index.html ├── assets │ └── css │ │ └── overrides.css ├── data │ └── bootstrap │ │ ├── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.ttf │ │ ├── glyphicons-halflings-regular.woff │ │ └── glyphicons-halflings-regular.svg │ │ └── js │ │ └── bootstrap.min.js └── options.js ├── user-source-ok-demo ├── .gitignore ├── assets │ └── css │ │ ├── user.css │ │ └── hacks.css ├── views │ ├── README.md │ ├── clean.html │ ├── index.html │ ├── lego-layouts.html │ └── lego-layer.html ├── README.md ├── core │ └── routes │ │ └── index.js └── options.js ├── assets ├── i │ ├── close.png │ ├── close_h.png │ ├── layout_1col.png │ ├── layout_2col.png │ ├── layout_3col.png │ └── layout_default.png ├── js │ ├── global.js │ ├── virtualblock.js │ ├── client-templates.js │ ├── components.js │ ├── search.js │ ├── main.js │ └── modifiers.js └── css │ └── lego.css ├── .gitignore ├── core ├── routes │ ├── index.js │ └── redirects.js ├── getView.js ├── loadOptions.js └── css-mod │ └── index.js ├── views ├── clean.html ├── index.html ├── lego-layer.html ├── lego-layouts.html ├── client-templates.html └── lego-сontainer.html ├── options.js ├── parser ├── package.json └── bootstrap.js ├── package.json ├── README.md └── lego.js /user-bootstrap/.gitignore: -------------------------------------------------------------------------------- 1 | /saved -------------------------------------------------------------------------------- /user-source-ok-demo/.gitignore: -------------------------------------------------------------------------------- 1 | /saved -------------------------------------------------------------------------------- /user-source-ok-demo/assets/css/user.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /user-bootstrap/views/README.md: -------------------------------------------------------------------------------- 1 | Overrides `public/views` -------------------------------------------------------------------------------- /user-source-ok-demo/views/README.md: -------------------------------------------------------------------------------- 1 | Overrides `public/views` -------------------------------------------------------------------------------- /user-bootstrap/assets/css/overrides.css: -------------------------------------------------------------------------------- 1 | .lego_content_w { 2 | margin-top: 0; 3 | } -------------------------------------------------------------------------------- /assets/i/close.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcejs/lego/HEAD/assets/i/close.png -------------------------------------------------------------------------------- /assets/i/close_h.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcejs/lego/HEAD/assets/i/close_h.png -------------------------------------------------------------------------------- /assets/i/layout_1col.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcejs/lego/HEAD/assets/i/layout_1col.png -------------------------------------------------------------------------------- /assets/i/layout_2col.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcejs/lego/HEAD/assets/i/layout_2col.png -------------------------------------------------------------------------------- /assets/i/layout_3col.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcejs/lego/HEAD/assets/i/layout_3col.png -------------------------------------------------------------------------------- /assets/i/layout_default.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcejs/lego/HEAD/assets/i/layout_default.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .idea 3 | node_modules 4 | 5 | # App data 6 | /data 7 | 8 | # User custom settings 9 | # /user -------------------------------------------------------------------------------- /user-source-ok-demo/README.md: -------------------------------------------------------------------------------- 1 | To point the app into this content folder run it with special flag: 2 | 3 | ``` 4 | node lego -u user-source-ok-demo 5 | ``` 6 | -------------------------------------------------------------------------------- /core/routes/index.js: -------------------------------------------------------------------------------- 1 | // Core routes 2 | require("./redirects.js"); 3 | 4 | // User custom routes 5 | try { 6 | require(global.app.get('user') + "/core/routes"); 7 | } catch(e){} -------------------------------------------------------------------------------- /user-bootstrap/data/bootstrap/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcejs/lego/HEAD/user-bootstrap/data/bootstrap/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /user-bootstrap/data/bootstrap/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcejs/lego/HEAD/user-bootstrap/data/bootstrap/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /user-bootstrap/data/bootstrap/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sourcejs/lego/HEAD/user-bootstrap/data/bootstrap/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /user-source-ok-demo/core/routes/index.js: -------------------------------------------------------------------------------- 1 | var staticDirs = ['/res/*','/data/res/*']; 2 | staticDirs.map(function(item) { 3 | global.app.get(item, function(req, res) { 4 | res.redirect(301, opts.specsMaster.current + req.url); 5 | }); 6 | }); -------------------------------------------------------------------------------- /views/clean.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Design Lego 6 | 7 | 8 | 9 | {{{legoLayer}}} 10 | 11 | -------------------------------------------------------------------------------- /views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Design-Lego 6 | 7 | 8 | 9 | {{{legoContainer}}} 10 | 11 | -------------------------------------------------------------------------------- /assets/js/global.js: -------------------------------------------------------------------------------- 1 | var lego = lego || {}; 2 | 3 | lego.elementList = {}; // Будет хранить список всех виртуальных элементов 4 | lego.specList = {}; // Будет кешировать данные о спеках по мере их подгрузки 5 | lego.parsedTree = {}; // Хранит структуру спецификаций -------------------------------------------------------------------------------- /options.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | common: { 3 | port: 7777, 4 | 5 | // Path to `user` folder with settings could be only overridden from app start arguments 6 | pathToUser: "user-bootstrap" 7 | }, 8 | 9 | modifyInnerBlocks: true 10 | }; -------------------------------------------------------------------------------- /user-source-ok-demo/views/clean.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Design Lego 6 | 7 | 8 | 9 | 10 | {{{legoLayer}}} 11 | 12 | -------------------------------------------------------------------------------- /parser/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Odnoklassniki front-end team", 3 | "name": "design-lego-parser", 4 | "version": "0.0.1", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/sourcejs/lego.git" 8 | }, 9 | "dependencies": { 10 | "jsdom": "~1.0.1", 11 | "fs-extra": "~0.12.0" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /user-bootstrap/views/clean.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Design Lego 6 | 7 | 8 | 9 | 10 | {{{legoLayer}}} 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /user-bootstrap/options.js: -------------------------------------------------------------------------------- 1 | // Overrides /options.js 2 | module.exports = { 3 | specsMaster: { 4 | customDataUrl: '/data/bootstrap/bootstrap.json' 5 | }, 6 | 7 | cssMod: { 8 | files: [ 9 | "http://127.0.0.1:7777/data/bootstrap/css/bootstrap.css" 10 | ], 11 | rules: { 12 | blockRule: "^[a-zA-Z0-9]+$", 13 | modifierRule: "-", 14 | startModifierRule: "^-" 15 | } 16 | } 17 | }; -------------------------------------------------------------------------------- /user-source-ok-demo/views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Design-Lego 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {{{legoContainer}}} 14 | 15 | -------------------------------------------------------------------------------- /user-bootstrap/views/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Design-Lego 6 | 7 | 8 | 9 | 10 | 11 | {{{legoContainer}}} 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /views/lego-layer.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
-------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "SourceJS team", 3 | "name": "design-lego", 4 | "version": "0.1.0-alpha", 5 | "repository": { 6 | "type": "git", 7 | "url": "https://github.com/sourcejs/lego.git" 8 | }, 9 | "dependencies": { 10 | "body-parser": "^1.6.4", 11 | "colors": "0.6.x", 12 | "css": "^2.1.0", 13 | "express": "~4.8.3", 14 | "gzippo": "~0.2.0", 15 | "mustache": "~0.7.3", 16 | "shorthash": "0.0.2", 17 | "request": "~2.44.0", 18 | "deep-extend": "~0.2.11", 19 | "fs-extra": "~0.12.0", 20 | "commander": "~2.3.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /user-source-ok-demo/assets/css/hacks.css: -------------------------------------------------------------------------------- 1 | /* Cosmetic styles just for demo 2 | ---------------------------------------------------------------------------------- */ 3 | 4 | .toolbar_search.__redesign { 5 | right: 5px; 6 | } 7 | 8 | .toolbar .layer_ovr, 9 | .toolbar + .layer_ovr { 10 | display: none; 11 | } 12 | 13 | .lego_content .toolbar { 14 | margin-left: -288px; 15 | left: 16px; 16 | } 17 | 18 | .cardsList_li { 19 | margin-left: 22px; 20 | } 21 | 22 | .compactProfile { 23 | margin-bottom: 34px; 24 | } 25 | 26 | /* /Cosmetic styles just for demo 27 | ---------------------------------------------------------------------------------- */ -------------------------------------------------------------------------------- /core/getView.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var pathToApp = path.dirname(require.main.filename); 5 | var fs = require('fs'); 6 | 7 | module.exports = function(requestedView){ 8 | var output; 9 | 10 | // Working Sync, waiting for async refactoring 11 | try { 12 | // Try reading user view 13 | output = fs.readFileSync(path.join(pathToApp, global.opts.common.pathToUser, 'views', requestedView), 'utf8'); 14 | } catch (e) { 15 | // If no user view, get regular view 16 | output = fs.readFileSync(path.join(pathToApp, 'views', requestedView), 'utf8'); 17 | } 18 | 19 | return output; 20 | }; -------------------------------------------------------------------------------- /user-source-ok-demo/options.js: -------------------------------------------------------------------------------- 1 | // Overrides /options.js 2 | module.exports = { 3 | common: { 4 | port: 7777 5 | }, 6 | 7 | specsMaster: { 8 | dev: "http://127.0.0.1:8080", 9 | prod: "http://okp.me:8080" 10 | }, 11 | 12 | cssMod: { 13 | files: [ 14 | "http://127.0.0.1:8080/res/css/prod/core/ncore.css", 15 | "http://127.0.0.1:8080/res/css/prod/core/ncore_postponed.css", 16 | "http://127.0.0.1:8080/res/css/prod/main/nmain_postponed.css", 17 | "http://127.0.0.1:8080/res/css/prod/main/nmain.css" 18 | ], 19 | rules: { 20 | blockRule: "^[a-zA-Z0-9\-]+$", 21 | modifierRule: "__", 22 | startModifierRule: "^__" 23 | }, 24 | debug: false 25 | } 26 | }; -------------------------------------------------------------------------------- /core/loadOptions.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var path = require('path'); 3 | var pathToApp = path.dirname(require.main.filename); 4 | var deepExtend = require('deep-extend'); 5 | var fs = require('fs'); 6 | 7 | module.exports = function(){ 8 | 9 | var requireUncached = function (module) { 10 | delete require.cache[require.resolve(module)]; 11 | return require(module); 12 | }; 13 | var coreSettings = requireUncached(path.join(pathToApp,'options')); 14 | 15 | var pathToUser = global.commander.user ? global.commander.user : coreSettings.common.pathToUser; 16 | var userSettingsFile = path.join(pathToApp, pathToUser, 'options.js'); 17 | 18 | // If user settings file is present, override core settings 19 | if(fs.existsSync(userSettingsFile)) { 20 | deepExtend(coreSettings, requireUncached(userSettingsFile)); 21 | } 22 | 23 | return coreSettings; 24 | }; -------------------------------------------------------------------------------- /views/lego-layouts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
Default
4 |
5 | 6 | 7 |
One column
8 |
9 | 10 | 11 |
2 columns
12 |
13 | 14 | 15 |
3 columns
16 |
-------------------------------------------------------------------------------- /user-source-ok-demo/views/lego-layouts.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |
По умолчанию
4 |
5 | 6 | 7 |
1 колонка
8 |
9 | 10 | 11 |
2 колонки
12 |
13 | 14 | 15 |
3 колонки
16 |
-------------------------------------------------------------------------------- /core/routes/redirects.js: -------------------------------------------------------------------------------- 1 | /* 2 | This file contains default redirects, to add custom redirects user /user/core/routes/index.js 3 | */ 4 | 5 | var path = require('path'); 6 | var express = require('express'); 7 | var pathToApp = path.dirname(require.main.filename); 8 | 9 | if (global.opts.specsMaster.current) { 10 | // Redirecting calls for master-app 11 | global.app.get('/master-app/*', function (req, res) { 12 | var processUrl = req.url.replace('/master-app/', ''); 13 | 14 | res.redirect(301, global.opts.specsMaster.current + '/' + processUrl); 15 | }); 16 | } 17 | 18 | // First, check if there's minified assets 19 | global.app.use('/lego/assets', express.static(pathToApp + '/build/assets')); 20 | global.app.use('/lego/views', express.static(pathToApp + '/build/views')); 21 | 22 | // Redirecting core client-side file requests to app root paths 23 | global.app.use('/lego/assets', express.static(pathToApp + '/assets')); 24 | global.app.use('/lego/views', express.static(pathToApp + '/views')); -------------------------------------------------------------------------------- /assets/js/virtualblock.js: -------------------------------------------------------------------------------- 1 | /*global window */ 2 | 3 | (function (global) { 4 | "use strict"; 5 | /** 6 | * конструктор виртуального блока 7 | * 8 | * @param specId String 9 | * @returns {VirtualBlock} 10 | * @constructor 11 | */ 12 | var VirtualBlock = global.lego.VirtualBlock = function (specId) { 13 | this.id = 'block-' + Math.round(Math.random() * 10000); 14 | this.specId = specId; 15 | this.modifiers = {}; 16 | this.variation = 0; 17 | this.changed = false; 18 | 19 | global.lego.elementList[this.id] = this; 20 | 21 | return this; 22 | }; 23 | 24 | /** 25 | * Сохранить данные о виртуальном блоке 26 | * 27 | * @param p Object 28 | * @param p.variation Number 29 | * @param p.modifiers Object { block: [mod, mod, mod] } 30 | */ 31 | VirtualBlock.prototype.save = function (p) { 32 | if (p.variation) { 33 | this.variation = p.variation; 34 | } 35 | 36 | if (p.modifiers) { 37 | this.modifiers = JSON.parse(JSON.stringify(p.modifiers)); 38 | } 39 | 40 | if (p.changed) { 41 | this.changed = true; 42 | } 43 | }; 44 | }(window)); -------------------------------------------------------------------------------- /assets/js/client-templates.js: -------------------------------------------------------------------------------- 1 | /*global window */ 2 | 3 | (function (global) { 4 | "use strict"; 5 | 6 | var $ = global.jQuery; 7 | var Handlebars = global.Handlebars; 8 | 9 | var clientTemplates = global.lego.clientTemplates = { 10 | get: function () { 11 | var that = this; 12 | 13 | return new Promise(function (resolve, reject) { 14 | $.ajax({ 15 | url: '/lego/views/client-templates.html', 16 | dataType: 'html', 17 | success: function (rawTemplates) { 18 | var $rawTemplates = $(rawTemplates); 19 | 20 | $rawTemplates.each(function () { 21 | var templateId = $(this).attr('id'); 22 | if (templateId) { 23 | that[templateId] = Handlebars.compile($(this).html()); 24 | } 25 | }); 26 | 27 | resolve(); 28 | }, 29 | error: function () { 30 | reject(); 31 | } 32 | }); 33 | }); 34 | } 35 | }; 36 | }(window)); -------------------------------------------------------------------------------- /views/client-templates.html: -------------------------------------------------------------------------------- 1 | 12 | 13 | 23 | 24 | 32 | 33 | -------------------------------------------------------------------------------- /assets/js/components.js: -------------------------------------------------------------------------------- 1 | /*global window */ 2 | 3 | (function (global) { 4 | "use strict"; 5 | var $ = global.jQuery; 6 | var component = global.lego.component = {}; 7 | 8 | component.expand = (function () { 9 | 10 | var expandContainerClassName = 'lego_expand'; 11 | var expandTitleClassName = 'lego_expand_h'; 12 | var expandBodyClassName = 'lego_expand_cnt'; 13 | var expandIsClosed = '__closed'; 14 | 15 | 16 | $('body').on('click', '.' + expandTitleClassName, function () { 17 | $(this).closest('.' + expandContainerClassName).toggleClass(expandIsClosed); 18 | }); 19 | 20 | return { 21 | 22 | create: function ($parentContainer, title, isClosed) { 23 | var template = '
\ 24 |
' + title + '
\ 25 |
\ 26 |
'; 27 | var $template = $(template); 28 | 29 | if (isClosed) { 30 | $template.addClass(expandIsClosed); 31 | } 32 | 33 | $parentContainer.append($template); 34 | 35 | return $template; 36 | }, 37 | 38 | append: function ($expandContainer, html) { 39 | var $addedElem = $(html); 40 | $expandContainer.find('.' + expandBodyClassName).append($addedElem); 41 | return $addedElem; 42 | }, 43 | 44 | remove: function ($expandContainer) { 45 | $expandContainer.remove(); 46 | } 47 | }; 48 | }()); 49 | }(window)); -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Design-lego 2 | 3 | [![Gitter chat](https://badges.gitter.im/gitterHQ/gitter.png)](https://gitter.im/sourcejs/Source) 4 | 5 | **v0.1.0-alpha (first concept)** 6 | 7 | [SourceJS](http://sourcejs.com) companion app, for layout prototyping based on own blocks library. Could be also used separately, without SourceJS, check `/user-bootstrap` demo (runs by default). 8 | 9 | ![image](http://habrastorage.org/files/6fd/bd8/4e4/6fdbd84e4ea2492f902c250d7dcd1d50.png) 10 | 11 | **[Demo video](https://www.youtube.com/watch?v=cefy_U5NU4o)** 12 | 13 | If you're already using [SourceJS](http://sourcejs.com) engine for organising your front-end component library, check demo configuration folder for integration `/user-source-ok-demo`. As from 0.4.0 SourceJS has own Content API, that lets you request any block resources from your library, all you need is to set some basic configuration for Design-Lego app. 14 | 15 | For running your own block library with Design-Lego app, all you need is to generate JSON data file with [similar structure](https://github.com/sourcejs/lego/blob/master/user-bootstrap/data/bootstrap/bootstrap.json). For bootstrap integration, we created [simple parser](https://github.com/sourcejs/lego/blob/master/parser/bootstrap.js) for required data generation. 16 | 17 | ## Features 18 | 19 | * Load your own component library (based on any frameworks and templating engines) 20 | * Review your existing blocks through search and switch between different variations (block examples) 21 | * If you're seperating CSS modifiers, Design-Lego will parse your CSS and suggest existing modifiers for your currently selected block 22 | * Build lite prototypes with different components combination and share 23 | * Block layout drop predictions 24 | 25 | ## Setup 26 | 27 | ``` 28 | git clone https://github.com/sourcejs/lego.git 29 | cd lego 30 | npm i 31 | node lego 32 | ``` 33 | 34 | NodeJS will run local server, with pre-build bootstrap library for playing in Design-lego app. 35 | 36 | ## Upcoming features 37 | 38 | * Configurable layouts 39 | * Improved UI 40 | * Improved prototyping features with drag n drop 41 | * Easy to setup configuration with SourceJS 42 | * Component export to tools for designing in the browser 43 | -------------------------------------------------------------------------------- /parser/bootstrap.js: -------------------------------------------------------------------------------- 1 | var jsdom = require('jsdom'); 2 | var fs = require('fs-extra'); 3 | var path = require('path'); 4 | var jquery = fs.readFileSync("./lib/jquery.js", "utf-8"); 5 | 6 | var generateJSON = function(window){ 7 | var $ = window.$; 8 | var output = {}; 9 | 10 | $('.bs-docs-section').each(function(){ 11 | var $t = $(this); 12 | var $header = $t.find('.page-header'); 13 | var id = $header.attr('id'); 14 | var title = $header.text(); 15 | 16 | var contents = []; 17 | var exampleID = 1; 18 | 19 | $t.find('.bs-example').each(function(){ 20 | var html = []; 21 | html.push($(this).children()[0].outerHTML); 22 | 23 | var header = $(this).prevAll('h4').html(); 24 | 25 | if (!header) header = $(this).prevAll('h3').html(); 26 | if (!header) header = $(this).prevAll('h2').html(); 27 | if (!header) header = $(this).prevAll('h1').html(); 28 | 29 | contents.push({ 30 | header: header, 31 | id: exampleID, 32 | html: html 33 | }); 34 | 35 | exampleID++; 36 | }); 37 | 38 | output[id] = { 39 | id: id, 40 | url: 'http://getbootstrap.com/components#' + id, 41 | title: title, 42 | contents: contents 43 | }; 44 | }); 45 | 46 | return output; 47 | }; 48 | 49 | jsdom.env({ 50 | file: 'raw/bootstrap.html', 51 | // url: "http://getbootstrap.com/components", 52 | src: [jquery], 53 | done: function (errors, window) { 54 | var dataPath = '../user-bootstrap/data/bootstrap'; 55 | var fileName = 'bootstrap.json'; 56 | var fullPath = path.join(dataPath, fileName); 57 | 58 | var data = generateJSON(window); 59 | 60 | // Preparing path for data write 61 | fs.mkdirp(dataPath, function (err) { 62 | if (err) return console.error(err); 63 | 64 | fs.writeFile(fullPath, JSON.stringify(data, null, 4), function(err) { 65 | if (err) return console.error(err); 66 | 67 | console.log('JSON saved to: ' + fullPath); 68 | }); 69 | }); 70 | } 71 | }); -------------------------------------------------------------------------------- /views/lego-сontainer.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |
5 | 6 | 7 |
8 |
9 |
10 |
Layouts
11 |
12 | {{{legoLayouts}}} 13 |
14 |
15 |
16 |
17 | 18 |
Loading components...
19 |
20 |
21 |
22 |
23 | {{{legoLayer}}} 24 |
25 |
26 |
27 |
28 |
29 |
Export to
30 | 34 |
35 | 36 |
37 |
Content
38 |
    39 |
    40 | 41 |
    42 |
    Variations
    43 |
    44 | 45 |
    46 | 47 |
    48 | 49 |
    50 |
    Modificators
    51 |
    52 | 53 |
    54 |
    55 |
    56 |
    57 |
    58 |
    59 | 60 | 61 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /user-source-ok-demo/views/lego-layer.html: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    25 |
    26 |
    27 |
    28 |
    29 |
    30 |
    31 |
    32 |
    33 |
    34 |
    35 |
    -------------------------------------------------------------------------------- /assets/js/search.js: -------------------------------------------------------------------------------- 1 | /*global window */ 2 | 3 | (function (global) { 4 | "use strict"; 5 | 6 | var $ = global.jQuery; 7 | var Handlebars = global.Handlebars; 8 | var globalOptions = global.lego.globalOptions; 9 | var component = global.lego.component; 10 | var clientTemplates = global.lego.clientTemplates; 11 | 12 | var parsed = false; 13 | 14 | var processSpecsData = function (specsTree, callback) { 15 | var resultTree = { 16 | root: [] 17 | }; 18 | var closedSection = false; 19 | var cb = callback || function () {}; 20 | var specsPath; 21 | var specsSection; 22 | 23 | // Separating spec cats 24 | for (specsPath in specsTree) { 25 | if (specsTree.hasOwnProperty(specsPath)) { 26 | var specPathParts = specsPath.split('/'); 27 | 28 | if (specPathParts.length === 1) { 29 | resultTree.root.push(specsTree[specsPath]); 30 | } else { 31 | if (!resultTree[specPathParts[0]]) { 32 | resultTree[specPathParts[0]] = []; 33 | } 34 | resultTree[specPathParts[0]].push(specsTree[specsPath]); 35 | } 36 | } 37 | } 38 | 39 | if (resultTree.root.length === 0) { 40 | delete resultTree.root; 41 | } 42 | 43 | for (specsSection in resultTree) { 44 | if (resultTree.hasOwnProperty(specsSection)) { 45 | resultTree[specsSection].sort(function (a, b) { 46 | if (a.title > b.title) { 47 | return 1; 48 | } 49 | if (a.title < b.title) { 50 | return -1; 51 | } 52 | return 0; 53 | }); 54 | 55 | var newNode = component.expand.create($('#lego_search-result'), specsSection, closedSection); 56 | component.expand.append(newNode, clientTemplates['search-item'](resultTree[specsSection])); 57 | 58 | closedSection = true; 59 | } 60 | } 61 | 62 | cb(); 63 | }; 64 | 65 | var prepareSpecsData = function (callback) { 66 | var specsMaster = globalOptions.specsMaster.current; 67 | var customDataUrl = globalOptions.specsMaster.customDataUrl; 68 | 69 | var drawNavigation = function (data) { 70 | global.lego.parsedTree = data; 71 | 72 | clientTemplates.get().then(function () { 73 | parsed = true; 74 | processSpecsData(global.lego.parsedTree, callback); 75 | }); 76 | }; 77 | 78 | if (customDataUrl) { 79 | $.ajax(customDataUrl, { 80 | method: 'GET', 81 | contentType: 'application/json', 82 | 83 | success: drawNavigation 84 | }); 85 | } else { 86 | $.ajax(specsMaster + '/api/specs', { 87 | method: 'GET', 88 | crossDomain: true, 89 | data: { 90 | filter: { 91 | cats: ['base'], 92 | forceTags: ['lego'] 93 | }, 94 | filterOut: { 95 | tags: ['html', 'lego-hide', 'hidden', 'deprecated'] 96 | } 97 | }, 98 | contentType: 'application/json', 99 | 100 | success: drawNavigation 101 | }); 102 | } 103 | 104 | Handlebars.registerHelper("imageUrl", function (url) { 105 | url = url.toString(); 106 | 107 | return specsMaster + url + "/thumbnail.png"; 108 | }); 109 | 110 | Handlebars.registerHelper("fullUrl", function (url) { 111 | url = url.toString(); 112 | 113 | return specsMaster ? specsMaster + url : url; 114 | }); 115 | }; 116 | 117 | var fuzzySearch = function (q, allData) { 118 | var result = {}; 119 | var query = q.toLowerCase().trim(); 120 | var qRegExp = new RegExp(query); 121 | var cat; 122 | 123 | for (cat in allData) { 124 | if (allData.hasOwnProperty(cat)) { 125 | var lowerCat = cat.toLowerCase(); 126 | var title = allData[cat].title || ''; 127 | var info = allData[cat].info || ''; 128 | var keywords = allData[cat].keywords || ''; 129 | 130 | title = title.toString().toLowerCase(); // Защита от массивов 131 | info = info.toString().toLowerCase(); 132 | keywords = keywords.toString().toLowerCase(); 133 | 134 | // if query matches current category, all category's articles are considered a match 135 | 136 | if (qRegExp.test(lowerCat.match(query)) || 137 | qRegExp.test(info.match(query)) || 138 | qRegExp.test(keywords.match(query)) || 139 | qRegExp.test(title.match(query))) { 140 | 141 | if (!result[cat]) { 142 | result[cat] = {}; 143 | } 144 | result[cat] = allData[cat]; 145 | } 146 | } 147 | } 148 | return result; 149 | }; 150 | 151 | var renderLiveSearchResults = function (value) { 152 | var $searchResult = $("#lego_search-result"); 153 | 154 | $searchResult.empty(); 155 | 156 | if (parsed) { 157 | var result = fuzzySearch(value, global.lego.parsedTree); 158 | if ($.isEmptyObject(result)) { 159 | $searchResult.html("No search results"); 160 | } else { 161 | processSpecsData(result); 162 | } 163 | } else { 164 | $searchResult.html("Loading components..."); 165 | 166 | global.setTimeout(function () { 167 | renderLiveSearchResults(value); 168 | }, 500); 169 | } 170 | }; 171 | 172 | $(function () { 173 | var $searchInput = $('#search'); 174 | 175 | // Загрузить дерево спецификаций и шаблон поисковой выдачи 176 | // После завершения обработки отрендерить значение фильтра 177 | prepareSpecsData(function () { 178 | renderLiveSearchResults($searchInput.val()); 179 | }); 180 | 181 | $searchInput.on("keyup search", function () { 182 | 183 | // Нет смысла дергать поиск, если нет данных и шаблона 184 | if (parsed) { 185 | renderLiveSearchResults($searchInput.val()); 186 | } 187 | }); 188 | }); 189 | }(window)); -------------------------------------------------------------------------------- /lego.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var fs = require('fs-extra'); 3 | var path = require('path'); 4 | var mustache = require('mustache'); 5 | var gzippo = require('gzippo'); 6 | var sh = require('shorthash'); 7 | var colors = require('colors'); 8 | var commander = require('commander'); 9 | var cssMod = require('./core/css-mod'); 10 | var bodyParser = require('body-parser'); 11 | var getView = require('./core/getView'); 12 | var defOpts = require('./options'); 13 | 14 | 15 | /* Globals */ 16 | 17 | // Parameters 18 | commander 19 | .option('-u, --user [string]', 'Path to user folder (default: ' + defOpts.common.pathToUser + ')', defOpts.common.pathToUser) 20 | .option('-p, --port [number]', 'Server port (default: ' + defOpts.common.port + ')', defOpts.common.port) 21 | .parse(process.argv); 22 | 23 | global.commander = commander; 24 | 25 | var app = global.app = express(); 26 | var opts = global.opts = require('./core/loadOptions')(); 27 | 28 | var MODE = global.MODE = process.env.NODE_ENV || 'development'; 29 | 30 | // Preparing environment 31 | if (global.opts.specsMaster.prod && global.opts.specsMaster.dev) { 32 | opts.specsMaster.current = global.MODE === 'production' ? global.opts.specsMaster.prod : global.opts.specsMaster.dev; 33 | } 34 | 35 | // Overriding options from specified arguments 36 | if (commander.port) global.opts.common.port = commander.port; 37 | if (commander.user) global.opts.common.pathToUser = commander.user; 38 | /* /Globals */ 39 | 40 | 41 | /* Express settings */ 42 | app.set('user', path.join(__dirname, opts.common.pathToUser)); 43 | 44 | app.use(bodyParser.json()); 45 | app.use(logErrors); 46 | app.use(clientErrorHandler); 47 | app.use(errorHandler); 48 | /* /Express settings */ 49 | 50 | 51 | 52 | /* Includes */ 53 | 54 | // Routes 55 | require('./core/routes'); 56 | 57 | /* /Includes */ 58 | 59 | 60 | 61 | /* Route for static files */ 62 | app.use(gzippo.staticGzip(app.get('user'))); 63 | app.use(gzippo.compress()); 64 | 65 | // Main page 66 | var arr = ['/','/index','/index.html','/home']; 67 | arr.map(function(item) { 68 | app.get(item, function(req, res) { 69 | var legoContainer = mustache.to_html(getView('lego-сontainer.html'), { 70 | legoLayer: getView('lego-layer.html'), 71 | legoLayouts: getView('lego-layouts.html'), 72 | globalOptions: JSON.stringify(opts) 73 | }); 74 | 75 | var htmlToSend = mustache.to_html(getView('index.html'), { 76 | legoContainer: legoContainer 77 | }); 78 | 79 | res.send(htmlToSend); 80 | }); 81 | }); 82 | 83 | // Share link 84 | app.get('/s/:page', function (req, res) { 85 | var page = req.params.page; 86 | 87 | fs.readFile(path.join(app.get('user'), 'saved', page + '.html'), "utf8", function (err, data) { 88 | if(err) { 89 | console.log(err); 90 | res.send('No saved data with this id.'); 91 | return; 92 | } 93 | 94 | var legoContainer = mustache.to_html(getView('lego-сontainer.html'), { 95 | legoLayer: data, 96 | legoLayouts: getView('lego-layouts.html'), 97 | globalOptions: JSON.stringify(opts) 98 | }); 99 | 100 | var htmlToSend = mustache.to_html(getView('index.html'), { 101 | legoContainer: legoContainer 102 | }); 103 | 104 | res.send(htmlToSend); 105 | }); 106 | }); 107 | 108 | // Clean html link 109 | app.get('/clean/:page', function (req, res) { 110 | var page = req.params.page; 111 | var view = getView('clean.html'); 112 | 113 | fs.readFile(path.join(app.get('user'), 'saved', page + '.html'), "utf8", function (err, data) { 114 | if (err) { 115 | console.log(err); 116 | res.send('No saved data with this id.'); 117 | return; 118 | } 119 | 120 | var htmlToSend = mustache.to_html(view, { 121 | legoLayer: data, 122 | globalOptions: JSON.stringify(opts) 123 | }); 124 | 125 | res.send(htmlToSend); 126 | }); 127 | }); 128 | /* /Route for static files */ 129 | 130 | 131 | 132 | /* API */ 133 | // Get Css modifiers 134 | app.post('/cssmod', function (req, res) { 135 | // Preparing config for cssMod parser from options file and received info 136 | var config = JSON.parse(JSON.stringify(opts.cssMod)); 137 | config.files = req.body.files; 138 | 139 | cssMod.getCssMod(config, function(err, parsedCss) { 140 | if (err) { 141 | console.log('getCssMod error: ', err); 142 | 143 | res.status(500).send('Error getting specified CSS files for modifier analysis, check options.js.'); 144 | return; 145 | } 146 | 147 | res.send(parsedCss); 148 | }); 149 | }); 150 | 151 | // Save working html 152 | app.get('/save', function (req, res) { 153 | var html = req.query.html; 154 | var outputDir = path.join(app.get('user'), 'saved'); 155 | var name = sh.unique(html); 156 | 157 | fs.mkdirp(outputDir, function (err) { 158 | if (err) { 159 | return console.error(err); 160 | } 161 | 162 | fs.writeFile(path.join(outputDir, name + ".html"), html, function(err) { 163 | if (err) { 164 | console.log(err); 165 | res.send({ 166 | success: false 167 | }); 168 | return; 169 | } 170 | 171 | res.send({ 172 | success: true, 173 | name: name 174 | }); 175 | }); 176 | }); 177 | }); 178 | /* /API */ 179 | 180 | 181 | 182 | /* Error handling */ 183 | function logErrors(err, req, res, next) { 184 | console.error(("Error: " + err.stack).red); 185 | next(err); 186 | } 187 | 188 | function clientErrorHandler(err, req, res, next) { 189 | if (req.xhr) { 190 | res.send(500, { error: 'Something blew up!' }); 191 | } else { 192 | next(err); 193 | } 194 | } 195 | 196 | function errorHandler(err, req, res, next) { 197 | res.status(500); 198 | res.render('error', { error: err }); 199 | } 200 | /* /Error handling */ 201 | 202 | 203 | // Starting server 204 | if (!module.parent) { 205 | var port = opts.common.port; 206 | var portString = port.toString(); 207 | 208 | app.listen(port); 209 | 210 | var d = new Date(); 211 | var dateArr = [d.getHours(), d.getMinutes(), d.getSeconds()]; 212 | dateArr = dateArr.map(function (el) { 213 | return (el > 9) ? el : '0' + el; 214 | }); 215 | 216 | var dateString = dateArr.join(':').red; 217 | 218 | console.log('[LEGO] '.blue + dateString + ' Server started on http://localhost:'.blue + portString.red + ', from '.blue + global.opts.common.pathToUser.red + ' folder in '.blue + MODE.blue + ' mode...'.blue); 219 | } -------------------------------------------------------------------------------- /core/css-mod/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS modifiers analyzer 3 | */ 4 | 5 | var fs = require('fs'); 6 | var css = require('css'); 7 | var request = require('request'); 8 | 9 | module.exports = function cssMod() { 10 | var debug = false; 11 | var modifierDetect = /__/; 12 | var startModifierDetect = /^__/; 13 | var lastProcessedObj = {}; 14 | var lastEtags = {}; 15 | var cssFilesCache = {}; 16 | 17 | // CSS Files collection 18 | var cssFiles = []; 19 | 20 | // Output tree with block, element, modifier structure 21 | var outputTree = {}; 22 | 23 | 24 | // Download all styles and parse CSS modifiers 25 | function parseCSS(callback) { 26 | var _callback = callback || function () {}; 27 | 28 | // String with all concatenated CSS files 29 | var importCss = ''; 30 | 31 | for (var i = 0, allReqComplete = 0, chachedCount = 0, reqErrors = 0; i < cssFiles.length; i++) { 32 | var currentFile = cssFiles[i]; 33 | var requrestOptions = { 34 | url: cssFiles[i], 35 | headers: { 36 | } 37 | }; 38 | var currentFileEtags = lastEtags[currentFile]; 39 | if (currentFileEtags) { 40 | // Setting etag for cache 41 | requrestOptions.headers['If-None-Match'] = currentFileEtags; 42 | } 43 | 44 | request(requrestOptions, function(err, response, body) { 45 | if (err) { 46 | console.log('Error loading stylesheet for modifiers parsing:', requrestOptions.url); 47 | 48 | reqErrors++; 49 | } else { 50 | var currentFile = response.request.uri.href; 51 | 52 | // Saving etag in memory 53 | lastEtags[currentFile] = response.headers.etag; 54 | 55 | // If file new 56 | if (response.statusCode == 200) { 57 | importCss += body; 58 | cssFilesCache[currentFile] = body; 59 | 60 | // If file is cached 61 | } else if (response.statusCode == 304) { 62 | importCss += cssFilesCache[currentFile]; 63 | chachedCount++; 64 | } 65 | } 66 | 67 | allReqComplete++; 68 | 69 | // When loop is done 70 | if (allReqComplete === cssFiles.length) { 71 | 72 | // Check we have at lest some downloaded files 73 | if (reqErrors === cssFiles.length) { 74 | _callback('All CSS files for modifier parsing are unreachable.', null); 75 | } else if (chachedCount === cssFiles.length) { 76 | // If all files are cached, return last parsed CSS 77 | _callback(null, lastProcessedObj); 78 | } else { 79 | // Or parse it again 80 | _callback(null, processCssList(css.parse(importCss).stylesheet.rules)) 81 | } 82 | } 83 | }); 84 | } 85 | } 86 | 87 | // Добавление селектора в итоговый список 88 | function addToList(block, modifier) { 89 | if ( outputTree[block] === undefined ) { 90 | outputTree[block] = []; 91 | } 92 | 93 | if (outputTree[block].indexOf(modifier) === -1) { 94 | outputTree[block].push(modifier); 95 | } 96 | } 97 | 98 | // Диагностика селекторов 99 | function dropException(context, selector, cause) { 100 | if (debug) { 101 | console.log('Ignored selector: ' + selector); 102 | console.log('Context: ' + context); 103 | console.log('Cause: ' + cause) 104 | console.log(''); 105 | } 106 | } 107 | 108 | // Разбор правил 109 | function processCssList(parsedCss) { 110 | 111 | function parseOldModifier(rule) { 112 | var separatorPos = stringList[sel].search(modifierDetect); 113 | 114 | return { 115 | block: rule.substring(0, separatorPos), 116 | modifier: rule 117 | } 118 | } 119 | 120 | for (var rule = 0; rule < parsedCss.length; rule++) { 121 | var selectors = parsedCss[rule].selectors; 122 | 123 | if (selectors !== undefined) { 124 | for (var selector = 0; selector < selectors.length; selectors++) { 125 | 126 | if (modifierDetect.test( selectors[selector] )) { 127 | 128 | var words = selectors[selector] 129 | 130 | // зачищаем псевдоэлементы и псевдоклассы 131 | .replace(/::before/g, '') 132 | .replace(/::after/g, '') 133 | .replace(/::disabled/g, '') 134 | 135 | .replace(/:before/g, '') 136 | .replace(/:after/g, '') 137 | .replace(/:hover/g, '') 138 | .replace(/:focus/g, '') 139 | .replace(/:target/g, '') 140 | .replace(/:active/g, '') 141 | .replace(/:disabled/g, '') 142 | .replace(/:first-child/g, '') 143 | .replace(/:last-child/g, '') 144 | .replace(/:empty/g, '') 145 | 146 | .replace(/::-moz-placeholder/g, '') 147 | .replace(/:-ms-input-placeholder/g, '') 148 | .replace(/::-webkit-input-placeholder/g, '') 149 | 150 | // из :not(.__class) можно извлечь пользу 151 | .replace(/:not\(/g, '') 152 | .replace(/\)/g, '') 153 | 154 | // удаляем атрибуты 155 | .replace(/\[.*?\]/ig, '') 156 | 157 | // сложные правила конвертируем в каскад 158 | .replace(/\+/g, ' ') 159 | .replace(/~/g, ' ') 160 | .replace(/>/g, ' ') 161 | .replace(/\*/, ' ') 162 | .split(' '); 163 | 164 | for (var word = 0; word < words.length; word++) { 165 | if ( modifierDetect.test(words[word]) ) { 166 | 167 | // Разбиваем селектор, содержащий class и id, и удаляем первый пустой элемент 168 | var stringList = words[word].split(/\.|#/g); 169 | stringList.shift(); 170 | 171 | var block = 0, // флаг неопределенности блока 172 | modifier = []; // массив для модификаторов в новом стиле 173 | 174 | for (var sel = 0; sel < stringList.length; sel++) { 175 | var currentClass = stringList[sel]; 176 | 177 | if (!modifierDetect.test(currentClass)) { 178 | // Это блок (элемент) 179 | if (block !== false && block !== currentClass) { 180 | if (block === 0) { 181 | block = currentClass 182 | } else { 183 | block = false; 184 | } 185 | } 186 | } else if (startModifierDetect.test(currentClass)) { 187 | // Это модификатор в новом стиле 188 | modifier.push(currentClass); 189 | } else { 190 | // Это модификатор в старом стиле 191 | var result = parseOldModifier(currentClass); 192 | 193 | if (block !== false && block !== result.block) { 194 | if (block === 0) { 195 | block = result.block 196 | } else { 197 | block = false; 198 | } 199 | } 200 | 201 | addToList(result.block, result.modifier); 202 | } 203 | } 204 | 205 | // Если строго один блок (элемент) и модификаторы в новом стиле, нет неопределенности 206 | if (block && modifier.length) { 207 | for (var i = 0; i < modifier.length; i++) { 208 | addToList(block, modifier[i]); 209 | } 210 | } else if (!block && modifier.length) { 211 | dropException(selectors[selector], words[word], 'Принадлежность модификатора не определена'); 212 | } 213 | } 214 | } 215 | } 216 | } 217 | } 218 | } 219 | 220 | // Caching parse output 221 | lastProcessedObj = outputTree; 222 | return outputTree; 223 | } 224 | 225 | return { 226 | getCssMod: function (config, callback) { 227 | var _callback = callback || function () {}; 228 | 229 | // TODO: Add automatic style parsing, based on page-content 230 | cssFiles = config.files; 231 | modifierDetect = new RegExp(config.rules.modifierRule) || modifierDetect; 232 | startModifierDetect = new RegExp(config.rules.startModifierRule) || startModifierDetect; 233 | debug = config.debug || debug; 234 | 235 | parseCSS(_callback); 236 | } 237 | } 238 | }(); 239 | -------------------------------------------------------------------------------- /assets/css/lego.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | position: relative; 4 | height: 100%; 5 | } 6 | 7 | /* Base 8 | ---------------------------------------------------------------------------------- */ 9 | 10 | .lego_lk:link, 11 | .lego_lk:visited { 12 | color: #ddd; 13 | text-decoration: none; 14 | } 15 | 16 | .lego_lk:hover { 17 | text-decoration: underline; 18 | } 19 | 20 | .lego_lk.__active { 21 | color:#EB722E; 22 | } 23 | 24 | /* Clearfix */ 25 | .lego_cf:before, 26 | .lego_cf:after { 27 | content: " "; 28 | display: table; 29 | } 30 | 31 | .lego_cf:after { 32 | clear: both; 33 | } 34 | 35 | /* Expand */ 36 | .lego_expand { 37 | } 38 | 39 | .lego_expand_h { 40 | position: relative; 41 | margin-bottom: 10px; 42 | color: #bababa; 43 | font-size: 14px; 44 | cursor: pointer; 45 | display: block; 46 | } 47 | .lego_expand_h:before{ 48 | display: inline-block; 49 | content: '▼'; 50 | margin-right: 5px; 51 | font-size: 12px; 52 | transform: rotate(0deg); 53 | transition: transform 0.2s; 54 | } 55 | .lego_expand.__closed .lego_expand_h:before { 56 | transform: rotate(-90deg); 57 | } 58 | 59 | .lego_expand_cnt { 60 | padding-left: 0; 61 | } 62 | .lego_search-result.__list .lego_expand_cnt { 63 | padding-left: 17px; 64 | } 65 | .lego_expand.__closed .lego_expand_cnt { 66 | display: none; 67 | } 68 | 69 | /* /Base 70 | ---------------------------------------------------------------------------------- */ 71 | 72 | 73 | 74 | /* Project 75 | ---------------------------------------------------------------------------------- */ 76 | 77 | /* Layout */ 78 | 79 | .lego_container { 80 | position: relative; 81 | height: 100%; 82 | display: flex; 83 | } 84 | 85 | .lego_aside, 86 | .lego_main { 87 | 88 | } 89 | 90 | .lego_aside { 91 | position: relative; 92 | flex: 1; 93 | min-width: 120px; 94 | } 95 | .lego_aside.__right { 96 | flex: initial; 97 | width: 200px; 98 | } 99 | 100 | .lego_aside_panel { 101 | position: absolute; 102 | top: 0; 103 | left: 0; 104 | overflow-y: auto; 105 | overflow-x: hidden; 106 | width: 100%; 107 | min-width: 100%; 108 | height: 100%; 109 | background: #444; 110 | z-index: 1000; 111 | box-shadow: none; 112 | transition: width 0.3s 0.15s, box-shadow 0.3s 0.15s; 113 | } 114 | .lego_aside_panel:hover { 115 | width: 215px; 116 | box-shadow: 0 0 10px #000; 117 | } 118 | .lego_aside_panel:hover:after { 119 | box-shadow: none; 120 | } 121 | 122 | .lego_aside_panel:after { 123 | content: ''; 124 | position: absolute; 125 | top: 0; 126 | right: 0; 127 | height: 100%; 128 | box-shadow: 0 0 10px 1px #000; 129 | transition: box-shadow 0.3s; 130 | } 131 | 132 | .lego_aside.__right .lego_aside_panel { 133 | left: auto; 134 | right: 0; 135 | } 136 | .lego_aside.__right .lego_aside_panel:after { 137 | right: auto; 138 | left: 0; 139 | } 140 | 141 | .lego_aside_cnt { 142 | width: 215px; 143 | padding-right: 10px; 144 | box-sizing: border-box; 145 | } 146 | 147 | .lego_main { 148 | position: relative; 149 | height: 100%; 150 | overflow: auto; 151 | background: #999; /* color of cover */ 152 | width: 1015px; /* 1000 + scroll width */ 153 | } 154 | 155 | .lego_toggler { 156 | margin: 10px; 157 | } 158 | .lego_toggle_i { 159 | display: inline-block; 160 | padding: 3px; 161 | font-size: 12px; 162 | } 163 | .lego_toggle_i.__active { 164 | background: #FF7F00; 165 | color: #fff; 166 | } 167 | 168 | .lego_aside_search { 169 | width: 85%; 170 | margin-top: 20px; 171 | margin-left: 10px; 172 | padding: 5px; 173 | 174 | border: 1px solid #bababa; 175 | } 176 | input[type="search"].lego_aside_search { 177 | -webkit-appearance: searchfield; 178 | } 179 | input[type="search"].lego_aside_search::-webkit-search-cancel-button { 180 | -webkit-appearance: searchfield-cancel-button; 181 | } 182 | 183 | .lego_search-result { 184 | padding: 10px; 185 | color: #fff; 186 | } 187 | 188 | .lego_search-result.__list .lego_search-result_i { 189 | display: block; 190 | width: auto; 191 | 192 | } 193 | .lego_search-result.__list .lego_search-result_img { 194 | display: none; 195 | 196 | } 197 | 198 | .lego_search-result_img { 199 | display: inline-block; 200 | width: 85px; 201 | height: 85px; 202 | background-color: #fff !important; 203 | background-position: 50% !important; 204 | background-repeat: no-repeat !important; 205 | background-size: contain !important; 206 | } 207 | 208 | .lego_search-result_w { 209 | 210 | } 211 | 212 | .lego_search-result_i { 213 | display: inline-block; 214 | width: 85px; 215 | vertical-align: top; 216 | margin-bottom: 10px; 217 | margin-right: 2px; 218 | } 219 | 220 | .lego_search-result_n { 221 | display: inline-block; 222 | vertical-align: middle; 223 | font-size: 12px; 224 | color: #fff; 225 | } 226 | 227 | .lego_export-select { 228 | display: block; 229 | margin: 0 auto; 230 | } 231 | 232 | .lego_widget:nth-of-type(1) { 233 | margin-top: 20px; 234 | } 235 | 236 | .lego_widget { 237 | color: #bababa; 238 | padding: 0 10px 20px; 239 | 240 | font-size: 12px; 241 | } 242 | 243 | .lego_widget_h { 244 | border-bottom: 1px solid #bababa; 245 | margin-bottom: 15px; 246 | font-size: 16px; 247 | } 248 | 249 | .lego_widget_ul { 250 | padding: 0; 251 | margin: 0; 252 | list-style-type: none; 253 | } 254 | 255 | .lego_widget_ul-i { 256 | position: relative; 257 | padding-right: 20px; 258 | padding-bottom: 5px; 259 | font-size: 12px; 260 | } 261 | 262 | .lego_widget_ul-i .lego_lk { 263 | display: inline-block; 264 | } 265 | 266 | .lego_widget_ul-i .lego_ic { 267 | display: none; 268 | position: absolute; 269 | top: -1px; right: 0; 270 | } 271 | 272 | .lego_widget_ul-i:hover .lego_ic_close { 273 | display: block; 274 | } 275 | 276 | .lego_widget_form { 277 | 278 | } 279 | 280 | .lego_form-i { 281 | margin-bottom: 15px; 282 | } 283 | 284 | .lego_form-i_w { 285 | clear: left; 286 | margin-bottom: 10px; 287 | } 288 | 289 | .lego_form-i_w .lego_it, 290 | .lego_form-i_w .lego_form-i_txt { 291 | display: inline-block; 292 | } 293 | 294 | .lego_form-i_w .lego_form-i_txt { 295 | 296 | } 297 | 298 | .lego_form-i .lego_checkbox { 299 | 300 | } 301 | 302 | .lego_label { 303 | display: block; 304 | margin-bottom: 5px; 305 | color: #ddd; 306 | } 307 | 308 | .lego_it { 309 | box-sizing: border-box; 310 | padding: 2px 3px; 311 | font-size: 13px; 312 | color: #555; 313 | } 314 | 315 | .lego_it.__short { 316 | width: 80px; 317 | } 318 | 319 | .lego_it.__medium { 320 | width: 155px; 321 | } 322 | 323 | 324 | 325 | .lego_ic { 326 | width: 16px; 327 | height: 16px; 328 | background-repeat: no-repeat; 329 | } 330 | 331 | .lego_ic.lego_ic_close { 332 | background: url(/lego/assets/i/close.png); 333 | } 334 | .lego_ic.lego_ic_close:hover { 335 | background: url(/lego/assets/i/close_h.png); 336 | } 337 | 338 | 339 | /* Layers */ 340 | 341 | /* default styles for all layers */ 342 | .lego_layer { 343 | display: flex; 344 | position: relative; 345 | overflow: hidden; 346 | margin: 10px; 347 | min-height: calc(100% - 20px); 348 | } 349 | .lego_layer.__one-column, 350 | .lego_layer.__two-column, 351 | .lego_layer.__three-column { 352 | width: 1000px; 353 | margin: 0; 354 | margin-right: 20px; 355 | } 356 | 357 | 358 | /* without background of grid */ 359 | .lego_layer.__hide-bg .lego_right-column, 360 | .lego_layer.__hide-bg .lego_sidebar, 361 | .lego_layer.__hide-bg .lego_toolbar, 362 | .lego_layer.__hide-bg .lego_navbar, 363 | .lego_layer.__hide-bg .lego_content { 364 | background: #fff; 365 | } 366 | 367 | 368 | /* by default all block exclude content_w are hidden */ 369 | .lego_right-column, 370 | .lego_sidebar, 371 | .lego_toolbar, 372 | .lego_navbar, 373 | .lego_content[data-target="overlay"], 374 | .lego_content[data-target="primary"] { 375 | display: none; 376 | background: #FFF4F8; 377 | } 378 | 379 | .lego_toolbar { 380 | width: 100%; 381 | height: 48px; 382 | } 383 | 384 | 385 | .lego_sidebar { 386 | float: left; 387 | margin: 15px 0 0 15px; 388 | width: 210px; 389 | min-height: 685px; 390 | } 391 | 392 | .lego_content_w { 393 | width: 100%; 394 | margin-top: 45px; 395 | background: #fff; 396 | } 397 | 398 | .lego_content[data-target="primary"], 399 | .lego_content[data-target="overlay"]{ 400 | width: 1000px; 401 | background: #FFF4F8; 402 | } 403 | 404 | .lego_navbar { 405 | float: right; 406 | min-height: 40px; 407 | width: 730px; 408 | margin-right: 15px; 409 | margin-top: 15px; 410 | } 411 | 412 | .lego_right-column { 413 | float: right; 414 | width: 215px; 415 | min-height: 600px; 416 | margin: 0 15px; 417 | } 418 | 419 | /* default */ 420 | 421 | .lego_layer.__default .lego_content[data-target="overlay"] { 422 | display: inline-block; 423 | width: auto; 424 | min-width: 150px; 425 | padding: 10px; 426 | text-align: left; 427 | background: #fff; 428 | } 429 | 430 | .lego_layer.__default .lego_content_dec { 431 | display: flex; 432 | align-items: center; 433 | justify-content: center; 434 | text-align: center; 435 | padding-top: 50px; 436 | } 437 | 438 | .lego_layer.__default .lego_content_w { 439 | display: flex; 440 | justify-content: center; 441 | margin-top: 0; 442 | } 443 | 444 | /* one column */ 445 | .lego_layer.__one-column .lego_toolbar, 446 | .lego_layer.__one-column .lego_content[data-target="primary"] { 447 | display: block; 448 | } 449 | 450 | .lego_layer.__one-column .lego_content[data-target="primary"] { 451 | width: 970px; 452 | margin: 15px; 453 | min-height: 40px; 454 | } 455 | 456 | .lego_layer.__one-column .lego_navbar { 457 | display: block; 458 | width: auto; 459 | float: none; 460 | margin-left: 15px; 461 | } 462 | 463 | /* two column layout */ 464 | .lego_layer.__two-column .lego_navbar, 465 | .lego_layer.__two-column .lego_sidebar, 466 | .lego_layer.__two-column .lego_content[data-target="primary"], 467 | .lego_layer.__two-column .lego_toolbar { display: block; } 468 | 469 | .lego_layer.__two-column .lego_sidebar { 470 | width: 230px; 471 | } 472 | 473 | .lego_layer.__two-column .lego_navbar { 474 | width: 715px; 475 | margin-bottom: 15px; 476 | } 477 | 478 | .lego_layer.__two-column .lego_content[data-target="primary"] { 479 | float: right; 480 | width: 715px; 481 | margin-right: 15px; 482 | min-height: 40px; 483 | text-align: left; 484 | } 485 | 486 | /* three column */ 487 | .lego_layer.__three-column .lego_navbar, 488 | .lego_layer.__three-column .lego_sidebar, 489 | .lego_layer.__three-column .lego_content[data-target="primary"], 490 | .lego_layer.__three-column .lego_toolbar, 491 | .lego_layer.__three-column .lego_right-column { display: block; } 492 | 493 | .lego_layer.__three-column .lego_content[data-target="primary"] { 494 | float: right; 495 | width: 500px; 496 | min-height: 40px; 497 | } 498 | 499 | .lego_layer.__three-column .lego_navbar { 500 | margin-bottom: 15px; 501 | } 502 | 503 | /* /Project 504 | ---------------------------------------------------------------------------------- */ 505 | 506 | /* temp */ 507 | 508 | .editable { 509 | box-shadow: 0 0 10px rgba(255,0,0,.5); 510 | } 511 | 512 | [data-active="true"] { 513 | outline: 1px solid #0CF; 514 | } 515 | [data-target="overlay"] [data-active="true"] { 516 | outline: 0 none; 517 | } 518 | 519 | [data-url]:before, 520 | [data-url]:after { 521 | content: " "; 522 | display: table; 523 | } 524 | 525 | [data-url]:after { 526 | clear: both; 527 | } 528 | 529 | [data-target="overlay"] > *:not([data-active="true"]) { 530 | position: absolute; 531 | top: -9999px; 532 | left: -9999px; 533 | } 534 | 535 | .__moved { 536 | position: absolute; 537 | opacity: 0.5; 538 | } -------------------------------------------------------------------------------- /assets/js/main.js: -------------------------------------------------------------------------------- 1 | /*global window */ 2 | 3 | (function (global) { 4 | "use strict"; 5 | 6 | var $ = global.jQuery; 7 | var console = global.console; 8 | var modifiers = global.lego.modifiers; 9 | var clientTemplates = global.lego.clientTemplates; 10 | 11 | var $body = $('body'); 12 | 13 | var possibleTargets = ['other', 'navbar', 'tertiary', 'secondary', 'primary']; 14 | var activeTargets = []; 15 | var acceptsHTML = false; 16 | 17 | // State variables 18 | var chosenNavigation; 19 | var $dragged = false; 20 | 21 | /* --- Служебные функции --- */ 22 | 23 | // 0 или пусто - overlay (добавляется автоматически) 24 | // 1 - primary 25 | // 2 - secondary 26 | // 4 - tertiary 27 | // 8 - navbar 28 | // 16 - other 29 | var parseTargets = function (targets) { 30 | var result = []; 31 | var mask, i; 32 | for (mask = 16, i = 0; mask > 0; mask >>= 1, i++) { 33 | if (mask && targets) { 34 | result.push(possibleTargets[i]); 35 | } 36 | } 37 | 38 | // Предполагаем, что если цель явно не задана, элемент может упасть в произвольный контейнер 39 | if (!result.length) { 40 | result = possibleTargets.slice(0); 41 | } 42 | 43 | result.push('overlay'); 44 | return result; 45 | }; 46 | 47 | //Clearing chosen 48 | var clearChosen = function () { 49 | chosenNavigation = false; 50 | $('.lego_layer *').removeClass('editable'); 51 | }; 52 | 53 | // Вставка нового блока 54 | var insertChosen = function ($targetContainer) { 55 | 56 | if (chosenNavigation) { 57 | var name = chosenNavigation.text(); 58 | var shortUrl = chosenNavigation.attr('data-spec-id'); 59 | var fullUrl = global.lego.parsedTree[shortUrl].url; 60 | 61 | var currentHTML = $('
    ').attr('data-container', acceptsHTML); 62 | var menuItem; 63 | 64 | // Создадим новый виртуальный блок 65 | var virtualBlock = new global.lego.VirtualBlock(shortUrl); 66 | var specId = virtualBlock.specId; 67 | 68 | // Добавляем новый элемент, сбросим признак активности 69 | modifiers.clearActiveNode(); 70 | 71 | $targetContainer.append(currentHTML); 72 | 73 | // Добавить ссылку на элемент в правое меню 74 | menuItem = clientTemplates['element-item']({ 75 | blockId: virtualBlock.id, 76 | specLink: fullUrl, 77 | specName: name 78 | }); 79 | $("#current-elements").append(menuItem); 80 | 81 | // Работа с виртуальным блоком и рендер 82 | modifiers.getSpecHTML(specId, function () { 83 | modifiers 84 | .generateVariationsList(specId) 85 | .generateModificatorsList(virtualBlock.id) 86 | .setupVariationsList(virtualBlock.id) 87 | .setupModificatorsList(virtualBlock.id) 88 | .render(virtualBlock.id); 89 | }); 90 | 91 | // После добавления элемента скрыть сетку 92 | $(".lego_layer").addClass('__hide-bg'); 93 | } 94 | 95 | // Сбросить информацию для добавления блока в контейнер 96 | clearChosen(); 97 | }; 98 | 99 | /* --- Инициализация загрузки модификаторов --- */ 100 | modifiers.init(); 101 | 102 | /* --- Обработчики кликов --- */ 103 | 104 | $('#export-img').on('click', function (e) { 105 | e.preventDefault(); 106 | 107 | console.log('In development...'); 108 | }); 109 | 110 | $('#save-html').on('click', function (e) { 111 | e.preventDefault(); 112 | 113 | var html = $('#working-space')[0].outerHTML; 114 | var $saveHtmlLk = $('#save-html-lk'); 115 | 116 | var data = { 117 | html: html 118 | }; 119 | 120 | var hostUrl = global.location.host; 121 | 122 | $.ajax({ 123 | url: '/save', 124 | cache: false, 125 | data: data, 126 | success: function (data) { 127 | if (data.success) { 128 | if ($saveHtmlLk.length === 0) { 129 | $('#save-html').after(''); 130 | } else { 131 | $saveHtmlLk.val(hostUrl + '/clean/' + data.name); 132 | } 133 | 134 | $saveHtmlLk.trigger('select'); 135 | } 136 | } 137 | }); 138 | }); 139 | 140 | // Компонент тогглер-переключатель 141 | $(".lego_toggler").on("click", ".lego_toggle_i", function () { 142 | var targetNode = $('.lego_search-result.__layout'); 143 | 144 | $(this) 145 | .addClass("__active") 146 | .siblings() 147 | .removeClass('__active'); 148 | 149 | if ($(this).attr('data-target') === 'list') { 150 | targetNode.addClass("__list"); 151 | } else { 152 | targetNode.removeClass("__list"); 153 | } 154 | 155 | }); 156 | 157 | // Вставить элемент в подсвеченную область, доступную для работы 158 | $body.on("click", ".editable", function (e) { 159 | e.stopPropagation(); // Не нужно сбрасывать фокус, это происходит при всплытии клика на контейнер 160 | insertChosen($(this)); 161 | }); 162 | 163 | // Клик по результатам поиска — выбор блока для вставки 164 | $("#lego_search-result").on("click", ".lego_search-result_i", function (e) { 165 | if (e.metaKey || e.ctrlKey) { 166 | return; 167 | } 168 | 169 | e.preventDefault(); 170 | 171 | chosenNavigation = $(this); 172 | acceptsHTML = chosenNavigation.data('accepts'); 173 | 174 | var i, j; 175 | 176 | // Инициализация областей, принимающих контент 177 | if (activeTargets.length) { 178 | for (i = 0; i < activeTargets.length; i++) { 179 | activeTargets[i].removeClass('editable'); 180 | } 181 | activeTargets = []; 182 | } 183 | 184 | var targets = $(this)[0].dataset.target; 185 | var results = parseTargets(targets); 186 | 187 | for (j = 0; j < results.length; j++) { 188 | var elem = $('[data-target="' + results[j] + '"]'); 189 | activeTargets.push(elem); 190 | elem.addClass('editable'); 191 | } 192 | 193 | // Обходим все блоки, способные принимать в себя контент 194 | $('[data-container="true"]').each(function () { 195 | activeTargets.push($(this)); 196 | $(this).addClass('editable'); 197 | $(this).find('div, span').each(function () { 198 | activeTargets.push($(this)); 199 | $(this).addClass('editable'); 200 | }); 201 | }); 202 | 203 | // Вставка выбранного блока 204 | if (activeTargets.length < 3) { 205 | for (i = 0; i < activeTargets.length; i++) { 206 | if (activeTargets[i].is(':visible')) { 207 | insertChosen(activeTargets[i]); 208 | } 209 | } 210 | } else if ($('[data-target="overlay"]:visible').length !== 0 || $('[data-target="container"]:visible').length !== 0) { 211 | for (i = 0; i < activeTargets.length; i++) { 212 | if (activeTargets[i].selector === '[data-target="overlay"]') { 213 | insertChosen(activeTargets[i]); 214 | } 215 | } 216 | } 217 | }); 218 | 219 | // Переключение режимов сетки 220 | $body.on('click', '.js-layouts-list .lego_search-result_i', function (e) { 221 | e.preventDefault(); 222 | 223 | var layerMode = $(this).find('.lego_search-result_n').data('layout'); 224 | 225 | $('.lego_layer') 226 | .removeClass('__default __one-column __two-column __three-column __hide-bg') 227 | .addClass('__' + layerMode); 228 | }); 229 | 230 | // Подсветить блоки по клику 231 | $body.on('click', '.lego_main [data-id]', function (e) { 232 | if (e.metaKey || e.ctrlKey) { 233 | return; 234 | } 235 | 236 | var virtualBlockId = $(this).attr('data-id') || $(this).closest('.lego_main [data-id]').attr('data-id'); 237 | 238 | modifiers 239 | .generateVariationsList(global.lego.elementList[virtualBlockId].specId) 240 | .generateModificatorsList(virtualBlockId) 241 | .setupVariationsList(virtualBlockId) 242 | .setupModificatorsList(virtualBlockId) 243 | .setActiveNode(virtualBlockId); // Перерендер по клику не нужен 244 | 245 | return false; 246 | }); 247 | 248 | // Обработка кликов по вариациям и модификаторам в сайдбаре 249 | $body.on('click', '.lego_checkbox', function () { 250 | 251 | var $activeNode = modifiers.getActiveNode(); 252 | var activeVirtualBlockId = $activeNode.attr('data-id'); 253 | var activeVirtualBlock = global.lego.elementList[activeVirtualBlockId]; 254 | 255 | if ($(this).attr('name') === 'variations') { 256 | 257 | // Получить номер выбранной вариации 258 | var variationValue = $(this).attr('data-variation'); 259 | 260 | // Сохранить номер в модели блока 261 | activeVirtualBlock.save({ 262 | variation: variationValue 263 | }); 264 | 265 | // Перерендерить список модификаторов в соответствии с новой вариацией 266 | modifiers 267 | .generateModificatorsList(activeVirtualBlockId) 268 | .setupModificatorsList(activeVirtualBlockId); 269 | 270 | } else { 271 | var variationExport = {}; 272 | 273 | // Собрать новый набор модификаторов 274 | $('.js-modificators .lego_expand').each(function () { 275 | var blockTitle = $(this).children('.lego_expand_h').text(); 276 | 277 | variationExport[blockTitle] = []; 278 | 279 | $(this).find('.lego_checkbox').each(function () { 280 | if ($(this).is(':checked')) { 281 | variationExport[blockTitle].push($(this).attr('data-mod')); 282 | } 283 | }); 284 | }); 285 | 286 | // Сохранить новый набор модификаторов 287 | activeVirtualBlock.save({ 288 | modifiers: variationExport, 289 | changed: true 290 | }); 291 | } 292 | 293 | // Перерендерить с учетом изменений 294 | modifiers.render(); 295 | }); 296 | 297 | // Обработка кликов переключения блоков 298 | $body.on('click', '#current-elements .lego_lk', function (e) { 299 | if (e.metaKey || e.ctrlKey) { 300 | return; 301 | } 302 | 303 | e.preventDefault(); 304 | 305 | var virtualBlockId = $(this).parent().attr('data-id'); 306 | var $allLegoLk = $("#current-elements").find(".lego_lk"); 307 | 308 | if (virtualBlockId) { 309 | $allLegoLk.removeClass("__active"); 310 | $(this).addClass('__active'); 311 | 312 | modifiers 313 | .generateVariationsList(global.lego.elementList[virtualBlockId].specId) 314 | .generateModificatorsList(virtualBlockId) 315 | .setupVariationsList(virtualBlockId) 316 | .setupModificatorsList(virtualBlockId) 317 | .render(virtualBlockId); 318 | } 319 | }); 320 | 321 | // Обработка кликов по иконке удаления блока 322 | $body.on('click', '.lego_ic_close', function () { 323 | 324 | var activeBlockId = modifiers.getActiveNode().attr('data-id'); 325 | var $listItem = $(this).parent('.lego_widget_ul-i'); 326 | var virtualBlockId = $listItem.attr('data-id'); 327 | var $blockNode = $('.lego_main [data-id="' + virtualBlockId + '"]'); 328 | var $candidatListItem = $listItem.prev(); 329 | var candidatVirtualBlockId = false; 330 | 331 | // По умолчанию при удалении переключаемся на предыдущий элемент, 332 | // Но если его нет, то пытаемся на следующий 333 | if (!$candidatListItem.length) { 334 | $candidatListItem = $listItem.next(); 335 | } 336 | 337 | if (delete global.lego.elementList[virtualBlockId]) { 338 | 339 | $blockNode.remove(); // удалить блок с холста 340 | $listItem.remove(); // удалить элемент в списке 341 | 342 | // Переключиться на новый блок и отрендерить его в том случае, 343 | // если на холсте вообще остаются какие-либо элементы 344 | // и при этом удаляемый элемент является активным 345 | if ($candidatListItem.length && activeBlockId === virtualBlockId) { 346 | candidatVirtualBlockId = $candidatListItem.attr('data-id'); 347 | 348 | modifiers 349 | .generateVariationsList(global.lego.elementList[candidatVirtualBlockId].specId) 350 | .generateModificatorsList(candidatVirtualBlockId) 351 | .setupVariationsList(candidatVirtualBlockId) 352 | .setupModificatorsList(candidatVirtualBlockId) 353 | .render(candidatVirtualBlockId); 354 | } else { 355 | 356 | // Если элементов нет, очистить сайдбар 357 | if (!Object.keys(global.lego.elementList).length) { 358 | modifiers 359 | .generateVariationsList() 360 | .generateModificatorsList(); 361 | } 362 | } 363 | } 364 | }); 365 | 366 | $body.on('click', '.lego_main', function () { 367 | // Очищать выделение только при наличии сетки 368 | if (!$('.lego_layer.__default').length) { 369 | $('.lego_layer').addClass('__hide-bg'); 370 | modifiers.clearActiveNode(); 371 | } 372 | }); 373 | 374 | /* Drag and drop */ 375 | $body.on('dragstart', function (e) { 376 | $dragged = $(e.target); 377 | 378 | // Не запускать в режиме по умолчанию 379 | if ($('.lego_layer.__default').length) { 380 | return false; 381 | } 382 | 383 | // Перетаскивание блока на холсте 384 | if ($dragged.attr('data-id') !== undefined) { 385 | $('.lego_layer').removeClass('__hide-bg'); 386 | e.target.style.opacity = 0.5; 387 | } else { 388 | // Перетаскивание блока из нав.меню 389 | if ($dragged.attr('data-spec-id') !== undefined) { 390 | $('.lego_layer').removeClass('__hide-bg'); 391 | chosenNavigation = $dragged; 392 | } else { 393 | $dragged = false; 394 | e.preventDefault(); 395 | } 396 | } 397 | }); 398 | 399 | $body.on('dragend', function (e) { 400 | if ($dragged) { 401 | $('.lego_layer').addClass('__hide-bg'); 402 | e.target.style.opacity = ""; 403 | } 404 | }); 405 | 406 | $body.on('dragover', function (e) { 407 | // prevent default to allow drop 408 | e.preventDefault(); 409 | }); 410 | 411 | $body.on('dragenter', function (e) { 412 | e.preventDefault(); 413 | 414 | if ($dragged) { 415 | var $closestDataTarget = $(e.target).closest('[data-target]'); 416 | 417 | if ($closestDataTarget.length) { 418 | $closestDataTarget.addClass('editable'); 419 | } 420 | } 421 | }); 422 | 423 | $body.on('dragleave', function (e) { 424 | if ($dragged) { 425 | var $closestDataTarget = $(e.target).closest('[data-target]'); 426 | 427 | if ($closestDataTarget.length) { 428 | $closestDataTarget.removeClass('editable'); 429 | } 430 | } 431 | }); 432 | 433 | $body.on('drop', function (e) { 434 | e.preventDefault(); 435 | if ($dragged) { 436 | var $closestDataTarget = $(e.target).closest('[data-target]'); 437 | if ($closestDataTarget.length) { 438 | if ($dragged.attr('data-id') !== undefined) { 439 | $closestDataTarget 440 | .append($dragged) 441 | .removeClass('editable'); 442 | } else if ($dragged.attr('data-spec-id')) { 443 | insertChosen($closestDataTarget); 444 | $closestDataTarget 445 | .removeClass('editable'); 446 | } 447 | } 448 | } 449 | }); 450 | 451 | }(window)); -------------------------------------------------------------------------------- /assets/js/modifiers.js: -------------------------------------------------------------------------------- 1 | /*global window */ 2 | 3 | (function (global) { 4 | "use strict"; 5 | 6 | var allModifiers = false; // Хранит объект всех блоков, элементов и модификаторов 7 | var $ = global.jQuery; 8 | var globalOptions = global.lego.globalOptions; 9 | var component = global.lego.component; 10 | var clientTemplates = global.lego.clientTemplates; 11 | 12 | // Получает все доступные модификаторы для всех блоков и эдементов 13 | function getCSSMod(callback) { 14 | var cb = callback || function () {}; 15 | 16 | if (!allModifiers) { 17 | $.ajax({ 18 | url: "/cssmod", 19 | type: 'POST', 20 | data: JSON.stringify({ 21 | // Массив в перечислением css-файлов для анализа 22 | // Может быть импортирован из глобальных настроек или задан вручную 23 | files: globalOptions.cssMod.files 24 | }), 25 | dataType: 'json', 26 | contentType: "application/json", // нужен для обработки параметров POST-запроса в express 27 | success: function (data) { 28 | allModifiers = $.extend({}, data, true); 29 | cb(); 30 | } 31 | }); 32 | } else { 33 | cb(); 34 | } 35 | } 36 | 37 | // Получает HTML-шаблон блока 38 | function getHTMLpart(specId, callback) { 39 | var cb = callback || function () {}; 40 | 41 | // Если в глобальном объекте спек нет экземпляра с этим id, выкачать и сохранить, иначе отдать готовый 42 | if (!global.lego.specList[specId]) { 43 | var specsMaster = globalOptions.specsMaster.current; 44 | var customDataUrl = globalOptions.specsMaster.customDataUrl; 45 | 46 | var processData = function (data) { 47 | var flatSections = []; 48 | var tempNode; 49 | var i; 50 | 51 | // Нам нужно развернуть древовидную структуру «секция[]»-«подсекция[]»—...—«примеры[]» в плоский массив 52 | // Все html хранятся в глобальном объекте спецификаций, индекс — из параметра 53 | (function flatten(target) { 54 | 55 | for (i = 0; i < target.length; i++) { 56 | 57 | // Выбросим блоки, в которых нет html-кода 58 | if (target[i].html.length) { 59 | 60 | // Для хорошей вставки шаблона нужно, чтобы был только один корневой узел 61 | tempNode = '
    ' + target[i].html[0] + '
    '; 62 | if ($(tempNode).children().length > 1) { 63 | target[i].html = [tempNode]; 64 | } 65 | 66 | flatSections.push(target[i]); 67 | } 68 | 69 | if (target[i].nested && target[i].nested.length) { 70 | flatten(target[i].nested); 71 | } 72 | } 73 | }(data.contents)); 74 | 75 | global.lego.specList[specId] = flatSections; 76 | cb(global.lego.specList[specId]); 77 | }; 78 | 79 | if (customDataUrl) { 80 | processData(global.lego.parsedTree[specId]); 81 | } else { 82 | $.ajax(specsMaster + '/api/specs/html', { 83 | data: { 84 | id: specId 85 | }, 86 | dataType: "json", 87 | type: 'GET', 88 | success: processData 89 | }); 90 | } 91 | } else { 92 | cb(global.lego.specList[specId]); 93 | } 94 | } 95 | 96 | 97 | // Применяет атрибуты виртуального блока к DOM-узлу 98 | function applyAttributes(virtualBlockId, $node) { 99 | var blockModifiers = global.lego.elementList[virtualBlockId].modifiers; 100 | var currentBlock; 101 | var currentModifier; 102 | 103 | for (currentBlock in blockModifiers) { 104 | 105 | if (blockModifiers.hasOwnProperty(currentBlock)) { 106 | var childBlocks = $node.find('.' + currentBlock); 107 | 108 | var allBlocksModifiers = allModifiers[currentBlock]; 109 | var usedModifiers = blockModifiers[currentBlock]; 110 | 111 | // Применяем к детям в неограниченном количестве 112 | // Эксперимент: сбросим исходные модификаторы 113 | childBlocks.each(function () { 114 | 115 | // TODO: remove mods removal 116 | for (currentModifier = 0; currentModifier < allBlocksModifiers.length; currentModifier++) { 117 | $(this).removeClass(allBlocksModifiers[currentModifier]); 118 | } 119 | }); 120 | 121 | // Эксперимент: применять модификатор не ко всем подходящим элементам, а только к одному — первому 122 | for (currentModifier = 0; currentModifier < usedModifiers.length; currentModifier++) { 123 | childBlocks.eq(0).addClass(usedModifiers[currentModifier]); 124 | } 125 | } 126 | } 127 | } 128 | 129 | 130 | // Просматриваем структуру на предмет вариаций, и на основе полученного 131 | // генерируем список вариаций в правом сайдбаре 132 | function generateVariationList(specId) { 133 | 134 | var $wrap = $('.js-variations .lego_form-i'); 135 | var template; 136 | 137 | $wrap.empty(); 138 | 139 | if (specId === undefined) { 140 | return; 141 | } 142 | 143 | // Рендер секции в сайдбаре 144 | template = clientTemplates['variation-item'](global.lego.specList[specId]); 145 | $wrap.append(template); 146 | } 147 | 148 | 149 | // Определение проектного класса на основе сопоставления иерархии разметки и присутствия 150 | // классов в дереве модификаторов (рассматриваются все классы в блоке) 151 | function detectBlockClassName(allSelectors) { 152 | var blockDetect = new RegExp(globalOptions.cssMod.rules.blockRule); 153 | var result = ''; 154 | var currentSelector; 155 | var curClassList; 156 | 157 | // По всем селекторам, содержащим класс 158 | for (currentSelector = 0; currentSelector < allSelectors.length; currentSelector++) { 159 | var currentSelectorClassList = allSelectors[currentSelector].classList; 160 | 161 | if (result) { 162 | break; 163 | } 164 | 165 | // По всем классам в полученных селекторах 166 | for (curClassList = 0; curClassList < currentSelectorClassList.length; curClassList++) { 167 | var currElem = currentSelectorClassList[curClassList]; 168 | 169 | if (blockDetect.test(currElem)) { 170 | result = currElem; 171 | break; 172 | } 173 | } 174 | } 175 | 176 | return result; 177 | } 178 | 179 | // Поиск доступных для блока+элементов модификаторов и рендер в правом сайдбаре 180 | function generateModificatorsList(virtualBlockId) { 181 | 182 | var $wrap = $('.js-modificators .lego_form-i'); 183 | var usedModifiers = []; 184 | var linksBlockToExpand = {}; 185 | var template; 186 | 187 | $wrap.empty(); 188 | 189 | if (virtualBlockId === undefined) { 190 | return; 191 | } 192 | 193 | var virtualBlockSpecId = global.lego.elementList[virtualBlockId].specId; 194 | var virtualBlockVariation = global.lego.elementList[virtualBlockId].variation; 195 | var virtualBlockHTML = global.lego.specList[virtualBlockSpecId][virtualBlockVariation].html[0]; // только первый source_example 196 | 197 | // Создадим временный узел для работы с классами через DOM 198 | $('body').append('
    ' + virtualBlockHTML + '
    '); 199 | 200 | // Дальше будет удобно работать с массивом классов, переходим на DOM ClassList 201 | var allSelectors = global.document.querySelectorAll('.temp-node [class]'); 202 | 203 | // Если работа со вложенными блоками выключена, 204 | // попробуем определить проектный класс 205 | var projectClass = detectBlockClassName(allSelectors); 206 | var baseClass = !globalOptions.modifyInnerBlocks 207 | ? projectClass 208 | : ''; 209 | 210 | var currentSelector; 211 | 212 | // По всем селекторам, содержащим класс 213 | for (currentSelector = 0; currentSelector < allSelectors.length; currentSelector++) { 214 | var currentSelectorClassList = allSelectors[currentSelector].classList; 215 | var curClassList; 216 | 217 | // перебор классов в класслисте 218 | for (curClassList = 0; curClassList < currentSelectorClassList.length; curClassList++) { 219 | 220 | // Выбираются только те, которые принадлежат подмножеству проектного класса 221 | if (currentSelectorClassList[curClassList].indexOf(baseClass) !== -1) { 222 | 223 | var currElem = currentSelectorClassList[curClassList]; 224 | 225 | // Если блок обладает модификаторами 226 | if ((allModifiers[currElem])) { 227 | var currentModifier; 228 | 229 | for (currentModifier = 0; currentModifier < allModifiers[currElem].length; currentModifier++) { 230 | 231 | // если такой блок+модификатор уже использовался, выйти 232 | if (usedModifiers.indexOf(currElem + allModifiers[currElem][currentModifier]) !== -1) { 233 | continue; 234 | } 235 | usedModifiers.push(currElem + allModifiers[currElem][currentModifier]); 236 | 237 | if (!linksBlockToExpand[currElem]) { 238 | var isClosed = false; 239 | if (Object.keys(linksBlockToExpand).length) { 240 | isClosed = true; 241 | } 242 | linksBlockToExpand[currElem] = component.expand.create($wrap, currElem, isClosed); 243 | } 244 | 245 | template = clientTemplates['modifier-item']({ 246 | element: currElem, 247 | modifier: allModifiers[currentSelectorClassList[curClassList]][currentModifier], 248 | modifierName: allModifiers[currElem][currentModifier] 249 | }); 250 | component.expand.append(linksBlockToExpand[currElem], template); 251 | } 252 | } 253 | } 254 | } 255 | } 256 | 257 | // Удаляем временный блок 258 | $('.temp-node').remove(); 259 | } 260 | 261 | // Перещелкнуть пункт в меню 262 | function setupBlockList(virtualBlockId) { 263 | // Перещелкнуть активную ссылку на блок в меню 264 | var $currentElements = $('#current-elements'); 265 | 266 | $currentElements.find('.lego_lk').removeClass('__active'); 267 | 268 | if (virtualBlockId) { 269 | $currentElements.find('.lego_widget_ul-i[data-id="' + virtualBlockId + '"] .lego_lk').addClass('__active'); 270 | } 271 | } 272 | 273 | // Проставляет активную вариацию в сайдбаре 274 | function setupVariationsList(virtualBlockId) { 275 | 276 | if (virtualBlockId === undefined) { 277 | return; 278 | } 279 | 280 | $('.js-variations .lego_form-i_w') 281 | .eq(global.lego.elementList[virtualBlockId].variation) 282 | .find('input') 283 | .prop('checked', true); 284 | 285 | } 286 | 287 | // Проставляет галочки модификаторов для хтмл из вариации 288 | function setupModificatorsList(virtualBlockId) { 289 | var modifiersData = {}; 290 | 291 | if (virtualBlockId === undefined) { 292 | return; 293 | } 294 | 295 | var virtualBlock = global.lego.elementList[virtualBlockId]; 296 | var virtualBlockSpecId = virtualBlock.specId; 297 | var virtualBlockVariation = virtualBlock.variation; 298 | var virtualBlockHTML = '
    ' + global.lego.specList[virtualBlockSpecId][virtualBlockVariation].html[0] + '
    '; // только первый source_example 299 | var $virtualBlockHTML = $(virtualBlockHTML); 300 | var block; 301 | 302 | $('input[name="modificators"]').each(function () { 303 | 304 | var modValue = $(this).attr('data-mod'); 305 | var elemValue = $(this).attr('data-elem'); 306 | var $affectedNodes = $virtualBlockHTML.find('.' + elemValue + '.' + modValue); 307 | 308 | $(this).prop('checked', false); 309 | 310 | if ($affectedNodes.length) { 311 | $(this).prop('checked', true); 312 | 313 | if (!modifiersData[elemValue]) { 314 | modifiersData[elemValue] = []; 315 | } 316 | 317 | modifiersData[elemValue].push(modValue); 318 | } 319 | }); 320 | 321 | // Восстановим данные из виртуального блока, если они там существуют 322 | if (Object.keys(virtualBlock.modifiers).length) { 323 | for (block in virtualBlock.modifiers) { 324 | if (virtualBlock.modifiers.hasOwnProperty(block)) { 325 | var modifier; 326 | $('input[name="modificators"][data-elem="' + block + '"]').prop('checked', false); 327 | 328 | for (modifier = 0; modifier < virtualBlock.modifiers[block].length; modifier++) { 329 | $('input[name="modificators"][data-elem="' + block + '"][data-mod="' + virtualBlock.modifiers[block][modifier] + '"]').prop('checked', true); 330 | } 331 | } 332 | } 333 | 334 | modifiersData = virtualBlock.modifiers; 335 | } 336 | 337 | // Сохраним полученные в результате анализа вариации модификаторы в модели 338 | virtualBlock.save({ 339 | modifiers: modifiersData 340 | }); 341 | } 342 | 343 | // Получить активную ноду 344 | function getActiveNode() { 345 | return $('.lego_main [data-active="true"]').eq(0); 346 | } 347 | 348 | // Сбросить фокус с активной ноды 349 | function clearActiveNode() { 350 | getActiveNode().removeAttr('data-active'); 351 | } 352 | 353 | // Поставить фокус на узел 354 | function setActiveNode(virtualBlockId) { 355 | clearActiveNode(); 356 | 357 | if (!virtualBlockId) { 358 | return; 359 | } 360 | 361 | $('.lego_main [data-id="' + virtualBlockId + '"]').attr('data-active', true); 362 | 363 | // Подсветить в списке блоков 364 | setupBlockList(virtualBlockId); 365 | } 366 | 367 | // Отрисовывает блок с учетом диффа 368 | function render(virtualBlockId) { 369 | var $activeElement = ''; 370 | 371 | // Если блок для отрисовки не указан явно, накатываем изменения на текущий активный блок 372 | if (!virtualBlockId) { 373 | $activeElement = getActiveNode(); 374 | virtualBlockId = $activeElement.attr('data-id'); 375 | } else { 376 | // Приоритет узла за блоком с заданным id, однако при инициализации такого атрибута может еще не быть 377 | $activeElement = $('.lego_main [data-id="' + virtualBlockId + '"]'); 378 | 379 | if (!$activeElement.length) { 380 | $activeElement = getActiveNode(); 381 | } 382 | } 383 | 384 | // Может оказаться, что рендерить нечего 385 | if (!$activeElement.length) { 386 | return; 387 | } 388 | 389 | // Мы знаем, с каким элементом мы работаем, можно удалить признак активности 390 | clearActiveNode(); 391 | 392 | var virtualBlock = global.lego.elementList[virtualBlockId]; 393 | var virtualBlockSpecId = virtualBlock.specId; 394 | var virtualBlockVariation = virtualBlock.variation; 395 | var virtualBlockOriginHTML = global.lego.specList[virtualBlockSpecId][virtualBlockVariation].html[0]; // Только первый source_example 396 | 397 | // Создадим временный блок и применим к нему дифф из виртуального блока 398 | var $tempHTML = $('
    ' + virtualBlockOriginHTML + '
    '); 399 | 400 | // Накладывать будем только на измененный блок 401 | if (virtualBlock.changed) { 402 | applyAttributes(virtualBlockId, $tempHTML); 403 | } 404 | 405 | // На самом деле нам интересно только содержимое временного блока 406 | $tempHTML = $tempHTML.children(); 407 | 408 | $tempHTML 409 | .attr('draggable', true) 410 | .attr('data-active', true) 411 | .attr('data-url', virtualBlockSpecId) 412 | .attr('data-id', virtualBlockId); 413 | 414 | $activeElement.replaceWith($tempHTML); 415 | 416 | // Перещелкнуть список блоков 417 | setupBlockList(virtualBlockId); 418 | } 419 | 420 | var modifiers = global.lego.modifiers = { 421 | init: function (callback) { 422 | getCSSMod(callback); 423 | return this; 424 | }, 425 | 426 | getSpecHTML: function (specId, callback) { 427 | var cb = callback || function () {}; 428 | 429 | // Если модификаторы не загружены, загрузить и работать дальше 430 | getCSSMod(function () { 431 | // Получить вариации спецификации 432 | getHTMLpart(specId, function () { 433 | cb(); 434 | }); 435 | }); 436 | 437 | return this; 438 | }, 439 | 440 | generateVariationsList: function (specId) { 441 | generateVariationList(specId); 442 | 443 | return this; 444 | }, 445 | 446 | generateModificatorsList: function (virtualBlockId) { 447 | generateModificatorsList(virtualBlockId); 448 | 449 | return this; 450 | }, 451 | 452 | setupVariationsList: function (virtualBlockId) { 453 | setupVariationsList(virtualBlockId); 454 | 455 | return this; 456 | }, 457 | 458 | setupModificatorsList: function (virtualBlockId) { 459 | setupModificatorsList(virtualBlockId); 460 | 461 | return this; 462 | }, 463 | 464 | 465 | getActiveNode: function () { 466 | 467 | return getActiveNode(); 468 | }, 469 | 470 | clearActiveNode: function () { 471 | // Для публичного метода делаем больше работы: 472 | // сбрасываем фокус, очищаем правый сайдбар и сбрасываем выделение в списке блоков 473 | 474 | clearActiveNode(); 475 | generateVariationList(); 476 | generateModificatorsList(); 477 | setupBlockList(); 478 | 479 | return this; 480 | }, 481 | 482 | setActiveNode: function (virtualBlockId) { 483 | setActiveNode(virtualBlockId); 484 | 485 | return this; 486 | }, 487 | 488 | render: function (virtualBlockId) { 489 | render(virtualBlockId); 490 | 491 | return this; 492 | } 493 | }; 494 | }(window)); -------------------------------------------------------------------------------- /user-bootstrap/data/bootstrap/js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v3.2.0 (http://getbootstrap.com) 3 | * Copyright 2011-2014 Twitter, Inc. 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | if("undefined"==typeof jQuery)throw new Error("Bootstrap's JavaScript requires jQuery");+function(a){"use strict";function b(){var a=document.createElement("bootstrap"),b={WebkitTransition:"webkitTransitionEnd",MozTransition:"transitionend",OTransition:"oTransitionEnd otransitionend",transition:"transitionend"};for(var c in b)if(void 0!==a.style[c])return{end:b[c]};return!1}a.fn.emulateTransitionEnd=function(b){var c=!1,d=this;a(this).one("bsTransitionEnd",function(){c=!0});var e=function(){c||a(d).trigger(a.support.transition.end)};return setTimeout(e,b),this},a(function(){a.support.transition=b(),a.support.transition&&(a.event.special.bsTransitionEnd={bindType:a.support.transition.end,delegateType:a.support.transition.end,handle:function(b){return a(b.target).is(this)?b.handleObj.handler.apply(this,arguments):void 0}})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var c=a(this),e=c.data("bs.alert");e||c.data("bs.alert",e=new d(this)),"string"==typeof b&&e[b].call(c)})}var c='[data-dismiss="alert"]',d=function(b){a(b).on("click",c,this.close)};d.VERSION="3.2.0",d.prototype.close=function(b){function c(){f.detach().trigger("closed.bs.alert").remove()}var d=a(this),e=d.attr("data-target");e||(e=d.attr("href"),e=e&&e.replace(/.*(?=#[^\s]*$)/,""));var f=a(e);b&&b.preventDefault(),f.length||(f=d.hasClass("alert")?d:d.parent()),f.trigger(b=a.Event("close.bs.alert")),b.isDefaultPrevented()||(f.removeClass("in"),a.support.transition&&f.hasClass("fade")?f.one("bsTransitionEnd",c).emulateTransitionEnd(150):c())};var e=a.fn.alert;a.fn.alert=b,a.fn.alert.Constructor=d,a.fn.alert.noConflict=function(){return a.fn.alert=e,this},a(document).on("click.bs.alert.data-api",c,d.prototype.close)}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.button"),f="object"==typeof b&&b;e||d.data("bs.button",e=new c(this,f)),"toggle"==b?e.toggle():b&&e.setState(b)})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.isLoading=!1};c.VERSION="3.2.0",c.DEFAULTS={loadingText:"loading..."},c.prototype.setState=function(b){var c="disabled",d=this.$element,e=d.is("input")?"val":"html",f=d.data();b+="Text",null==f.resetText&&d.data("resetText",d[e]()),d[e](null==f[b]?this.options[b]:f[b]),setTimeout(a.proxy(function(){"loadingText"==b?(this.isLoading=!0,d.addClass(c).attr(c,c)):this.isLoading&&(this.isLoading=!1,d.removeClass(c).removeAttr(c))},this),0)},c.prototype.toggle=function(){var a=!0,b=this.$element.closest('[data-toggle="buttons"]');if(b.length){var c=this.$element.find("input");"radio"==c.prop("type")&&(c.prop("checked")&&this.$element.hasClass("active")?a=!1:b.find(".active").removeClass("active")),a&&c.prop("checked",!this.$element.hasClass("active")).trigger("change")}a&&this.$element.toggleClass("active")};var d=a.fn.button;a.fn.button=b,a.fn.button.Constructor=c,a.fn.button.noConflict=function(){return a.fn.button=d,this},a(document).on("click.bs.button.data-api",'[data-toggle^="button"]',function(c){var d=a(c.target);d.hasClass("btn")||(d=d.closest(".btn")),b.call(d,"toggle"),c.preventDefault()})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.carousel"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b),g="string"==typeof b?b:f.slide;e||d.data("bs.carousel",e=new c(this,f)),"number"==typeof b?e.to(b):g?e[g]():f.interval&&e.pause().cycle()})}var c=function(b,c){this.$element=a(b).on("keydown.bs.carousel",a.proxy(this.keydown,this)),this.$indicators=this.$element.find(".carousel-indicators"),this.options=c,this.paused=this.sliding=this.interval=this.$active=this.$items=null,"hover"==this.options.pause&&this.$element.on("mouseenter.bs.carousel",a.proxy(this.pause,this)).on("mouseleave.bs.carousel",a.proxy(this.cycle,this))};c.VERSION="3.2.0",c.DEFAULTS={interval:5e3,pause:"hover",wrap:!0},c.prototype.keydown=function(a){switch(a.which){case 37:this.prev();break;case 39:this.next();break;default:return}a.preventDefault()},c.prototype.cycle=function(b){return b||(this.paused=!1),this.interval&&clearInterval(this.interval),this.options.interval&&!this.paused&&(this.interval=setInterval(a.proxy(this.next,this),this.options.interval)),this},c.prototype.getItemIndex=function(a){return this.$items=a.parent().children(".item"),this.$items.index(a||this.$active)},c.prototype.to=function(b){var c=this,d=this.getItemIndex(this.$active=this.$element.find(".item.active"));return b>this.$items.length-1||0>b?void 0:this.sliding?this.$element.one("slid.bs.carousel",function(){c.to(b)}):d==b?this.pause().cycle():this.slide(b>d?"next":"prev",a(this.$items[b]))},c.prototype.pause=function(b){return b||(this.paused=!0),this.$element.find(".next, .prev").length&&a.support.transition&&(this.$element.trigger(a.support.transition.end),this.cycle(!0)),this.interval=clearInterval(this.interval),this},c.prototype.next=function(){return this.sliding?void 0:this.slide("next")},c.prototype.prev=function(){return this.sliding?void 0:this.slide("prev")},c.prototype.slide=function(b,c){var d=this.$element.find(".item.active"),e=c||d[b](),f=this.interval,g="next"==b?"left":"right",h="next"==b?"first":"last",i=this;if(!e.length){if(!this.options.wrap)return;e=this.$element.find(".item")[h]()}if(e.hasClass("active"))return this.sliding=!1;var j=e[0],k=a.Event("slide.bs.carousel",{relatedTarget:j,direction:g});if(this.$element.trigger(k),!k.isDefaultPrevented()){if(this.sliding=!0,f&&this.pause(),this.$indicators.length){this.$indicators.find(".active").removeClass("active");var l=a(this.$indicators.children()[this.getItemIndex(e)]);l&&l.addClass("active")}var m=a.Event("slid.bs.carousel",{relatedTarget:j,direction:g});return a.support.transition&&this.$element.hasClass("slide")?(e.addClass(b),e[0].offsetWidth,d.addClass(g),e.addClass(g),d.one("bsTransitionEnd",function(){e.removeClass([b,g].join(" ")).addClass("active"),d.removeClass(["active",g].join(" ")),i.sliding=!1,setTimeout(function(){i.$element.trigger(m)},0)}).emulateTransitionEnd(1e3*d.css("transition-duration").slice(0,-1))):(d.removeClass("active"),e.addClass("active"),this.sliding=!1,this.$element.trigger(m)),f&&this.cycle(),this}};var d=a.fn.carousel;a.fn.carousel=b,a.fn.carousel.Constructor=c,a.fn.carousel.noConflict=function(){return a.fn.carousel=d,this},a(document).on("click.bs.carousel.data-api","[data-slide], [data-slide-to]",function(c){var d,e=a(this),f=a(e.attr("data-target")||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""));if(f.hasClass("carousel")){var g=a.extend({},f.data(),e.data()),h=e.attr("data-slide-to");h&&(g.interval=!1),b.call(f,g),h&&f.data("bs.carousel").to(h),c.preventDefault()}}),a(window).on("load",function(){a('[data-ride="carousel"]').each(function(){var c=a(this);b.call(c,c.data())})})}(jQuery),+function(a){"use strict";function b(b){return this.each(function(){var d=a(this),e=d.data("bs.collapse"),f=a.extend({},c.DEFAULTS,d.data(),"object"==typeof b&&b);!e&&f.toggle&&"show"==b&&(b=!b),e||d.data("bs.collapse",e=new c(this,f)),"string"==typeof b&&e[b]()})}var c=function(b,d){this.$element=a(b),this.options=a.extend({},c.DEFAULTS,d),this.transitioning=null,this.options.parent&&(this.$parent=a(this.options.parent)),this.options.toggle&&this.toggle()};c.VERSION="3.2.0",c.DEFAULTS={toggle:!0},c.prototype.dimension=function(){var a=this.$element.hasClass("width");return a?"width":"height"},c.prototype.show=function(){if(!this.transitioning&&!this.$element.hasClass("in")){var c=a.Event("show.bs.collapse");if(this.$element.trigger(c),!c.isDefaultPrevented()){var d=this.$parent&&this.$parent.find("> .panel > .in");if(d&&d.length){var e=d.data("bs.collapse");if(e&&e.transitioning)return;b.call(d,"hide"),e||d.data("bs.collapse",null)}var f=this.dimension();this.$element.removeClass("collapse").addClass("collapsing")[f](0),this.transitioning=1;var g=function(){this.$element.removeClass("collapsing").addClass("collapse in")[f](""),this.transitioning=0,this.$element.trigger("shown.bs.collapse")};if(!a.support.transition)return g.call(this);var h=a.camelCase(["scroll",f].join("-"));this.$element.one("bsTransitionEnd",a.proxy(g,this)).emulateTransitionEnd(350)[f](this.$element[0][h])}}},c.prototype.hide=function(){if(!this.transitioning&&this.$element.hasClass("in")){var b=a.Event("hide.bs.collapse");if(this.$element.trigger(b),!b.isDefaultPrevented()){var c=this.dimension();this.$element[c](this.$element[c]())[0].offsetHeight,this.$element.addClass("collapsing").removeClass("collapse").removeClass("in"),this.transitioning=1;var d=function(){this.transitioning=0,this.$element.trigger("hidden.bs.collapse").removeClass("collapsing").addClass("collapse")};return a.support.transition?void this.$element[c](0).one("bsTransitionEnd",a.proxy(d,this)).emulateTransitionEnd(350):d.call(this)}}},c.prototype.toggle=function(){this[this.$element.hasClass("in")?"hide":"show"]()};var d=a.fn.collapse;a.fn.collapse=b,a.fn.collapse.Constructor=c,a.fn.collapse.noConflict=function(){return a.fn.collapse=d,this},a(document).on("click.bs.collapse.data-api",'[data-toggle="collapse"]',function(c){var d,e=a(this),f=e.attr("data-target")||c.preventDefault()||(d=e.attr("href"))&&d.replace(/.*(?=#[^\s]+$)/,""),g=a(f),h=g.data("bs.collapse"),i=h?"toggle":e.data(),j=e.attr("data-parent"),k=j&&a(j);h&&h.transitioning||(k&&k.find('[data-toggle="collapse"][data-parent="'+j+'"]').not(e).addClass("collapsed"),e[g.hasClass("in")?"addClass":"removeClass"]("collapsed")),b.call(g,i)})}(jQuery),+function(a){"use strict";function b(b){b&&3===b.which||(a(e).remove(),a(f).each(function(){var d=c(a(this)),e={relatedTarget:this};d.hasClass("open")&&(d.trigger(b=a.Event("hide.bs.dropdown",e)),b.isDefaultPrevented()||d.removeClass("open").trigger("hidden.bs.dropdown",e))}))}function c(b){var c=b.attr("data-target");c||(c=b.attr("href"),c=c&&/#[A-Za-z]/.test(c)&&c.replace(/.*(?=#[^\s]*$)/,""));var d=c&&a(c);return d&&d.length?d:b.parent()}function d(b){return this.each(function(){var c=a(this),d=c.data("bs.dropdown");d||c.data("bs.dropdown",d=new g(this)),"string"==typeof b&&d[b].call(c)})}var e=".dropdown-backdrop",f='[data-toggle="dropdown"]',g=function(b){a(b).on("click.bs.dropdown",this.toggle)};g.VERSION="3.2.0",g.prototype.toggle=function(d){var e=a(this);if(!e.is(".disabled, :disabled")){var f=c(e),g=f.hasClass("open");if(b(),!g){"ontouchstart"in document.documentElement&&!f.closest(".navbar-nav").length&&a('