├── .github └── workflows │ ├── npm-publish.yml │ └── test.yml ├── .gitignore ├── .npmignore ├── .nvmrc ├── README.md ├── demo-template ├── LICENSE.txt ├── README.txt ├── assets │ ├── css │ │ ├── font-awesome.min.css │ │ ├── images │ │ │ └── overlay.png │ │ ├── main.css │ │ └── noscript.css │ ├── fonts │ │ ├── FontAwesome.otf │ │ ├── fontawesome-webfont.eot │ │ ├── fontawesome-webfont.svg │ │ ├── fontawesome-webfont.ttf │ │ ├── fontawesome-webfont.woff │ │ └── fontawesome-webfont.woff2 │ ├── js │ │ ├── breakpoints.min.js │ │ ├── browser.min.js │ │ ├── jquery.min.js │ │ ├── jquery.scrollex.min.js │ │ ├── jquery.scrolly.min.js │ │ ├── main.js │ │ └── util.js │ └── sass │ │ ├── base │ │ ├── _page.scss │ │ ├── _reset.scss │ │ └── _typography.scss │ │ ├── components │ │ ├── _actions.scss │ │ ├── _box.scss │ │ ├── _button.scss │ │ ├── _features.scss │ │ ├── _form.scss │ │ ├── _icon.scss │ │ ├── _icons.scss │ │ ├── _image.scss │ │ ├── _list.scss │ │ ├── _row.scss │ │ ├── _section.scss │ │ ├── _spotlight.scss │ │ ├── _statistics.scss │ │ └── _table.scss │ │ ├── layout │ │ ├── _footer.scss │ │ ├── _header.scss │ │ ├── _main.scss │ │ ├── _nav.scss │ │ └── _wrapper.scss │ │ ├── libs │ │ ├── _breakpoints.scss │ │ ├── _functions.scss │ │ ├── _html-grid.scss │ │ ├── _mixins.scss │ │ ├── _vars.scss │ │ └── _vendor.scss │ │ ├── main.scss │ │ └── noscript.scss ├── elements.html ├── generic.html ├── images │ ├── logo.svg │ ├── pic01.jpg │ ├── pic02.jpg │ ├── pic03.jpg │ ├── pic04.jpg │ ├── pic05.jpg │ └── pic06.jpg └── index.html ├── favicon.png ├── jest.config.js ├── package-lock.json ├── package.json ├── src ├── jade │ └── index.jade ├── less │ └── demo.less └── ts │ ├── Constants.ts │ ├── Keyboard.ts │ ├── Mouse.ts │ ├── README.md │ ├── Types.ts │ ├── Ui.ts │ ├── demo.ts │ ├── flux │ ├── MouseState.ts │ ├── SelectableState.ts │ ├── SelectionState.ts │ ├── StageStore.ts │ └── UiState.ts │ ├── handlers │ ├── DrawHandler.ts │ ├── MouseHandlerBase.ts │ ├── MoveHandler.ts │ └── ResizeHandler.ts │ ├── index.ts │ ├── observers │ ├── DomObserver.ts │ ├── MouseObserver.ts │ ├── SelectablesObserver.ts │ └── UiObserver.ts │ └── utils │ ├── DomMetrics.ts │ ├── Events.ts │ └── Polyfill.ts ├── tests ├── __snapshots__ │ └── ui.test.electron.ts.snap ├── flux │ ├── StageStoreMock.ts │ └── stagestore.test.jsdom.ts ├── handlers │ ├── drawhandler.test.jsdom.ts │ ├── movehandler.test.electron.ts │ └── resizehandler.test.electron.ts ├── mouse.test.electron.ts ├── observers │ ├── dom-observer.test.electron.ts │ ├── mouse-observer.test.electron.ts │ ├── selectables-observer.test.jsdom.ts │ └── ui-observer.test.jsdom.ts ├── stage.test.electron.ts ├── ui.test.electron.ts └── utils │ ├── dom-metrics.test.electron.ts │ └── polyfill.test.electron.ts ├── tsconfig-tests.json └── tsconfig.json /.github/workflows/npm-publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish on npm 2 | on: 3 | push: 4 | tags: 5 | - '*' 6 | jobs: 7 | build: 8 | runs-on: ubuntu-latest 9 | steps: 10 | - uses: actions/checkout@v2 11 | # Setup .npmrc file to publish to npm 12 | - uses: actions/setup-node@v1 13 | with: 14 | node-version: '12.x' 15 | registry-url: 'https://registry.npmjs.org' 16 | - run: npm install 17 | - run: npm publish 18 | env: 19 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 20 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: [push] 4 | 5 | jobs: 6 | build: 7 | 8 | runs-on: ${{ matrix.os }} 9 | 10 | strategy: 11 | matrix: 12 | node-version: [10.x, 11.x, 12.x] 13 | # os: [macos-latest, windows-latest, ubuntu-latest] 14 | os: [ubuntu-latest] 15 | 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v1 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | - run: npm install 23 | - run: npm run build 24 | - run: npm test 25 | env: 26 | CI: true 27 | - run: npm run lint 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | pub/ 2 | node_modules/ 3 | *.log -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.log -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 16 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## About this project 2 | 3 | This "stage" component enables the user select elements, drag and drop them, resize them. 4 | 5 | A component like this will be useful to the developer building any tool which includes a WYSIWYG. 6 | 7 | This project is maintained, has very few dependencies and is used in [Silex website builder](https://www.silex.me) 8 | 9 | [Here is a very simple example](http://projects.silexlabs.org/drag-drop-stage-component/pub/). 10 | 11 | ## Features 12 | 13 | 14 | * [x] move and resize elements 15 | * [x] supports absolute position as well as elements in the flow 16 | * [x] multi selection 17 | * [x] shift + resize will keep proportions 18 | * [x] shift + move to stay aligned 19 | * [x] drawing mode, which let the user select multiple elements easily or draw a new element on the stage 20 | * [x] hooks give you full control over what is selectable, draggable, droppable, resizeable, a drop zone 21 | * [x] events to be notified of every action of the user 22 | * [x] scroll when the user moves near the border of the stage, or to show a specific element 23 | * [x] handle the events outside the iframe (the user can drag an element and release the mouse outside the iframe) 24 | * [x] sticky elements 25 | * [ ] [vote and submit feature requests](https://github.com/silexlabs/drag-drop-stage-component/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement) 26 | 27 | Here is a [list of features](https://github.com/silexlabs/drag-drop-stage-component/issues?q=is%3Aissue+is%3Aopen+label%3Aenhancement) which is the current road map (please vote with :+1:s). 28 | 29 | ## Use 30 | 31 | See [the online demo](http://projects.silexlabs.org/drag-drop-stage-component/pub/) and its sources: [html here](https://github.com/silexlabs/drag-drop-stage-component/blob/main/src/jade/index.jade) and [js here](https://github.com/silexlabs/drag-drop-stage-componentdrag-drop-stage-component/blob/main/src/ts/demo.js). 32 | 33 | The component can be initialized like this, which will make it possible to select, move and resize all the elements marked with the `.selectable` css class. 34 | 35 | ```javascript 36 | // All the div in the iframe 37 | const iframe = document.querySelector('#iframe') 38 | const stage = new Stage(iframe, iframe.contentDocument.querySelectorAll('div')) 39 | ``` 40 | 41 | The iframe is where you add elements with the `.selectable`, `.draggable`, `.resizeable`, `.droppable` css classes, which can then be moved and resized. 42 | 43 | Your application can catch events and store the new style of the elements after a drop. 44 | 45 | ``` 46 | stage.on('drop', e => { 47 | console.log('elements have been moved or resized, store their new styles if you wish', e.elements); 48 | }); 49 | ``` 50 | 51 | By default the elements which can be dragged or moved are those with the CSS classes `.selectable`,`.draggable`, `.resizeable` but you can override this as follow. The `.droppable` CSS class can be overrided too: 52 | 53 | ```javascript 54 | const stage = new Stage(iframe, { 55 | isSelectable: (el) => el.classList.contains('selectable'), 56 | isDroppable: (el, selection) => el.classList.contains('droppable'), 57 | }) 58 | ``` 59 | 60 | ## Build 61 | 62 | The build requires nodejs and npm, and it produces these files: 63 | * `pub/stage.js`, which you need to include in your project 64 | * `pub/stage.css`, which will be included in the iframe to draw the UI 65 | * `pub/demo.html`, which is a demo page for you to test the component 66 | 67 | Run `npm install` and `npm run build` to build these files. 68 | 69 | ## Dependencies 70 | 71 | This component only depenency is [the redux library](https://www.npmjs.com/package/redux). 72 | 73 | It uses [Typescript](https://www.typescriptlang.org/) to compile to Javscript and [less](http://lesscss.org/) to compile to CSS. Also the unit tests are written with [Jest](https://jestjs.io/). 74 | 75 | ## Contribute 76 | 77 | Please [vote for the features which matter to you here](https://github.com/drag-drop-stage-component/labels/enhancement). 78 | 79 | If you want to contribute code, [read this readme for an introduction to the source code](./src/ts/). And then you can help fixing the [issues found in the code by Code Climat](https://codeclimate.com/github/drag-drop-stage-component/issues) or find things to do [in these issues which need to be done](https://github.com/drag-drop-stage-component/labels/ready). 80 | 81 | The source code is written in ES2015 with less and jade. 82 | -------------------------------------------------------------------------------- /demo-template/README.txt: -------------------------------------------------------------------------------- 1 | Stellar by HTML5 UP 2 | html5up.net | @ajlkn 3 | Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) 4 | 5 | 6 | Say hello to Stellar, a slick little one-pager with a super vibrant color palette (which 7 | I guess you can always tone down if it's a little too vibrant for you), a "sticky" in-page 8 | nav bar (powered by my Scrollex plugin), a separate generic page template (just in case 9 | you need one), and an assortment of pre-styled elements. 10 | 11 | Demo images* courtesy of Unsplash, a radtastic collection of CC0 (public domain) images 12 | you can use for pretty much whatever. 13 | 14 | (* = not included) 15 | 16 | AJ 17 | aj@lkn.io | @ajlkn 18 | 19 | 20 | Credits: 21 | 22 | Demo Images: 23 | Unsplash (unsplash.com) 24 | 25 | Icons: 26 | Font Awesome (fontawesome.io) 27 | 28 | Other: 29 | jQuery (jquery.com) 30 | Scrollex (github.com/ajlkn/jquery.scrollex) 31 | Responsive Tools (github.com/ajlkn/responsive-tools) -------------------------------------------------------------------------------- /demo-template/assets/css/images/overlay.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silexlabs/drag-drop-stage-component/3e9273608b0a85f7cee55419f8a4d258c9af946b/demo-template/assets/css/images/overlay.png -------------------------------------------------------------------------------- /demo-template/assets/css/noscript.css: -------------------------------------------------------------------------------- 1 | /* 2 | Stellar by HTML5 UP 3 | html5up.net | @ajlkn 4 | Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) 5 | */ 6 | 7 | /* Header */ 8 | 9 | body.is-preload #header.alt > * { 10 | opacity: 1; 11 | } 12 | 13 | body.is-preload #header.alt .logo { 14 | -moz-transform: none; 15 | -webkit-transform: none; 16 | -ms-transform: none; 17 | transform: none; 18 | } -------------------------------------------------------------------------------- /demo-template/assets/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silexlabs/drag-drop-stage-component/3e9273608b0a85f7cee55419f8a4d258c9af946b/demo-template/assets/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /demo-template/assets/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silexlabs/drag-drop-stage-component/3e9273608b0a85f7cee55419f8a4d258c9af946b/demo-template/assets/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /demo-template/assets/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silexlabs/drag-drop-stage-component/3e9273608b0a85f7cee55419f8a4d258c9af946b/demo-template/assets/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /demo-template/assets/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silexlabs/drag-drop-stage-component/3e9273608b0a85f7cee55419f8a4d258c9af946b/demo-template/assets/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /demo-template/assets/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silexlabs/drag-drop-stage-component/3e9273608b0a85f7cee55419f8a4d258c9af946b/demo-template/assets/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /demo-template/assets/js/breakpoints.min.js: -------------------------------------------------------------------------------- 1 | /* breakpoints.js v1.0 | @ajlkn | MIT licensed */ 2 | var breakpoints=function(){"use strict";function e(e){t.init(e)}var t={list:null,media:{},events:[],init:function(e){t.list=e,window.addEventListener("resize",t.poll),window.addEventListener("orientationchange",t.poll),window.addEventListener("load",t.poll),window.addEventListener("fullscreenchange",t.poll)},active:function(e){var n,a,s,i,r,d,c;if(!(e in t.media)){if(">="==e.substr(0,2)?(a="gte",n=e.substr(2)):"<="==e.substr(0,2)?(a="lte",n=e.substr(2)):">"==e.substr(0,1)?(a="gt",n=e.substr(1)):"<"==e.substr(0,1)?(a="lt",n=e.substr(1)):"!"==e.substr(0,1)?(a="not",n=e.substr(1)):(a="eq",n=e),n&&n in t.list)if(i=t.list[n],Array.isArray(i)){if(r=parseInt(i[0]),d=parseInt(i[1]),isNaN(r)){if(isNaN(d))return;c=i[1].substr(String(d).length)}else c=i[0].substr(String(r).length);if(isNaN(r))switch(a){case"gte":s="screen";break;case"lte":s="screen and (max-width: "+d+c+")";break;case"gt":s="screen and (min-width: "+(d+1)+c+")";break;case"lt":s="screen and (max-width: -1px)";break;case"not":s="screen and (min-width: "+(d+1)+c+")";break;default:s="screen and (max-width: "+d+c+")"}else if(isNaN(d))switch(a){case"gte":s="screen and (min-width: "+r+c+")";break;case"lte":s="screen";break;case"gt":s="screen and (max-width: -1px)";break;case"lt":s="screen and (max-width: "+(r-1)+c+")";break;case"not":s="screen and (max-width: "+(r-1)+c+")";break;default:s="screen and (min-width: "+r+c+")"}else switch(a){case"gte":s="screen and (min-width: "+r+c+")";break;case"lte":s="screen and (max-width: "+d+c+")";break;case"gt":s="screen and (min-width: "+(d+1)+c+")";break;case"lt":s="screen and (max-width: "+(r-1)+c+")";break;case"not":s="screen and (max-width: "+(r-1)+c+"), screen and (min-width: "+(d+1)+c+")";break;default:s="screen and (min-width: "+r+c+") and (max-width: "+d+c+")"}}else s="("==i.charAt(0)?"screen and "+i:i;t.media[e]=!!s&&s}return t.media[e]!==!1&&window.matchMedia(t.media[e]).matches},on:function(e,n){t.events.push({query:e,handler:n,state:!1}),t.active(e)&&n()},poll:function(){var e,n;for(e=0;e0:!!("ontouchstart"in window),e.mobile="wp"==e.os||"android"==e.os||"ios"==e.os||"bb"==e.os}};return e.init(),e}();!function(e,n){"function"==typeof define&&define.amd?define([],n):"object"==typeof exports?module.exports=n():e.browser=n()}(this,function(){return browser}); 3 | -------------------------------------------------------------------------------- /demo-template/assets/js/jquery.scrollex.min.js: -------------------------------------------------------------------------------- 1 | /* jquery.scrollex v0.2.1 | (c) @ajlkn | github.com/ajlkn/jquery.scrollex | MIT licensed */ 2 | !function(t){function e(t,e,n){return"string"==typeof t&&("%"==t.slice(-1)?t=parseInt(t.substring(0,t.length-1))/100*e:"vh"==t.slice(-2)?t=parseInt(t.substring(0,t.length-2))/100*n:"px"==t.slice(-2)&&(t=parseInt(t.substring(0,t.length-2)))),t}var n=t(window),i=1,o={};n.on("scroll",function(){var e=n.scrollTop();t.map(o,function(t){window.clearTimeout(t.timeoutId),t.timeoutId=window.setTimeout(function(){t.handler(e)},t.options.delay)})}).on("load",function(){n.trigger("scroll")}),jQuery.fn.scrollex=function(l){var s=t(this);if(0==this.length)return s;if(this.length>1){for(var r=0;r=i&&o>=t};break;case"bottom":h=function(t,e,n,i,o){return n>=i&&o>=n};break;case"middle":h=function(t,e,n,i,o){return e>=i&&o>=e};break;case"top-only":h=function(t,e,n,i,o){return i>=t&&n>=i};break;case"bottom-only":h=function(t,e,n,i,o){return n>=o&&o>=t};break;default:case"default":h=function(t,e,n,i,o){return n>=i&&o>=t}}return c=function(t){var i,o,l,s,r,a,u=this.state,h=!1,c=this.$element.offset();i=n.height(),o=t+i/2,l=t+i,s=this.$element.outerHeight(),r=c.top+e(this.options.top,s,i),a=c.top+s-e(this.options.bottom,s,i),h=this.test(t,o,l,r,a),h!=u&&(this.state=h,h?this.options.enter&&this.options.enter.apply(this.element):this.options.leave&&this.options.leave.apply(this.element)),this.options.scroll&&this.options.scroll.apply(this.element,[(o-r)/(a-r)])},p={id:a,options:u,test:h,handler:c,state:null,element:this,$element:s,timeoutId:null},o[a]=p,s.data("_scrollexId",p.id),p.options.initialize&&p.options.initialize.apply(this),s},jQuery.fn.unscrollex=function(){var e=t(this);if(0==this.length)return e;if(this.length>1){for(var n=0;n1){for(o=0;o 0) { 34 | 35 | // Shrink effect. 36 | $main 37 | .scrollex({ 38 | mode: 'top', 39 | enter: function() { 40 | $nav.addClass('alt'); 41 | }, 42 | leave: function() { 43 | $nav.removeClass('alt'); 44 | }, 45 | }); 46 | 47 | // Links. 48 | var $nav_a = $nav.find('a'); 49 | 50 | $nav_a 51 | .scrolly({ 52 | speed: 1000, 53 | offset: function() { return $nav.height(); } 54 | }) 55 | .on('click', function() { 56 | 57 | var $this = $(this); 58 | 59 | // External link? Bail. 60 | if ($this.attr('href').charAt(0) != '#') 61 | return; 62 | 63 | // Deactivate all links. 64 | $nav_a 65 | .removeClass('active') 66 | .removeClass('active-locked'); 67 | 68 | // Activate link *and* lock it (so Scrollex doesn't try to activate other links as we're scrolling to this one's section). 69 | $this 70 | .addClass('active') 71 | .addClass('active-locked'); 72 | 73 | }) 74 | .each(function() { 75 | 76 | var $this = $(this), 77 | id = $this.attr('href'), 78 | $section = $(id); 79 | 80 | // No section for this link? Bail. 81 | if ($section.length < 1) 82 | return; 83 | 84 | // Scrollex. 85 | $section.scrollex({ 86 | mode: 'middle', 87 | initialize: function() { 88 | 89 | // Deactivate section. 90 | if (browser.canUse('transition')) 91 | $section.addClass('inactive'); 92 | 93 | }, 94 | enter: function() { 95 | 96 | // Activate section. 97 | $section.removeClass('inactive'); 98 | 99 | // No locked links? Deactivate all links and activate this section's one. 100 | if ($nav_a.filter('.active-locked').length == 0) { 101 | 102 | $nav_a.removeClass('active'); 103 | $this.addClass('active'); 104 | 105 | } 106 | 107 | // Otherwise, if this section's link is the one that's locked, unlock it. 108 | else if ($this.hasClass('active-locked')) 109 | $this.removeClass('active-locked'); 110 | 111 | } 112 | }); 113 | 114 | }); 115 | 116 | } 117 | 118 | // Scrolly. 119 | $('.scrolly').scrolly({ 120 | speed: 1000 121 | }); 122 | 123 | })(jQuery); -------------------------------------------------------------------------------- /demo-template/assets/sass/base/_page.scss: -------------------------------------------------------------------------------- 1 | /// 2 | /// Stellar by HTML5 UP 3 | /// html5up.net | @ajlkn 4 | /// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) 5 | /// 6 | 7 | /* Basic */ 8 | 9 | // MSIE: Required for IEMobile. 10 | @-ms-viewport { 11 | width: device-width; 12 | } 13 | 14 | // MSIE: Prevents scrollbar from overlapping content. 15 | body { 16 | -ms-overflow-style: scrollbar; 17 | } 18 | 19 | // Ensures page width is always >=320px. 20 | @include breakpoint('<=xsmall') { 21 | html, body { 22 | min-width: 320px; 23 | } 24 | } 25 | 26 | // Set box model to border-box. 27 | // Based on css-tricks.com/inheriting-box-sizing-probably-slightly-better-best-practice 28 | html { 29 | box-sizing: border-box; 30 | } 31 | 32 | *, *:before, *:after { 33 | box-sizing: inherit; 34 | } 35 | 36 | body { 37 | background-color: _palette(bg); 38 | @include vendor('background-image', ( 39 | 'url("images/overlay.png")', 40 | 'linear-gradient(45deg, #{_palette(bg1)} 15%, #{_palette(bg2) 85%})', 41 | )); 42 | 43 | // Stops initial animations until page loads. 44 | &.is-preload { 45 | *, *:before, *:after { 46 | @include vendor('animation', 'none !important'); 47 | @include vendor('transition', 'none !important'); 48 | } 49 | } 50 | 51 | } -------------------------------------------------------------------------------- /demo-template/assets/sass/base/_reset.scss: -------------------------------------------------------------------------------- 1 | /// 2 | /// Stellar by HTML5 UP 3 | /// html5up.net | @ajlkn 4 | /// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) 5 | /// 6 | 7 | // Reset. 8 | // Based on meyerweb.com/eric/tools/css/reset (v2.0 | 20110126 | License: public domain) 9 | 10 | html, body, div, span, applet, object, 11 | iframe, h1, h2, h3, h4, h5, h6, p, blockquote, 12 | pre, a, abbr, acronym, address, big, cite, 13 | code, del, dfn, em, img, ins, kbd, q, s, samp, 14 | small, strike, strong, sub, sup, tt, var, b, 15 | u, i, center, dl, dt, dd, ol, ul, li, fieldset, 16 | form, label, legend, table, caption, tbody, 17 | tfoot, thead, tr, th, td, article, aside, 18 | canvas, details, embed, figure, figcaption, 19 | footer, header, hgroup, menu, nav, output, ruby, 20 | section, summary, time, mark, audio, video { 21 | margin: 0; 22 | padding: 0; 23 | border: 0; 24 | font-size: 100%; 25 | font: inherit; 26 | vertical-align: baseline; 27 | } 28 | 29 | article, aside, details, figcaption, figure, 30 | footer, header, hgroup, menu, nav, section { 31 | display: block; 32 | } 33 | 34 | body { 35 | line-height: 1; 36 | } 37 | 38 | ol, ul { 39 | list-style:none; 40 | } 41 | 42 | blockquote, q { 43 | quotes: none; 44 | 45 | &:before, 46 | &:after { 47 | content: ''; 48 | content: none; 49 | } 50 | } 51 | 52 | table { 53 | border-collapse: collapse; 54 | border-spacing: 0; 55 | } 56 | 57 | body { 58 | -webkit-text-size-adjust: none; 59 | } 60 | 61 | mark { 62 | background-color: transparent; 63 | color: inherit; 64 | } 65 | 66 | input::-moz-focus-inner { 67 | border: 0; 68 | padding: 0; 69 | } 70 | 71 | input, select, textarea { 72 | -moz-appearance: none; 73 | -webkit-appearance: none; 74 | -ms-appearance: none; 75 | appearance: none; 76 | } -------------------------------------------------------------------------------- /demo-template/assets/sass/base/_typography.scss: -------------------------------------------------------------------------------- 1 | /// 2 | /// Stellar by HTML5 UP 3 | /// html5up.net | @ajlkn 4 | /// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) 5 | /// 6 | 7 | /* Type */ 8 | 9 | body { 10 | background-color: _palette(bg); 11 | color: _palette(fg); 12 | } 13 | 14 | body, input, select, textarea { 15 | font-family: _font(family); 16 | font-size: 17pt; 17 | font-weight: _font(weight); 18 | line-height: 1.65; 19 | 20 | @include breakpoint('<=xlarge') { 21 | font-size: 14pt; 22 | } 23 | 24 | @include breakpoint('<=large') { 25 | font-size: 12pt; 26 | } 27 | 28 | @include breakpoint('<=xxsmall') { 29 | font-size: 11pt; 30 | } 31 | } 32 | 33 | a { 34 | @include vendor('transition', ( 35 | 'color #{_duration(transition)} ease', 36 | 'border-bottom #{_duration(transition)} ease' 37 | )); 38 | text-decoration: none; 39 | border-bottom: dotted 1px; 40 | color: inherit; 41 | 42 | &:hover { 43 | border-bottom-color: transparent; 44 | } 45 | } 46 | 47 | strong, b { 48 | font-weight: _font(weight-bold); 49 | } 50 | 51 | em, i { 52 | font-style: italic; 53 | } 54 | 55 | p { 56 | margin: 0 0 _size(element-margin) 0; 57 | 58 | &.content { 59 | -moz-columns: 20em 2; 60 | -webkit-columns: 20em 2; 61 | -ms-columns: 20em 2; 62 | columns: 20em 2; 63 | -moz-column-gap: _size(element-margin); 64 | -webkit-column-gap: _size(element-margin); 65 | -ms-column-gap: _size(element-margin); 66 | column-gap: _size(element-margin); 67 | text-align: justify; 68 | } 69 | } 70 | 71 | h1, h2, h3, h4, h5, h6 { 72 | font-weight: _font(weight); 73 | line-height: 1.5; 74 | margin: 0 0 (_size(element-margin) * 0.35) 0; 75 | letter-spacing: _font(letter-spacing); 76 | 77 | a { 78 | color: inherit; 79 | text-decoration: none; 80 | } 81 | } 82 | 83 | h1 { 84 | font-size: 2.5em; 85 | line-height: 1.2; 86 | } 87 | 88 | h2 { 89 | font-size: 1.5em; 90 | } 91 | 92 | h3 { 93 | font-size: 1.25em; 94 | } 95 | 96 | h4 { 97 | font-size: 1.1em; 98 | } 99 | 100 | h5 { 101 | font-size: 0.9em; 102 | } 103 | 104 | h6 { 105 | font-size: 0.7em; 106 | } 107 | 108 | @include breakpoint('<=small') { 109 | h1 { 110 | font-size: 2em; 111 | } 112 | } 113 | 114 | sub { 115 | font-size: 0.8em; 116 | position: relative; 117 | top: 0.5em; 118 | } 119 | 120 | sup { 121 | font-size: 0.8em; 122 | position: relative; 123 | top: -0.5em; 124 | } 125 | 126 | blockquote { 127 | border-left: solid 4px; 128 | font-style: italic; 129 | margin: 0 0 _size(element-margin) 0; 130 | padding: (_size(element-margin) / 4) 0 (_size(element-margin) / 4) _size(element-margin); 131 | } 132 | 133 | code { 134 | border-radius: _size(border-radius); 135 | border: solid 1px; 136 | font-family: _font(family-fixed); 137 | font-size: 0.9em; 138 | margin: 0 0.25em; 139 | padding: 0.25em 0.65em; 140 | } 141 | 142 | pre { 143 | -webkit-overflow-scrolling: touch; 144 | font-family: _font(family-fixed); 145 | font-size: 0.9em; 146 | margin: 0 0 _size(element-margin) 0; 147 | 148 | code { 149 | display: block; 150 | line-height: 1.75; 151 | padding: 1em 1.5em; 152 | overflow-x: auto; 153 | } 154 | } 155 | 156 | hr { 157 | border: 0; 158 | border-bottom: solid 1px; 159 | margin: _size(element-margin) 0; 160 | 161 | &.major { 162 | margin: (_size(element-margin) * 1.5) 0; 163 | } 164 | } 165 | 166 | .align-left { 167 | text-align: left; 168 | } 169 | 170 | .align-center { 171 | text-align: center; 172 | } 173 | 174 | .align-right { 175 | text-align: right; 176 | } 177 | 178 | @mixin color-typography($p: null) { 179 | @if $p != null { 180 | background-color: _palette($p, bg); 181 | color: _palette($p, fg); 182 | } 183 | 184 | input, select, textarea { 185 | color: _palette($p, fg-bold); 186 | } 187 | 188 | a { 189 | &:hover { 190 | color: _palette($p, fg-bold); 191 | } 192 | } 193 | 194 | strong, b { 195 | color: _palette($p, fg-bold); 196 | } 197 | 198 | h1, h2, h3, h4, h5, h6 { 199 | color: _palette($p, fg-bold); 200 | } 201 | 202 | blockquote { 203 | border-left-color: _palette($p, border); 204 | } 205 | 206 | code { 207 | background: _palette($p, border-bg); 208 | border-color: _palette($p, border); 209 | } 210 | 211 | hr { 212 | border-bottom-color: _palette($p, border); 213 | } 214 | } 215 | 216 | @include color-typography; -------------------------------------------------------------------------------- /demo-template/assets/sass/components/_actions.scss: -------------------------------------------------------------------------------- 1 | /// 2 | /// Stellar by HTML5 UP 3 | /// html5up.net | @ajlkn 4 | /// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) 5 | /// 6 | 7 | /* Actions */ 8 | 9 | ul.actions { 10 | @include vendor('display', 'flex'); 11 | cursor: default; 12 | list-style: none; 13 | margin-left: (_size(element-margin) * -0.5); 14 | padding-left: 0; 15 | 16 | li { 17 | padding: 0 0 0 (_size(element-margin) * 0.5); 18 | vertical-align: middle; 19 | } 20 | 21 | &.special { 22 | @include vendor('justify-content', 'center'); 23 | width: 100%; 24 | margin-left: 0; 25 | 26 | li { 27 | &:first-child { 28 | padding-left: 0; 29 | } 30 | } 31 | } 32 | 33 | &.stacked { 34 | @include vendor('flex-direction', 'column'); 35 | margin-left: 0; 36 | 37 | li { 38 | padding: (_size(element-margin) * 0.65) 0 0 0; 39 | 40 | &:first-child { 41 | padding-top: 0; 42 | } 43 | } 44 | } 45 | 46 | &.fit { 47 | width: calc(100% + #{_size(element-margin) * 0.5}); 48 | 49 | li { 50 | @include vendor('flex-grow', '1'); 51 | @include vendor('flex-shrink', '1'); 52 | width: 100%; 53 | 54 | > * { 55 | width: 100%; 56 | } 57 | } 58 | 59 | &.stacked { 60 | width: 100%; 61 | } 62 | } 63 | 64 | @include breakpoint('<=xsmall') { 65 | &:not(.fixed) { 66 | @include vendor('flex-direction', 'column'); 67 | margin-left: 0; 68 | width: 100% !important; 69 | 70 | li { 71 | @include vendor('flex-grow', '1'); 72 | @include vendor('flex-shrink', '1'); 73 | padding: (_size(element-margin) * 0.5) 0 0 0; 74 | text-align: center; 75 | width: 100%; 76 | 77 | > * { 78 | width: 100%; 79 | } 80 | 81 | &:first-child { 82 | padding-top: 0; 83 | } 84 | 85 | input[type="submit"], 86 | input[type="reset"], 87 | input[type="button"], 88 | button, 89 | .button { 90 | width: 100%; 91 | 92 | &.icon { 93 | &:before { 94 | margin-left: -0.5rem; 95 | } 96 | } 97 | } 98 | } 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /demo-template/assets/sass/components/_box.scss: -------------------------------------------------------------------------------- 1 | /// 2 | /// Stellar by HTML5 UP 3 | /// html5up.net | @ajlkn 4 | /// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) 5 | /// 6 | 7 | /* Box */ 8 | 9 | .box { 10 | border-radius: _size(border-radius); 11 | border: solid _size(border-width); 12 | margin-bottom: _size(element-margin); 13 | padding: 1.5em; 14 | 15 | > :last-child, 16 | > :last-child > :last-child, 17 | > :last-child > :last-child > :last-child { 18 | margin-bottom: 0; 19 | } 20 | 21 | &.alt { 22 | border: 0; 23 | border-radius: 0; 24 | padding: 0; 25 | } 26 | } 27 | 28 | @mixin color-box($p: null) { 29 | .box { 30 | border-color: _palette($p, border); 31 | } 32 | } 33 | 34 | @include color-box; -------------------------------------------------------------------------------- /demo-template/assets/sass/components/_button.scss: -------------------------------------------------------------------------------- 1 | /// 2 | /// Stellar by HTML5 UP 3 | /// html5up.net | @ajlkn 4 | /// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) 5 | /// 6 | 7 | /* Button */ 8 | 9 | input[type="submit"], 10 | input[type="reset"], 11 | input[type="button"], 12 | button, 13 | .button { 14 | @include vendor('appearance', 'none'); 15 | @include vendor('transition', ( 16 | 'background-color #{_duration(transition)} ease-in-out', 17 | 'color #{_duration(transition)} ease-in-out' 18 | )); 19 | border-radius: _size(border-radius); 20 | border: 0; 21 | cursor: pointer; 22 | display: inline-block; 23 | font-weight: _font(weight); 24 | height: 2.75em; 25 | line-height: 2.75em; 26 | min-width: 9.25em; 27 | padding: 0 1.5em; 28 | text-align: center; 29 | text-decoration: none; 30 | white-space: nowrap; 31 | 32 | &.icon { 33 | padding-left: 1.35em; 34 | 35 | &:before { 36 | margin-right: 0.5em; 37 | } 38 | } 39 | 40 | &.fit { 41 | width: 100%; 42 | } 43 | 44 | &.small { 45 | font-size: 0.8em; 46 | } 47 | 48 | &.large { 49 | font-size: 1.35em; 50 | } 51 | 52 | &.disabled, 53 | &:disabled { 54 | @include vendor('pointer-events', 'none'); 55 | opacity: 0.25; 56 | } 57 | 58 | @include breakpoint('<=small') { 59 | min-width: 0; 60 | } 61 | } 62 | 63 | @mixin color-button($p: null) { 64 | input[type="submit"], 65 | input[type="reset"], 66 | input[type="button"], 67 | button, 68 | .button { 69 | background-color: transparent; 70 | box-shadow: inset 0 0 0 1px _palette($p, border); 71 | color: _palette($p, fg-bold) !important; 72 | 73 | &:hover { 74 | background-color: _palette($p, border-bg); 75 | } 76 | 77 | &:active { 78 | background-color: _palette($p, border2-bg); 79 | } 80 | 81 | &.icon { 82 | &:before { 83 | color: _palette($p, fg-light); 84 | } 85 | } 86 | 87 | &.primary { 88 | background-color: _palette(accent); 89 | color: _palette(invert, bg) !important; 90 | box-shadow: none; 91 | 92 | &:hover { 93 | background-color: lighten(_palette(accent), 3); 94 | } 95 | 96 | &:active { 97 | background-color: darken(_palette(accent), 3); 98 | } 99 | 100 | &.icon { 101 | &:before { 102 | color: _palette(invert, bg) !important; 103 | } 104 | } 105 | } 106 | } 107 | } 108 | 109 | @include color-button; -------------------------------------------------------------------------------- /demo-template/assets/sass/components/_features.scss: -------------------------------------------------------------------------------- 1 | /// 2 | /// Stellar by HTML5 UP 3 | /// html5up.net | @ajlkn 4 | /// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) 5 | /// 6 | 7 | /* Features */ 8 | 9 | .features { 10 | @include vendor('display', 'flex'); 11 | @include vendor('flex-wrap', 'wrap'); 12 | @include vendor('justify-content', 'center'); 13 | width: calc(100% + #{_size(element-margin)}); 14 | margin: 0 0 (_size(element-margin) * 1.5) (_size(element-margin) * -1); 15 | padding: 0; 16 | list-style: none; 17 | 18 | li { 19 | width: calc(#{(100% / 3)} - #{_size(element-margin)}); 20 | margin-left: _size(element-margin); 21 | margin-top: (_size(element-margin) * 1.5); 22 | padding: 0; 23 | 24 | &:nth-child(1), 25 | &:nth-child(2), 26 | &:nth-child(3) { 27 | margin-top: 0; 28 | } 29 | 30 | > :last-child { 31 | margin-bottom: 0; 32 | } 33 | } 34 | 35 | @include breakpoint('<=medium') { 36 | li { 37 | width: calc(#{(100% / 2)} - #{_size(element-margin)}); 38 | 39 | &:nth-child(3) { 40 | margin-top: (_size(element-margin) * 1.5); 41 | } 42 | } 43 | } 44 | 45 | @include breakpoint('<=small') { 46 | width: 100%; 47 | margin: 0 0 _size(element-margin) 0; 48 | 49 | li { 50 | width: 100%; 51 | margin-left: 0; 52 | margin-top: _size(element-margin); 53 | 54 | &:nth-child(2), 55 | &:nth-child(3) { 56 | margin-top: _size(element-margin); 57 | } 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /demo-template/assets/sass/components/_form.scss: -------------------------------------------------------------------------------- 1 | /// 2 | /// Stellar by HTML5 UP 3 | /// html5up.net | @ajlkn 4 | /// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) 5 | /// 6 | 7 | /* Form */ 8 | 9 | form { 10 | margin: 0 0 _size(element-margin) 0; 11 | } 12 | 13 | label { 14 | display: block; 15 | font-size: 0.9em; 16 | font-weight: _font(weight-bold); 17 | margin: 0 0 (_size(element-margin) * 0.5) 0; 18 | } 19 | 20 | input[type="text"], 21 | input[type="password"], 22 | input[type="email"], 23 | select, 24 | textarea { 25 | @include vendor('appearance', 'none'); 26 | border-radius: _size(border-radius); 27 | border: solid 1px; 28 | color: inherit; 29 | display: block; 30 | outline: 0; 31 | padding: 0 1em; 32 | text-decoration: none; 33 | width: 100%; 34 | 35 | &:invalid { 36 | box-shadow: none; 37 | } 38 | } 39 | 40 | select { 41 | background-size: 1.25rem; 42 | background-repeat: no-repeat; 43 | background-position: calc(100% - 1rem) center; 44 | height: _size(element-height); 45 | padding-right: _size(element-height); 46 | text-overflow: ellipsis; 47 | 48 | &:focus { 49 | &::-ms-value { 50 | background-color: transparent; 51 | } 52 | } 53 | 54 | &::-ms-expand { 55 | display: none; 56 | } 57 | } 58 | 59 | input[type="text"], 60 | input[type="password"], 61 | input[type="email"], 62 | select { 63 | height: _size(element-height); 64 | } 65 | 66 | textarea { 67 | padding: 0.75em 1em; 68 | } 69 | 70 | input[type="checkbox"], 71 | input[type="radio"], { 72 | @include vendor('appearance', 'none'); 73 | display: block; 74 | float: left; 75 | margin-right: -2em; 76 | opacity: 0; 77 | width: 1em; 78 | z-index: -1; 79 | 80 | & + label { 81 | @include icon; 82 | cursor: pointer; 83 | display: inline-block; 84 | font-size: 1em; 85 | font-weight: _font(weight); 86 | padding-left: (_size(element-height) * 0.6) + 0.75em; 87 | padding-right: 0.75em; 88 | position: relative; 89 | 90 | &:before { 91 | border-radius: _size(border-radius); 92 | border: solid 1px; 93 | content: ''; 94 | display: inline-block; 95 | height: (_size(element-height) * 0.6); 96 | left: 0; 97 | line-height: (_size(element-height) * 0.575); 98 | position: absolute; 99 | text-align: center; 100 | top: 0; 101 | width: (_size(element-height) * 0.6); 102 | } 103 | } 104 | 105 | &:checked + label { 106 | &:before { 107 | content: '\f00c'; 108 | } 109 | } 110 | } 111 | 112 | input[type="checkbox"] { 113 | & + label { 114 | &:before { 115 | border-radius: _size(border-radius); 116 | } 117 | } 118 | } 119 | 120 | input[type="radio"] { 121 | & + label { 122 | &:before { 123 | border-radius: 100%; 124 | } 125 | } 126 | } 127 | 128 | ::-webkit-input-placeholder { 129 | opacity: 1.0; 130 | } 131 | 132 | :-moz-placeholder { 133 | opacity: 1.0; 134 | } 135 | 136 | ::-moz-placeholder { 137 | opacity: 1.0; 138 | } 139 | 140 | :-ms-input-placeholder { 141 | opacity: 1.0; 142 | } 143 | 144 | @mixin color-form($p: null) { 145 | label { 146 | color: _palette($p, fg-bold); 147 | } 148 | 149 | input[type="text"], 150 | input[type="password"], 151 | input[type="email"], 152 | select, 153 | textarea { 154 | background-color: _palette($p, border-bg); 155 | border-color: _palette($p, border); 156 | 157 | &:focus { 158 | border-color: _palette(accent); 159 | box-shadow: 0 0 0 1px _palette(accent); 160 | } 161 | } 162 | 163 | select { 164 | background-image: svg-url(""); 165 | 166 | option { 167 | color: _palette($p, fg-bold); 168 | background: _palette($p, bg); 169 | } 170 | } 171 | 172 | input[type="checkbox"], 173 | input[type="radio"], { 174 | & + label { 175 | color: _palette($p, fg); 176 | 177 | &:before { 178 | background: _palette($p, border-bg); 179 | border-color: _palette($p, border); 180 | } 181 | } 182 | 183 | &:checked + label { 184 | &:before { 185 | background-color: _palette($p, fg-bold); 186 | border-color: _palette($p, fg-bold); 187 | color: _palette($p, bg); 188 | } 189 | } 190 | 191 | &:focus + label { 192 | &:before { 193 | border-color: _palette(accent); 194 | box-shadow: 0 0 0 1px _palette(accent); 195 | } 196 | } 197 | } 198 | 199 | ::-webkit-input-placeholder { 200 | color: _palette($p, fg-light) !important; 201 | } 202 | 203 | :-moz-placeholder { 204 | color: _palette($p, fg-light) !important; 205 | } 206 | 207 | ::-moz-placeholder { 208 | color: _palette($p, fg-light) !important; 209 | } 210 | 211 | :-ms-input-placeholder { 212 | color: _palette($p, fg-light) !important; 213 | } 214 | 215 | .formerize-placeholder { 216 | color: _palette($p, fg-light) !important; 217 | } 218 | } 219 | 220 | @include color-form; -------------------------------------------------------------------------------- /demo-template/assets/sass/components/_icon.scss: -------------------------------------------------------------------------------- 1 | /// 2 | /// Stellar by HTML5 UP 3 | /// html5up.net | @ajlkn 4 | /// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) 5 | /// 6 | 7 | /* Icon */ 8 | 9 | .icon { 10 | @include icon; 11 | @include vendor('transition', ( 12 | 'background-color #{_duration(transition)} ease-in-out', 13 | 'color #{_duration(transition)} ease-in-out' 14 | )); 15 | border-bottom: none; 16 | position: relative; 17 | 18 | > .label { 19 | display: none; 20 | } 21 | 22 | &.major { 23 | border: solid 1px; 24 | display: inline-block; 25 | border-radius: 100%; 26 | padding: 0.65em; 27 | margin: 0 0 _size(element-margin) 0; 28 | cursor: default; 29 | 30 | &:before { 31 | display: inline-block; 32 | font-size: 6.25rem; 33 | width: 2.25em; 34 | height: 2.25em; 35 | line-height: 2.2em; 36 | border-radius: 100%; 37 | border: solid 1px; 38 | text-align: center; 39 | } 40 | } 41 | 42 | &.alt { 43 | display: inline-block; 44 | border: solid 1px; 45 | border-radius: 100%; 46 | 47 | &:before { 48 | display: block; 49 | font-size: 1.25em; 50 | width: 2em; 51 | height: 2em; 52 | text-align: center; 53 | line-height: 2em; 54 | } 55 | } 56 | 57 | &.style1 { 58 | color: _palette(accent1); 59 | } 60 | 61 | &.style2 { 62 | color: _palette(accent2); 63 | } 64 | 65 | &.style3 { 66 | color: _palette(accent3); 67 | } 68 | 69 | &.style4 { 70 | color: _palette(accent4); 71 | } 72 | 73 | &.style5 { 74 | color: _palette(accent5); 75 | } 76 | 77 | @include breakpoint('<=xlarge') { 78 | &.major { 79 | &:before { 80 | font-size: 5.5rem; 81 | } 82 | } 83 | } 84 | 85 | @include breakpoint('<=large') { 86 | &.major { 87 | &:before { 88 | font-size: 4.75rem; 89 | } 90 | } 91 | } 92 | 93 | @include breakpoint('<=small') { 94 | &.major { 95 | margin: 0 0 (_size(element-margin) * 0.75) 0; 96 | padding: 0.35em; 97 | 98 | &:before { 99 | font-size: 3.5rem; 100 | } 101 | } 102 | } 103 | } 104 | 105 | @mixin color-icon($p: null) { 106 | .icon { 107 | &.major { 108 | border-color: _palette($p, border); 109 | 110 | &:before { 111 | border-color: _palette($p, border); 112 | } 113 | } 114 | 115 | &.alt { 116 | border-color: _palette($p, border); 117 | color: _palette($p, fg-bold); 118 | 119 | &:hover { 120 | background-color: _palette($p, border-bg); 121 | } 122 | 123 | &:active { 124 | background-color: _palette($p, border2-bg); 125 | } 126 | } 127 | } 128 | } 129 | 130 | @include color-icon; -------------------------------------------------------------------------------- /demo-template/assets/sass/components/_icons.scss: -------------------------------------------------------------------------------- 1 | /// 2 | /// Stellar by HTML5 UP 3 | /// html5up.net | @ajlkn 4 | /// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) 5 | /// 6 | 7 | /* Icons */ 8 | 9 | ul.icons { 10 | cursor: default; 11 | list-style: none; 12 | padding-left: 0; 13 | 14 | li { 15 | display: inline-block; 16 | padding: 0 0.65em 0 0; 17 | 18 | &:last-child { 19 | padding-right: 0 !important; 20 | } 21 | } 22 | } -------------------------------------------------------------------------------- /demo-template/assets/sass/components/_image.scss: -------------------------------------------------------------------------------- 1 | /// 2 | /// Stellar by HTML5 UP 3 | /// html5up.net | @ajlkn 4 | /// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) 5 | /// 6 | 7 | /* Image */ 8 | 9 | .image { 10 | border-radius: _size(border-radius); 11 | border: 0; 12 | display: inline-block; 13 | position: relative; 14 | 15 | img { 16 | border-radius: _size(border-radius); 17 | display: block; 18 | } 19 | 20 | &.left, 21 | &.right { 22 | max-width: 40%; 23 | 24 | img { 25 | width: 100%; 26 | } 27 | } 28 | 29 | &.left { 30 | float: left; 31 | margin: 0 1.5em 1em 0; 32 | top: 0.25em; 33 | } 34 | 35 | &.right { 36 | float: right; 37 | margin: 0 0 1em 1.5em; 38 | top: 0.25em; 39 | } 40 | 41 | &.fit { 42 | display: block; 43 | margin: 0 0 _size(element-margin) 0; 44 | width: 100%; 45 | 46 | img { 47 | width: 100%; 48 | } 49 | } 50 | 51 | &.main { 52 | display: block; 53 | margin: 0 0 (_size(element-margin) * 1.5) 0; 54 | width: 100%; 55 | 56 | img { 57 | width: 100%; 58 | } 59 | } 60 | } -------------------------------------------------------------------------------- /demo-template/assets/sass/components/_list.scss: -------------------------------------------------------------------------------- 1 | /// 2 | /// Stellar by HTML5 UP 3 | /// html5up.net | @ajlkn 4 | /// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) 5 | /// 6 | 7 | /* List */ 8 | 9 | ol { 10 | list-style: decimal; 11 | margin: 0 0 _size(element-margin) 0; 12 | padding-left: 1.25em; 13 | 14 | li { 15 | padding-left: 0.25em; 16 | } 17 | } 18 | 19 | ul { 20 | list-style: disc; 21 | margin: 0 0 _size(element-margin) 0; 22 | padding-left: 1em; 23 | 24 | li { 25 | padding-left: 0.5em; 26 | } 27 | 28 | &.alt { 29 | list-style: none; 30 | padding-left: 0; 31 | 32 | li { 33 | border-top: solid 1px; 34 | padding: 0.5em 0; 35 | 36 | &:first-child { 37 | border-top: 0; 38 | padding-top: 0; 39 | } 40 | } 41 | } 42 | } 43 | 44 | dl { 45 | margin: 0 0 _size(element-margin) 0; 46 | 47 | dt { 48 | display: block; 49 | font-weight: _font(weight-bold); 50 | margin: 0 0 (_size(element-margin) * 0.5) 0; 51 | } 52 | 53 | dd { 54 | margin-left: _size(element-margin); 55 | } 56 | 57 | &.alt { 58 | dt { 59 | display: block; 60 | width: 3em; 61 | margin: 0; 62 | clear: left; 63 | float: left; 64 | } 65 | 66 | dd { 67 | margin: 0 0 0.85em 5.5em; 68 | } 69 | 70 | &:after { 71 | content: ''; 72 | display: block; 73 | clear: both; 74 | } 75 | } 76 | } 77 | 78 | @mixin color-list($p: null) { 79 | ul { 80 | &.alt { 81 | li { 82 | border-top-color: _palette($p, border); 83 | } 84 | } 85 | } 86 | 87 | dl { 88 | dt { 89 | color: _palette($p, fg-bold); 90 | } 91 | } 92 | } 93 | 94 | @include color-list; -------------------------------------------------------------------------------- /demo-template/assets/sass/components/_row.scss: -------------------------------------------------------------------------------- 1 | /// 2 | /// Stellar by HTML5 UP 3 | /// html5up.net | @ajlkn 4 | /// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) 5 | /// 6 | 7 | /* Row */ 8 | 9 | .row { 10 | @include html-grid(1.5em); 11 | 12 | @include breakpoint('<=xlarge') { 13 | @include html-grid(1.5em, 'xlarge'); 14 | } 15 | 16 | @include breakpoint('<=large') { 17 | @include html-grid(1.5em, 'large'); 18 | } 19 | 20 | @include breakpoint('<=medium') { 21 | @include html-grid(1.5em, 'medium'); 22 | } 23 | 24 | @include breakpoint('<=small') { 25 | @include html-grid(1em, 'small'); 26 | } 27 | 28 | @include breakpoint('<=xsmall') { 29 | @include html-grid(1.25em, 'xsmall'); 30 | } 31 | } -------------------------------------------------------------------------------- /demo-template/assets/sass/components/_section.scss: -------------------------------------------------------------------------------- 1 | /// 2 | /// Stellar by HTML5 UP 3 | /// html5up.net | @ajlkn 4 | /// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) 5 | /// 6 | 7 | /* Section/Article */ 8 | 9 | section, article { 10 | &.special { 11 | text-align: center; 12 | } 13 | } 14 | 15 | header { 16 | &.major { 17 | margin-bottom: (_size(element-margin) * 1.5); 18 | 19 | h2 { 20 | font-size: 2em; 21 | 22 | &:after { 23 | display: block; 24 | content: ''; 25 | width: 3.25em; 26 | height: 2px; 27 | margin: (_size(element-margin) * 0.35) 0 (_size(element-margin) * 0.5) 0; 28 | border-radius: 2px; 29 | 30 | section.special &, article.special & { 31 | margin-left: auto; 32 | margin-right: auto; 33 | } 34 | } 35 | } 36 | 37 | p { 38 | font-size: 1.25em; 39 | letter-spacing: _font(letter-spacing); 40 | } 41 | 42 | &.special { 43 | text-align: center; 44 | 45 | h2 { 46 | &:after { 47 | margin-left: auto; 48 | margin-right: auto; 49 | } 50 | } 51 | } 52 | } 53 | } 54 | 55 | footer { 56 | &.major { 57 | margin-top: (_size(element-margin) * 1.5); 58 | } 59 | } 60 | 61 | @include breakpoint('<=small') { 62 | header { 63 | &.major { 64 | margin-bottom: 0; 65 | 66 | h2 { 67 | font-size: 1.5em; 68 | } 69 | 70 | p { 71 | font-size: 1em; 72 | letter-spacing: 0; 73 | 74 | br { 75 | display: none; 76 | } 77 | } 78 | } 79 | } 80 | 81 | footer { 82 | &.major { 83 | margin-top: 0; 84 | } 85 | } 86 | } 87 | 88 | @mixin color-section($p: null) { 89 | header { 90 | &.major { 91 | h2 { 92 | &:after { 93 | background-color: _palette($p, border); 94 | 95 | @if $p == 'invert' { 96 | @include vendor('background-image', 'linear-gradient(90deg, #{_palette(accent1)}, #{_palette(accent3)}, #{_palette(accent5)})'); 97 | } 98 | } 99 | } 100 | } 101 | } 102 | } 103 | 104 | @include color-section; -------------------------------------------------------------------------------- /demo-template/assets/sass/components/_spotlight.scss: -------------------------------------------------------------------------------- 1 | /// 2 | /// Stellar by HTML5 UP 3 | /// html5up.net | @ajlkn 4 | /// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) 5 | /// 6 | 7 | /* Spotlight */ 8 | 9 | .spotlight { 10 | @include vendor('display', 'flex'); 11 | @include vendor('align-items', 'center'); 12 | margin: 0 0 _size(element-margin) 0; 13 | 14 | .content { 15 | @include vendor('flex', '1'); 16 | 17 | > :last-child { 18 | margin-bottom: 0; 19 | } 20 | 21 | header { 22 | &.major { 23 | margin: 0 0 _size(element-margin) 0; 24 | } 25 | } 26 | } 27 | 28 | .image { 29 | display: inline-block; 30 | margin-left: 4em; 31 | padding: 0.65em; 32 | border-radius: 100%; 33 | border: solid 1px; 34 | 35 | img { 36 | display: block; 37 | border-radius: 100%; 38 | width: 16em; 39 | } 40 | } 41 | 42 | @include breakpoint('<=medium') { 43 | @include vendor('flex-direction', 'column-reverse'); 44 | text-align: center; 45 | 46 | .content { 47 | @include vendor('flex', '0 1 auto'); 48 | width: 100%; 49 | 50 | header { 51 | &.major { 52 | h2 { 53 | &:after { 54 | margin-left: auto; 55 | margin-right: auto; 56 | } 57 | } 58 | } 59 | } 60 | 61 | .actions { 62 | @include vendor('justify-content', 'center'); 63 | width: calc(100% + #{_size(element-margin) * 0.5}); 64 | } 65 | } 66 | 67 | .image { 68 | @include vendor('flex', '0 1 auto'); 69 | margin-left: 0; 70 | margin-bottom: _size(element-margin); 71 | } 72 | } 73 | 74 | @include breakpoint('<=small') { 75 | .image { 76 | padding: 0.35em; 77 | 78 | img { 79 | width: 12em; 80 | } 81 | } 82 | } 83 | } 84 | 85 | @mixin color-spotlight($p: null) { 86 | .spotlight { 87 | .image { 88 | border-color: _palette($p, border); 89 | } 90 | } 91 | } 92 | 93 | @include color-spotlight; -------------------------------------------------------------------------------- /demo-template/assets/sass/components/_statistics.scss: -------------------------------------------------------------------------------- 1 | /// 2 | /// Stellar by HTML5 UP 3 | /// html5up.net | @ajlkn 4 | /// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) 5 | /// 6 | 7 | /* Statistics */ 8 | 9 | .statistics { 10 | @include vendor('display', 'flex'); 11 | width: 100%; 12 | margin: 0 0 (_size(element-margin) * 1.5) 0; 13 | padding: 0; 14 | list-style: none; 15 | cursor: default; 16 | 17 | li { 18 | @include vendor('flex', '1'); 19 | padding: 1.5em; 20 | color: _palette(fg-bold); 21 | text-align: center; 22 | 23 | &.style1 { 24 | background-color: _palette(accent1); 25 | } 26 | 27 | &.style2 { 28 | background-color: _palette(accent2); 29 | } 30 | 31 | &.style3 { 32 | background-color: _palette(accent3); 33 | } 34 | 35 | &.style4 { 36 | background-color: _palette(accent4); 37 | } 38 | 39 | &.style5 { 40 | background-color: _palette(accent5); 41 | } 42 | 43 | strong, b { 44 | display: block; 45 | font-size: 2em; 46 | line-height: 1.1; 47 | color: inherit !important; 48 | font-weight: _font(weight); 49 | letter-spacing: _font(letter-spacing); 50 | } 51 | 52 | &:first-child { 53 | border-top-left-radius: _size(border-radius); 54 | border-bottom-left-radius: _size(border-radius); 55 | } 56 | 57 | &:last-child { 58 | border-top-right-radius: _size(border-radius); 59 | border-bottom-right-radius: _size(border-radius); 60 | } 61 | 62 | .icon { 63 | display: inline-block; 64 | 65 | &:before { 66 | font-size: 2.75rem; 67 | line-height: 1.3; 68 | } 69 | } 70 | } 71 | 72 | @include breakpoint('<=medium') { 73 | li { 74 | strong, b { 75 | font-size: 1.5em; 76 | } 77 | } 78 | } 79 | 80 | @include breakpoint('<=small') { 81 | display: block; 82 | width: 20em; 83 | max-width: 100%; 84 | margin: 0 auto _size(element-margin) auto; 85 | 86 | li { 87 | &:first-child { 88 | border-bottom-left-radius: 0; 89 | border-top-right-radius: _size(border-radius); 90 | } 91 | 92 | &:last-child { 93 | border-top-right-radius: 0; 94 | border-bottom-left-radius: _size(border-radius); 95 | } 96 | 97 | .icon { 98 | &:before { 99 | font-size: 3.75rem; 100 | } 101 | } 102 | 103 | strong, b { 104 | font-size: 2.5em; 105 | } 106 | } 107 | } 108 | } -------------------------------------------------------------------------------- /demo-template/assets/sass/components/_table.scss: -------------------------------------------------------------------------------- 1 | /// 2 | /// Stellar by HTML5 UP 3 | /// html5up.net | @ajlkn 4 | /// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) 5 | /// 6 | 7 | /* Table */ 8 | 9 | .table-wrapper { 10 | -webkit-overflow-scrolling: touch; 11 | overflow-x: auto; 12 | } 13 | 14 | table { 15 | margin: 0 0 _size(element-margin) 0; 16 | width: 100%; 17 | 18 | tbody { 19 | tr { 20 | border: solid 1px; 21 | border-left: 0; 22 | border-right: 0; 23 | } 24 | } 25 | 26 | td { 27 | padding: 0.75em 0.75em; 28 | } 29 | 30 | th { 31 | font-size: 0.9em; 32 | font-weight: _font(weight-bold); 33 | padding: 0 0.75em 0.75em 0.75em; 34 | text-align: left; 35 | } 36 | 37 | thead { 38 | border-bottom: solid 2px; 39 | } 40 | 41 | tfoot { 42 | border-top: solid 2px; 43 | } 44 | 45 | &.alt { 46 | border-collapse: separate; 47 | 48 | tbody { 49 | tr { 50 | td { 51 | border: solid 1px; 52 | border-left-width: 0; 53 | border-top-width: 0; 54 | 55 | &:first-child { 56 | border-left-width: 1px; 57 | } 58 | } 59 | 60 | &:first-child { 61 | td { 62 | border-top-width: 1px; 63 | } 64 | } 65 | } 66 | } 67 | 68 | thead { 69 | border-bottom: 0; 70 | } 71 | 72 | tfoot { 73 | border-top: 0; 74 | } 75 | } 76 | } 77 | 78 | @mixin color-table($p: null) { 79 | table { 80 | tbody { 81 | tr { 82 | border-color: _palette($p, border); 83 | 84 | &:nth-child(2n + 1) { 85 | background-color: _palette($p, border-bg); 86 | } 87 | } 88 | } 89 | 90 | th { 91 | color: _palette($p, fg-bold); 92 | } 93 | 94 | thead { 95 | border-bottom-color: _palette($p, border); 96 | } 97 | 98 | tfoot { 99 | border-top-color: _palette($p, border); 100 | } 101 | 102 | &.alt { 103 | tbody { 104 | tr { 105 | td { 106 | border-color: _palette($p, border); 107 | } 108 | } 109 | } 110 | } 111 | } 112 | } 113 | 114 | @include color-table; -------------------------------------------------------------------------------- /demo-template/assets/sass/layout/_footer.scss: -------------------------------------------------------------------------------- 1 | /// 2 | /// Stellar by HTML5 UP 3 | /// html5up.net | @ajlkn 4 | /// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) 5 | /// 6 | 7 | /* Footer */ 8 | 9 | #footer { 10 | @include vendor('display', 'flex'); 11 | @include vendor('flex-wrap', 'wrap'); 12 | @include padding(5em, 5em); 13 | width: calc(100% + #{_size(element-margin)}); 14 | margin: 0 0 (_size(element-margin) * 1.5) (_size(element-margin) * -1); 15 | 16 | > * { 17 | width: calc(50% - #{_size(element-margin)}); 18 | margin-left: _size(element-margin); 19 | } 20 | 21 | .copyright { 22 | width: 100%; 23 | margin: (_size(element-margin) * 1.25) 0 _size(element-margin) 0; 24 | font-size: 0.8em; 25 | text-align: center; 26 | } 27 | 28 | @include breakpoint('<=large') { 29 | @include padding(4em, 4em); 30 | } 31 | 32 | @include breakpoint('<=medium') { 33 | @include padding(4em, 3em); 34 | display: block; 35 | margin: 0 0 (_size(element-margin) * 1.5) 0; 36 | width: 100%; 37 | 38 | > * { 39 | width: 100%; 40 | margin-left: 0; 41 | margin-bottom: (_size(element-margin) * 1.5); 42 | } 43 | 44 | .copyright { 45 | text-align: left; 46 | } 47 | } 48 | 49 | @include breakpoint('<=small') { 50 | @include padding(3em, 2em); 51 | } 52 | 53 | @include breakpoint('<=xsmall') { 54 | @include padding(3em, 1.5em); 55 | } 56 | 57 | @include breakpoint('<=xsmall') { 58 | @include padding(2.5em, 1em); 59 | } 60 | } -------------------------------------------------------------------------------- /demo-template/assets/sass/layout/_header.scss: -------------------------------------------------------------------------------- 1 | /// 2 | /// Stellar by HTML5 UP 3 | /// html5up.net | @ajlkn 4 | /// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) 5 | /// 6 | 7 | /* Header */ 8 | 9 | #header { 10 | @include padding(5em, 5em, (0, 0, -2em, 0)); 11 | text-align: center; 12 | 13 | h1 { 14 | margin: 0 0 (_size(element-margin) * 0.125) 0; 15 | } 16 | 17 | p { 18 | font-size: 1.25em; 19 | letter-spacing: _font(letter-spacing); 20 | } 21 | 22 | &.alt { 23 | @include padding(6em, 5em, (1em, 0, 0, 0)); 24 | 25 | h1 { 26 | font-size: 3.25em; 27 | } 28 | 29 | > * { 30 | @include vendor('transition', 'opacity 3s ease'); 31 | @include vendor('transition-delay', '0.5s'); 32 | opacity: 1; 33 | } 34 | 35 | .logo { 36 | @include vendor('transition', ( 37 | 'opacity 1.25s ease', 38 | 'transform 0.5s ease' 39 | )); 40 | @include vendor('transition-delay', '0s'); 41 | display: block; 42 | margin: 0 0 (_size(element-margin) * 0.75) 0; 43 | 44 | img { 45 | display: block; 46 | margin: 0 auto; 47 | max-width: 75%; 48 | } 49 | } 50 | } 51 | 52 | @include breakpoint('<=large') { 53 | @include padding(4em, 4em, (0, 0, -2em, 0)); 54 | 55 | &.alt { 56 | @include padding(5em, 4em, (1em, 0, 0, 0)); 57 | } 58 | } 59 | 60 | @include breakpoint('<=medium') { 61 | @include padding(4em, 3em, (0, 0, -2em, 0)); 62 | 63 | &.alt { 64 | @include padding(4em, 3em, (1em, 0, 0, 0)); 65 | } 66 | } 67 | 68 | @include breakpoint('<=small') { 69 | @include padding(3em, 2em, (0, 0, -1em, 0)); 70 | 71 | p { 72 | font-size: 1em; 73 | letter-spacing: 0; 74 | 75 | br { 76 | display: none; 77 | } 78 | } 79 | 80 | &.alt { 81 | @include padding(3em, 2em, (1em, 0, 0, 0)); 82 | 83 | h1 { 84 | font-size: 2.5em; 85 | } 86 | } 87 | } 88 | 89 | @include breakpoint('<=xsmall') { 90 | @include padding(3em, 1.5em, (0, 0, -1em, 0)); 91 | 92 | &.alt { 93 | @include padding(3em, 1.5em, (1em, 0, 0, 0)); 94 | } 95 | } 96 | 97 | @include breakpoint('<=xxsmall') { 98 | @include padding(2.5em, 1em, (0, 0, -1em, 0)); 99 | 100 | &.alt { 101 | @include padding(2.5em, 1em, (1em, 0, 0, 0)); 102 | } 103 | } 104 | 105 | body.is-preload & { 106 | &.alt { 107 | > * { 108 | opacity: 0; 109 | } 110 | 111 | .logo { 112 | @include vendor('transform', 'scale(0.8) rotate(-30deg)'); 113 | } 114 | } 115 | } 116 | } -------------------------------------------------------------------------------- /demo-template/assets/sass/layout/_main.scss: -------------------------------------------------------------------------------- 1 | /// 2 | /// Stellar by HTML5 UP 3 | /// html5up.net | @ajlkn 4 | /// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) 5 | /// 6 | 7 | /* Main */ 8 | 9 | #main { 10 | @include color(invert); 11 | border-radius: _size(border-radius-main); 12 | 13 | > .main { 14 | @include padding(5em, 5em); 15 | border-top: solid 1px _palette(invert, border); 16 | 17 | &:first-child { 18 | border-top: 0; 19 | } 20 | 21 | > .image.main:first-child { 22 | margin: -5em 0 5em -5em; 23 | width: calc(100% + 10em); 24 | border-top-right-radius: _size(border-radius-main); 25 | border-top-left-radius: _size(border-radius-main); 26 | border-bottom-right-radius: 0; 27 | border-bottom-left-radius: 0; 28 | 29 | img { 30 | border-top-right-radius: _size(border-radius-main); 31 | border-top-left-radius: _size(border-radius-main); 32 | border-bottom-right-radius: 0; 33 | border-bottom-left-radius: 0; 34 | } 35 | } 36 | } 37 | 38 | @include breakpoint('<=large') { 39 | > .main { 40 | @include padding(4em, 4em); 41 | 42 | > .image.main:first-child { 43 | margin: -4em 0 4em -4em; 44 | width: calc(100% + 8em); 45 | } 46 | } 47 | } 48 | 49 | @include breakpoint('<=medium') { 50 | > .main { 51 | @include padding(4em, 3em); 52 | 53 | > .image.main:first-child { 54 | margin: -4em 0 4em -3em; 55 | width: calc(100% + 6em); 56 | } 57 | } 58 | } 59 | 60 | @include breakpoint('<=small') { 61 | > .main { 62 | @include padding(3em, 2em); 63 | 64 | > .image.main:first-child { 65 | margin: -3em 0 2em -2em; 66 | width: calc(100% + 4em); 67 | } 68 | } 69 | } 70 | 71 | @include breakpoint('<=xsmall') { 72 | > .main { 73 | @include padding(3em, 1.5em); 74 | 75 | > .image.main:first-child { 76 | margin: -3em 0 1.5em -1.5em; 77 | width: calc(100% + 3em); 78 | } 79 | } 80 | } 81 | 82 | @include breakpoint('<=xxsmall') { 83 | border-radius: 0; 84 | 85 | > .main { 86 | @include padding(2.5em, 1em); 87 | 88 | > .image.main:first-child { 89 | margin: -2.5em 0 1.5em -1em; 90 | width: calc(100% + 2em); 91 | border-radius: 0; 92 | 93 | img { 94 | border-radius: 0; 95 | } 96 | } 97 | } 98 | } 99 | } -------------------------------------------------------------------------------- /demo-template/assets/sass/layout/_nav.scss: -------------------------------------------------------------------------------- 1 | /// 2 | /// Stellar by HTML5 UP 3 | /// html5up.net | @ajlkn 4 | /// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) 5 | /// 6 | 7 | /* Nav */ 8 | 9 | #nav { 10 | @include vendor('transition', ( 11 | 'background-color #{_duration(transition)} ease', 12 | 'border-top-left-radius #{_duration(transition)} ease', 13 | 'border-top-right-radius #{_duration(transition)} ease', 14 | 'padding #{_duration(transition)} ease', 15 | )); 16 | @include color-typography(invert); 17 | position: absolute; 18 | width: _size(inner); 19 | max-width: calc(100% - #{_size(element-margin) * 2}); 20 | padding: 1em; 21 | background-color: _palette(invert, bg-alt); 22 | border-top-left-radius: _size(border-radius-main); 23 | border-top-right-radius: _size(border-radius-main); 24 | cursor: default; 25 | text-align: center; 26 | 27 | & + #main { 28 | padding-top: 4.25em; 29 | } 30 | 31 | ul { 32 | margin: 0; 33 | padding: 0; 34 | list-style: none; 35 | 36 | li { 37 | @include vendor('transition', ( 38 | 'margin #{_duration(transition)} ease' 39 | )); 40 | display: inline-block; 41 | margin: 0 0.35em; 42 | padding: 0; 43 | vertical-align: middle; 44 | 45 | a { 46 | @include vendor('transition', ( 47 | 'font-size #{_duration(transition)} ease' 48 | )); 49 | display: inline-block; 50 | height: 2.25em; 51 | line-height: 2.25em; 52 | padding: 0 1.25em; 53 | border: 0; 54 | border-radius: _size(border-radius); 55 | box-shadow: inset 0 0 0 1px transparent; 56 | 57 | &:hover { 58 | background-color: _palette(invert, border-bg); 59 | } 60 | 61 | &.active { 62 | background-color: _palette(invert, bg); 63 | box-shadow: none; 64 | } 65 | } 66 | } 67 | } 68 | 69 | &.alt { 70 | position: fixed; 71 | top: 0; 72 | padding: 0.5em 1em; 73 | background-color: transparentize(_palette(invert, bg-alt), 0.05); 74 | border-top-left-radius: 0; 75 | border-top-right-radius: 0; 76 | z-index: _misc(z-index-base); 77 | 78 | ul { 79 | li { 80 | margin: 0 0.175em; 81 | 82 | a { 83 | font-size: 0.9em; 84 | } 85 | } 86 | } 87 | } 88 | 89 | @include breakpoint('<=small') { 90 | display: none; 91 | 92 | & + #main { 93 | padding-top: 0; 94 | } 95 | } 96 | } -------------------------------------------------------------------------------- /demo-template/assets/sass/layout/_wrapper.scss: -------------------------------------------------------------------------------- 1 | /// 2 | /// Stellar by HTML5 UP 3 | /// html5up.net | @ajlkn 4 | /// Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) 5 | /// 6 | 7 | /* Wrapper */ 8 | 9 | #wrapper { 10 | width: _size(inner); 11 | max-width: calc(100% - 4em); 12 | margin: 0 auto; 13 | 14 | @include breakpoint('<=xsmall') { 15 | max-width: calc(100% - 2em); 16 | } 17 | 18 | @include breakpoint('<=xxsmall') { 19 | max-width: 100%; 20 | } 21 | } -------------------------------------------------------------------------------- /demo-template/assets/sass/libs/_breakpoints.scss: -------------------------------------------------------------------------------- 1 | // breakpoints.scss v1.0 | @ajlkn | MIT licensed */ 2 | 3 | // Vars. 4 | 5 | /// Breakpoints. 6 | /// @var {list} 7 | $breakpoints: () !global; 8 | 9 | // Mixins. 10 | 11 | /// Sets breakpoints. 12 | /// @param {map} $x Breakpoints. 13 | @mixin breakpoints($x: ()) { 14 | $breakpoints: $x !global; 15 | } 16 | 17 | /// Wraps @content in a @media block targeting a specific orientation. 18 | /// @param {string} $orientation Orientation. 19 | @mixin orientation($orientation) { 20 | @media screen and (orientation: #{$orientation}) { 21 | @content; 22 | } 23 | } 24 | 25 | /// Wraps @content in a @media block using a given query. 26 | /// @param {string} $query Query. 27 | @mixin breakpoint($query: null) { 28 | 29 | $breakpoint: null; 30 | $op: null; 31 | $media: null; 32 | 33 | // Determine operator, breakpoint. 34 | 35 | // Greater than or equal. 36 | @if (str-slice($query, 0, 2) == '>=') { 37 | 38 | $op: 'gte'; 39 | $breakpoint: str-slice($query, 3); 40 | 41 | } 42 | 43 | // Less than or equal. 44 | @elseif (str-slice($query, 0, 2) == '<=') { 45 | 46 | $op: 'lte'; 47 | $breakpoint: str-slice($query, 3); 48 | 49 | } 50 | 51 | // Greater than. 52 | @elseif (str-slice($query, 0, 1) == '>') { 53 | 54 | $op: 'gt'; 55 | $breakpoint: str-slice($query, 2); 56 | 57 | } 58 | 59 | // Less than. 60 | @elseif (str-slice($query, 0, 1) == '<') { 61 | 62 | $op: 'lt'; 63 | $breakpoint: str-slice($query, 2); 64 | 65 | } 66 | 67 | // Not. 68 | @elseif (str-slice($query, 0, 1) == '!') { 69 | 70 | $op: 'not'; 71 | $breakpoint: str-slice($query, 2); 72 | 73 | } 74 | 75 | // Equal. 76 | @else { 77 | 78 | $op: 'eq'; 79 | $breakpoint: $query; 80 | 81 | } 82 | 83 | // Build media. 84 | @if ($breakpoint and map-has-key($breakpoints, $breakpoint)) { 85 | 86 | $a: map-get($breakpoints, $breakpoint); 87 | 88 | // Range. 89 | @if (type-of($a) == 'list') { 90 | 91 | $x: nth($a, 1); 92 | $y: nth($a, 2); 93 | 94 | // Max only. 95 | @if ($x == null) { 96 | 97 | // Greater than or equal (>= 0 / anything) 98 | @if ($op == 'gte') { 99 | $media: 'screen'; 100 | } 101 | 102 | // Less than or equal (<= y) 103 | @elseif ($op == 'lte') { 104 | $media: 'screen and (max-width: ' + $y + ')'; 105 | } 106 | 107 | // Greater than (> y) 108 | @elseif ($op == 'gt') { 109 | $media: 'screen and (min-width: ' + ($y + 1) + ')'; 110 | } 111 | 112 | // Less than (< 0 / invalid) 113 | @elseif ($op == 'lt') { 114 | $media: 'screen and (max-width: -1px)'; 115 | } 116 | 117 | // Not (> y) 118 | @elseif ($op == 'not') { 119 | $media: 'screen and (min-width: ' + ($y + 1) + ')'; 120 | } 121 | 122 | // Equal (<= y) 123 | @else { 124 | $media: 'screen and (max-width: ' + $y + ')'; 125 | } 126 | 127 | } 128 | 129 | // Min only. 130 | @else if ($y == null) { 131 | 132 | // Greater than or equal (>= x) 133 | @if ($op == 'gte') { 134 | $media: 'screen and (min-width: ' + $x + ')'; 135 | } 136 | 137 | // Less than or equal (<= inf / anything) 138 | @elseif ($op == 'lte') { 139 | $media: 'screen'; 140 | } 141 | 142 | // Greater than (> inf / invalid) 143 | @elseif ($op == 'gt') { 144 | $media: 'screen and (max-width: -1px)'; 145 | } 146 | 147 | // Less than (< x) 148 | @elseif ($op == 'lt') { 149 | $media: 'screen and (max-width: ' + ($x - 1) + ')'; 150 | } 151 | 152 | // Not (< x) 153 | @elseif ($op == 'not') { 154 | $media: 'screen and (max-width: ' + ($x - 1) + ')'; 155 | } 156 | 157 | // Equal (>= x) 158 | @else { 159 | $media: 'screen and (min-width: ' + $x + ')'; 160 | } 161 | 162 | } 163 | 164 | // Min and max. 165 | @else { 166 | 167 | // Greater than or equal (>= x) 168 | @if ($op == 'gte') { 169 | $media: 'screen and (min-width: ' + $x + ')'; 170 | } 171 | 172 | // Less than or equal (<= y) 173 | @elseif ($op == 'lte') { 174 | $media: 'screen and (max-width: ' + $y + ')'; 175 | } 176 | 177 | // Greater than (> y) 178 | @elseif ($op == 'gt') { 179 | $media: 'screen and (min-width: ' + ($y + 1) + ')'; 180 | } 181 | 182 | // Less than (< x) 183 | @elseif ($op == 'lt') { 184 | $media: 'screen and (max-width: ' + ($x - 1) + ')'; 185 | } 186 | 187 | // Not (< x and > y) 188 | @elseif ($op == 'not') { 189 | $media: 'screen and (max-width: ' + ($x - 1) + '), screen and (min-width: ' + ($y + 1) + ')'; 190 | } 191 | 192 | // Equal (>= x and <= y) 193 | @else { 194 | $media: 'screen and (min-width: ' + $x + ') and (max-width: ' + $y + ')'; 195 | } 196 | 197 | } 198 | 199 | } 200 | 201 | // String. 202 | @else { 203 | 204 | // Missing a media type? Prefix with "screen". 205 | @if (str-slice($a, 0, 1) == '(') { 206 | $media: 'screen and ' + $a; 207 | } 208 | 209 | // Otherwise, use as-is. 210 | @else { 211 | $media: $a; 212 | } 213 | 214 | } 215 | 216 | } 217 | 218 | // Output. 219 | @media #{$media} { 220 | @content; 221 | } 222 | 223 | } -------------------------------------------------------------------------------- /demo-template/assets/sass/libs/_functions.scss: -------------------------------------------------------------------------------- 1 | /// Removes a specific item from a list. 2 | /// @author Hugo Giraudel 3 | /// @param {list} $list List. 4 | /// @param {integer} $index Index. 5 | /// @return {list} Updated list. 6 | @function remove-nth($list, $index) { 7 | 8 | $result: null; 9 | 10 | @if type-of($index) != number { 11 | @warn "$index: #{quote($index)} is not a number for `remove-nth`."; 12 | } 13 | @else if $index == 0 { 14 | @warn "List index 0 must be a non-zero integer for `remove-nth`."; 15 | } 16 | @else if abs($index) > length($list) { 17 | @warn "List index is #{$index} but list is only #{length($list)} item long for `remove-nth`."; 18 | } 19 | @else { 20 | 21 | $result: (); 22 | $index: if($index < 0, length($list) + $index + 1, $index); 23 | 24 | @for $i from 1 through length($list) { 25 | 26 | @if $i != $index { 27 | $result: append($result, nth($list, $i)); 28 | } 29 | 30 | } 31 | 32 | } 33 | 34 | @return $result; 35 | 36 | } 37 | 38 | /// Gets a value from a map. 39 | /// @author Hugo Giraudel 40 | /// @param {map} $map Map. 41 | /// @param {string} $keys Key(s). 42 | /// @return {string} Value. 43 | @function val($map, $keys...) { 44 | 45 | @if nth($keys, 1) == null { 46 | $keys: remove-nth($keys, 1); 47 | } 48 | 49 | @each $key in $keys { 50 | $map: map-get($map, $key); 51 | } 52 | 53 | @return $map; 54 | 55 | } 56 | 57 | /// Gets a duration value. 58 | /// @param {string} $keys Key(s). 59 | /// @return {string} Value. 60 | @function _duration($keys...) { 61 | @return val($duration, $keys...); 62 | } 63 | 64 | /// Gets a font value. 65 | /// @param {string} $keys Key(s). 66 | /// @return {string} Value. 67 | @function _font($keys...) { 68 | @return val($font, $keys...); 69 | } 70 | 71 | /// Gets a misc value. 72 | /// @param {string} $keys Key(s). 73 | /// @return {string} Value. 74 | @function _misc($keys...) { 75 | @return val($misc, $keys...); 76 | } 77 | 78 | /// Gets a palette value. 79 | /// @param {string} $keys Key(s). 80 | /// @return {string} Value. 81 | @function _palette($keys...) { 82 | @return val($palette, $keys...); 83 | } 84 | 85 | /// Gets a size value. 86 | /// @param {string} $keys Key(s). 87 | /// @return {string} Value. 88 | @function _size($keys...) { 89 | @return val($size, $keys...); 90 | } -------------------------------------------------------------------------------- /demo-template/assets/sass/libs/_html-grid.scss: -------------------------------------------------------------------------------- 1 | // html-grid.scss v1.0 | @ajlkn | MIT licensed */ 2 | 3 | // Mixins. 4 | 5 | /// Initializes the current element as an HTML grid. 6 | /// @param {mixed} $gutters Gutters (either a single number to set both column/row gutters, or a list to set them individually). 7 | /// @param {mixed} $suffix Column class suffix (optional; either a single suffix or a list). 8 | @mixin html-grid($gutters: 1.5em, $suffix: '') { 9 | 10 | // Initialize. 11 | $cols: 12; 12 | $multipliers: 0, 0.25, 0.5, 1, 1.50, 2.00; 13 | $unit: 100% / $cols; 14 | 15 | // Suffixes. 16 | $suffixes: null; 17 | 18 | @if (type-of($suffix) == 'list') { 19 | $suffixes: $suffix; 20 | } 21 | @else { 22 | $suffixes: ($suffix); 23 | } 24 | 25 | // Gutters. 26 | $guttersCols: null; 27 | $guttersRows: null; 28 | 29 | @if (type-of($gutters) == 'list') { 30 | 31 | $guttersCols: nth($gutters, 1); 32 | $guttersRows: nth($gutters, 2); 33 | 34 | } 35 | @else { 36 | 37 | $guttersCols: $gutters; 38 | $guttersRows: 0; 39 | 40 | } 41 | 42 | // Row. 43 | display: flex; 44 | flex-wrap: wrap; 45 | box-sizing: border-box; 46 | align-items: stretch; 47 | 48 | // Columns. 49 | > * { 50 | box-sizing: border-box; 51 | } 52 | 53 | // Gutters. 54 | &.gtr-uniform { 55 | > * { 56 | > :last-child { 57 | margin-bottom: 0; 58 | } 59 | } 60 | } 61 | 62 | // Alignment. 63 | &.aln-left { 64 | justify-content: flex-start; 65 | } 66 | 67 | &.aln-center { 68 | justify-content: center; 69 | } 70 | 71 | &.aln-right { 72 | justify-content: flex-end; 73 | } 74 | 75 | &.aln-top { 76 | align-items: flex-start; 77 | } 78 | 79 | &.aln-middle { 80 | align-items: center; 81 | } 82 | 83 | &.aln-bottom { 84 | align-items: flex-end; 85 | } 86 | 87 | // Step through suffixes. 88 | @each $suffix in $suffixes { 89 | 90 | // Suffix. 91 | @if ($suffix != '') { 92 | $suffix: '-' + $suffix; 93 | } 94 | @else { 95 | $suffix: ''; 96 | } 97 | 98 | // Row. 99 | 100 | // Important. 101 | > .imp#{$suffix} { 102 | order: -1; 103 | } 104 | 105 | // Columns, offsets. 106 | @for $i from 1 through $cols { 107 | > .col-#{$i}#{$suffix} { 108 | width: $unit * $i; 109 | } 110 | 111 | > .off-#{$i}#{$suffix} { 112 | margin-left: $unit * $i; 113 | } 114 | } 115 | 116 | // Step through multipliers. 117 | @each $multiplier in $multipliers { 118 | 119 | // Gutters. 120 | $class: null; 121 | 122 | @if ($multiplier != 1) { 123 | $class: '.gtr-' + ($multiplier * 100); 124 | } 125 | 126 | &#{$class} { 127 | margin-top: ($guttersRows * $multiplier * -1); 128 | margin-left: ($guttersCols * $multiplier * -1); 129 | 130 | > * { 131 | padding: ($guttersRows * $multiplier) 0 0 ($guttersCols * $multiplier); 132 | } 133 | 134 | // Uniform. 135 | &.gtr-uniform { 136 | margin-top: $guttersCols * $multiplier * -1; 137 | 138 | > * { 139 | padding-top: $guttersCols * $multiplier; 140 | } 141 | } 142 | 143 | } 144 | 145 | } 146 | 147 | } 148 | 149 | } -------------------------------------------------------------------------------- /demo-template/assets/sass/libs/_mixins.scss: -------------------------------------------------------------------------------- 1 | /// Makes an element's :before pseudoelement a FontAwesome icon. 2 | /// @param {string} $content Optional content value to use. 3 | /// @param {string} $where Optional pseudoelement to target (before or after). 4 | @mixin icon($content: false, $where: before) { 5 | 6 | text-decoration: none; 7 | 8 | &:#{$where} { 9 | 10 | @if $content { 11 | content: $content; 12 | } 13 | 14 | -moz-osx-font-smoothing: grayscale; 15 | -webkit-font-smoothing: antialiased; 16 | font-family: FontAwesome; 17 | font-style: normal; 18 | font-weight: normal; 19 | text-transform: none !important; 20 | 21 | } 22 | 23 | } 24 | 25 | /// Applies padding to an element, taking the current element-margin value into account. 26 | /// @param {mixed} $tb Top/bottom padding. 27 | /// @param {mixed} $lr Left/right padding. 28 | /// @param {list} $pad Optional extra padding (in the following order top, right, bottom, left) 29 | /// @param {bool} $important If true, adds !important. 30 | @mixin padding($tb, $lr, $pad: (0,0,0,0), $important: null) { 31 | 32 | @if $important { 33 | $important: '!important'; 34 | } 35 | 36 | $x: 0.1em; 37 | 38 | @if unit(_size(element-margin)) == 'rem' { 39 | $x: 0.1rem; 40 | } 41 | 42 | padding: ($tb + nth($pad,1)) ($lr + nth($pad,2)) max($x, $tb - _size(element-margin) + nth($pad,3)) ($lr + nth($pad,4)) #{$important}; 43 | 44 | } 45 | 46 | /// Encodes a SVG data URL so IE doesn't choke (via codepen.io/jakob-e/pen/YXXBrp). 47 | /// @param {string} $svg SVG data URL. 48 | /// @return {string} Encoded SVG data URL. 49 | @function svg-url($svg) { 50 | 51 | $svg: str-replace($svg, '"', '\''); 52 | $svg: str-replace($svg, '%', '%25'); 53 | $svg: str-replace($svg, '<', '%3C'); 54 | $svg: str-replace($svg, '>', '%3E'); 55 | $svg: str-replace($svg, '&', '%26'); 56 | $svg: str-replace($svg, '#', '%23'); 57 | $svg: str-replace($svg, '{', '%7B'); 58 | $svg: str-replace($svg, '}', '%7D'); 59 | $svg: str-replace($svg, ';', '%3B'); 60 | 61 | @return url("data:image/svg+xml;charset=utf8,#{$svg}"); 62 | 63 | } -------------------------------------------------------------------------------- /demo-template/assets/sass/libs/_vars.scss: -------------------------------------------------------------------------------- 1 | // Misc. 2 | $misc: ( 3 | z-index-base: 10000 4 | ); 5 | 6 | // Duration. 7 | $duration: ( 8 | transition: 0.2s 9 | ); 10 | 11 | // Size. 12 | $size: ( 13 | border-radius: 8px, 14 | border-radius-main: 0.25em, 15 | element-height: 2.75em, 16 | element-margin: 2em, 17 | inner: 64em 18 | ); 19 | 20 | // Font. 21 | $font: ( 22 | family: ('Source Sans Pro', Helvetica, sans-serif), 23 | family-fixed: ('Courier New', monospace), 24 | weight: 300, 25 | weight-bold: 400, 26 | letter-spacing: -0.025em 27 | ); 28 | 29 | // Palette. 30 | $palette: ( 31 | bg: #935d8c, 32 | fg: rgba(255,255,255,0.65), 33 | fg-bold: #ffffff, 34 | fg-light: rgba(255,255,255,0.5), 35 | border: rgba(255,255,255,0.35), 36 | border-bg: rgba(255,255,255,0.075), 37 | border2: rgba(255,255,255,0.75), 38 | border2-bg: rgba(255,255,255,0.2), 39 | 40 | invert: ( 41 | bg: #ffffff, 42 | bg-alt: #f7f7f7, 43 | fg: #636363, 44 | fg-bold: #636363, 45 | fg-light: rgba(99,99,99,0.25), 46 | border: #dddddd, 47 | border-bg: rgba(222,222,222,0.25), 48 | border2: #dddddd, 49 | border2-bg: rgba(222,222,222,0.5), 50 | ), 51 | 52 | accent: #8cc9f0, 53 | accent1: #efa8b0, 54 | accent2: #c79cc8, 55 | accent3: #a89cc8, 56 | accent4: #9bb2e1, 57 | accent5: #8cc9f0, 58 | bg1: #e37682, 59 | bg2: #5f4d93 60 | ); -------------------------------------------------------------------------------- /demo-template/assets/sass/libs/_vendor.scss: -------------------------------------------------------------------------------- 1 | // vendor.scss v1.0 | @ajlkn | MIT licensed */ 2 | 3 | // Vars. 4 | 5 | /// Vendor prefixes. 6 | /// @var {list} 7 | $vendor-prefixes: ( 8 | '-moz-', 9 | '-webkit-', 10 | '-ms-', 11 | '' 12 | ); 13 | 14 | /// Properties that should be vendorized. 15 | /// Data via caniuse.com, github.com/postcss/autoprefixer, and developer.mozilla.org 16 | /// @var {list} 17 | $vendor-properties: ( 18 | 19 | // Animation. 20 | 'animation', 21 | 'animation-delay', 22 | 'animation-direction', 23 | 'animation-duration', 24 | 'animation-fill-mode', 25 | 'animation-iteration-count', 26 | 'animation-name', 27 | 'animation-play-state', 28 | 'animation-timing-function', 29 | 30 | // Appearance. 31 | 'appearance', 32 | 33 | // Backdrop filter. 34 | 'backdrop-filter', 35 | 36 | // Background image options. 37 | 'background-clip', 38 | 'background-origin', 39 | 'background-size', 40 | 41 | // Box sizing. 42 | 'box-sizing', 43 | 44 | // Clip path. 45 | 'clip-path', 46 | 47 | // Filter effects. 48 | 'filter', 49 | 50 | // Flexbox. 51 | 'align-content', 52 | 'align-items', 53 | 'align-self', 54 | 'flex', 55 | 'flex-basis', 56 | 'flex-direction', 57 | 'flex-flow', 58 | 'flex-grow', 59 | 'flex-shrink', 60 | 'flex-wrap', 61 | 'justify-content', 62 | 'order', 63 | 64 | // Font feature. 65 | 'font-feature-settings', 66 | 'font-language-override', 67 | 'font-variant-ligatures', 68 | 69 | // Font kerning. 70 | 'font-kerning', 71 | 72 | // Fragmented borders and backgrounds. 73 | 'box-decoration-break', 74 | 75 | // Grid layout. 76 | 'grid-column', 77 | 'grid-column-align', 78 | 'grid-column-end', 79 | 'grid-column-start', 80 | 'grid-row', 81 | 'grid-row-align', 82 | 'grid-row-end', 83 | 'grid-row-start', 84 | 'grid-template-columns', 85 | 'grid-template-rows', 86 | 87 | // Hyphens. 88 | 'hyphens', 89 | 'word-break', 90 | 91 | // Masks. 92 | 'mask', 93 | 'mask-border', 94 | 'mask-border-outset', 95 | 'mask-border-repeat', 96 | 'mask-border-slice', 97 | 'mask-border-source', 98 | 'mask-border-width', 99 | 'mask-clip', 100 | 'mask-composite', 101 | 'mask-image', 102 | 'mask-origin', 103 | 'mask-position', 104 | 'mask-repeat', 105 | 'mask-size', 106 | 107 | // Multicolumn. 108 | 'break-after', 109 | 'break-before', 110 | 'break-inside', 111 | 'column-count', 112 | 'column-fill', 113 | 'column-gap', 114 | 'column-rule', 115 | 'column-rule-color', 116 | 'column-rule-style', 117 | 'column-rule-width', 118 | 'column-span', 119 | 'column-width', 120 | 'columns', 121 | 122 | // Object fit. 123 | 'object-fit', 124 | 'object-position', 125 | 126 | // Regions. 127 | 'flow-from', 128 | 'flow-into', 129 | 'region-fragment', 130 | 131 | // Scroll snap points. 132 | 'scroll-snap-coordinate', 133 | 'scroll-snap-destination', 134 | 'scroll-snap-points-x', 135 | 'scroll-snap-points-y', 136 | 'scroll-snap-type', 137 | 138 | // Shapes. 139 | 'shape-image-threshold', 140 | 'shape-margin', 141 | 'shape-outside', 142 | 143 | // Tab size. 144 | 'tab-size', 145 | 146 | // Text align last. 147 | 'text-align-last', 148 | 149 | // Text decoration. 150 | 'text-decoration-color', 151 | 'text-decoration-line', 152 | 'text-decoration-skip', 153 | 'text-decoration-style', 154 | 155 | // Text emphasis. 156 | 'text-emphasis', 157 | 'text-emphasis-color', 158 | 'text-emphasis-position', 159 | 'text-emphasis-style', 160 | 161 | // Text size adjust. 162 | 'text-size-adjust', 163 | 164 | // Text spacing. 165 | 'text-spacing', 166 | 167 | // Transform. 168 | 'transform', 169 | 'transform-origin', 170 | 171 | // Transform 3D. 172 | 'backface-visibility', 173 | 'perspective', 174 | 'perspective-origin', 175 | 'transform-style', 176 | 177 | // Transition. 178 | 'transition', 179 | 'transition-delay', 180 | 'transition-duration', 181 | 'transition-property', 182 | 'transition-timing-function', 183 | 184 | // Unicode bidi. 185 | 'unicode-bidi', 186 | 187 | // User select. 188 | 'user-select', 189 | 190 | // Writing mode. 191 | 'writing-mode', 192 | 193 | ); 194 | 195 | /// Values that should be vendorized. 196 | /// Data via caniuse.com, github.com/postcss/autoprefixer, and developer.mozilla.org 197 | /// @var {list} 198 | $vendor-values: ( 199 | 200 | // Cross fade. 201 | 'cross-fade', 202 | 203 | // Element function. 204 | 'element', 205 | 206 | // Filter function. 207 | 'filter', 208 | 209 | // Flexbox. 210 | 'flex', 211 | 'inline-flex', 212 | 213 | // Grab cursors. 214 | 'grab', 215 | 'grabbing', 216 | 217 | // Gradients. 218 | 'linear-gradient', 219 | 'repeating-linear-gradient', 220 | 'radial-gradient', 221 | 'repeating-radial-gradient', 222 | 223 | // Grid layout. 224 | 'grid', 225 | 'inline-grid', 226 | 227 | // Image set. 228 | 'image-set', 229 | 230 | // Intrinsic width. 231 | 'max-content', 232 | 'min-content', 233 | 'fit-content', 234 | 'fill', 235 | 'fill-available', 236 | 'stretch', 237 | 238 | // Sticky position. 239 | 'sticky', 240 | 241 | // Transform. 242 | 'transform', 243 | 244 | // Zoom cursors. 245 | 'zoom-in', 246 | 'zoom-out', 247 | 248 | ); 249 | 250 | // Functions. 251 | 252 | /// Removes a specific item from a list. 253 | /// @author Hugo Giraudel 254 | /// @param {list} $list List. 255 | /// @param {integer} $index Index. 256 | /// @return {list} Updated list. 257 | @function remove-nth($list, $index) { 258 | 259 | $result: null; 260 | 261 | @if type-of($index) != number { 262 | @warn "$index: #{quote($index)} is not a number for `remove-nth`."; 263 | } 264 | @else if $index == 0 { 265 | @warn "List index 0 must be a non-zero integer for `remove-nth`."; 266 | } 267 | @else if abs($index) > length($list) { 268 | @warn "List index is #{$index} but list is only #{length($list)} item long for `remove-nth`."; 269 | } 270 | @else { 271 | 272 | $result: (); 273 | $index: if($index < 0, length($list) + $index + 1, $index); 274 | 275 | @for $i from 1 through length($list) { 276 | 277 | @if $i != $index { 278 | $result: append($result, nth($list, $i)); 279 | } 280 | 281 | } 282 | 283 | } 284 | 285 | @return $result; 286 | 287 | } 288 | 289 | /// Replaces a substring within another string. 290 | /// @author Hugo Giraudel 291 | /// @param {string} $string String. 292 | /// @param {string} $search Substring. 293 | /// @param {string} $replace Replacement. 294 | /// @return {string} Updated string. 295 | @function str-replace($string, $search, $replace: '') { 296 | 297 | $index: str-index($string, $search); 298 | 299 | @if $index { 300 | @return str-slice($string, 1, $index - 1) + $replace + str-replace(str-slice($string, $index + str-length($search)), $search, $replace); 301 | } 302 | 303 | @return $string; 304 | 305 | } 306 | 307 | /// Replaces a substring within each string in a list. 308 | /// @param {list} $strings List of strings. 309 | /// @param {string} $search Substring. 310 | /// @param {string} $replace Replacement. 311 | /// @return {list} Updated list of strings. 312 | @function str-replace-all($strings, $search, $replace: '') { 313 | 314 | @each $string in $strings { 315 | $strings: set-nth($strings, index($strings, $string), str-replace($string, $search, $replace)); 316 | } 317 | 318 | @return $strings; 319 | 320 | } 321 | 322 | // Mixins. 323 | 324 | /// Wraps @content in vendorized keyframe blocks. 325 | /// @param {string} $name Name. 326 | @mixin keyframes($name) { 327 | 328 | @-moz-keyframes #{$name} { @content; } 329 | @-webkit-keyframes #{$name} { @content; } 330 | @-ms-keyframes #{$name} { @content; } 331 | @keyframes #{$name} { @content; } 332 | 333 | } 334 | 335 | /// Vendorizes a declaration's property and/or value(s). 336 | /// @param {string} $property Property. 337 | /// @param {mixed} $value String/list of value(s). 338 | @mixin vendor($property, $value) { 339 | 340 | // Determine if property should expand. 341 | $expandProperty: index($vendor-properties, $property); 342 | 343 | // Determine if value should expand (and if so, add '-prefix-' placeholder). 344 | $expandValue: false; 345 | 346 | @each $x in $value { 347 | @each $y in $vendor-values { 348 | @if $y == str-slice($x, 1, str-length($y)) { 349 | 350 | $value: set-nth($value, index($value, $x), '-prefix-' + $x); 351 | $expandValue: true; 352 | 353 | } 354 | } 355 | } 356 | 357 | // Expand property? 358 | @if $expandProperty { 359 | @each $vendor in $vendor-prefixes { 360 | #{$vendor}#{$property}: #{str-replace-all($value, '-prefix-', $vendor)}; 361 | } 362 | } 363 | 364 | // Expand just the value? 365 | @elseif $expandValue { 366 | @each $vendor in $vendor-prefixes { 367 | #{$property}: #{str-replace-all($value, '-prefix-', $vendor)}; 368 | } 369 | } 370 | 371 | // Neither? Treat them as a normal declaration. 372 | @else { 373 | #{$property}: #{$value}; 374 | } 375 | 376 | } -------------------------------------------------------------------------------- /demo-template/assets/sass/main.scss: -------------------------------------------------------------------------------- 1 | @import 'libs/vars'; 2 | @import 'libs/functions'; 3 | @import 'libs/mixins'; 4 | @import 'libs/vendor'; 5 | @import 'libs/breakpoints'; 6 | @import 'libs/html-grid'; 7 | @import 'font-awesome.min.css'; 8 | @import 'https://fonts.googleapis.com/css?family=Source+Sans+Pro:300,400'; 9 | 10 | /* 11 | Stellar by HTML5 UP 12 | html5up.net | @ajlkn 13 | Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) 14 | */ 15 | 16 | // Breakpoints. 17 | 18 | @include breakpoints(( 19 | xlarge: ( 1281px, 1680px ), 20 | large: ( 981px, 1280px ), 21 | medium: ( 737px, 980px ), 22 | small: ( 481px, 736px ), 23 | xsmall: ( 361px, 480px ), 24 | xxsmall: ( null, 360px ) 25 | )); 26 | 27 | // Mixins. 28 | 29 | @mixin color($p) { 30 | @include color-typography($p); 31 | @include color-box($p); 32 | @include color-button($p); 33 | @include color-form($p); 34 | @include color-icon($p); 35 | @include color-list($p); 36 | @include color-section($p); 37 | @include color-table($p); 38 | @include color-spotlight($p); 39 | } 40 | 41 | // Base. 42 | 43 | @import 'base/reset'; 44 | @import 'base/page'; 45 | @import 'base/typography'; 46 | 47 | // Component. 48 | 49 | @import 'components/row'; 50 | @import 'components/box'; 51 | @import 'components/button'; 52 | @import 'components/form'; 53 | @import 'components/icon'; 54 | @import 'components/image'; 55 | @import 'components/list'; 56 | @import 'components/actions'; 57 | @import 'components/icons'; 58 | @import 'components/section'; 59 | @import 'components/table'; 60 | @import 'components/features'; 61 | @import 'components/statistics'; 62 | @import 'components/spotlight'; 63 | 64 | // Layout. 65 | 66 | @import 'layout/header'; 67 | @import 'layout/nav'; 68 | @import 'layout/main'; 69 | @import 'layout/footer'; 70 | @import 'layout/wrapper'; -------------------------------------------------------------------------------- /demo-template/assets/sass/noscript.scss: -------------------------------------------------------------------------------- 1 | @import 'libs/vars'; 2 | @import 'libs/functions'; 3 | @import 'libs/mixins'; 4 | @import 'libs/vendor'; 5 | @import 'libs/breakpoints'; 6 | @import 'libs/html-grid'; 7 | 8 | /* 9 | Stellar by HTML5 UP 10 | html5up.net | @ajlkn 11 | Free for personal and commercial use under the CCA 3.0 license (html5up.net/license) 12 | */ 13 | 14 | /* Header */ 15 | 16 | #header { 17 | body.is-preload & { 18 | &.alt { 19 | > * { 20 | opacity: 1; 21 | } 22 | 23 | .logo { 24 | @include vendor('transform', 'none'); 25 | } 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /demo-template/generic.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | Generic - Stellar by HTML5 UP 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 25 | 26 | 27 |
28 | 29 | 30 |
31 | 32 |

Magna feugiat lorem

33 |

Donec eget ex magna. Interdum et malesuada fames ac ante ipsum primis in faucibus. Pellentesque venenatis dolor imperdiet dolor mattis sagittis. Praesent rutrum sem diam, vitae egestas enim auctor sit amet. Pellentesque leo mauris, consectetur id ipsum sit amet, fergiat. Pellentesque in mi eu massa lacinia malesuada et a elit. Donec urna ex, lacinia in purus ac, pretium pulvinar mauris. Curabitur sapien risus, commodo eget turpis at, elementum convallis fames ac ante ipsum primis in faucibus.

34 |

Pellentesque venenatis dolor imperdiet dolor mattis sagittis. Praesent rutrum sem diam, vitae egestas enim auctor sit amet. Consequat leo mauris, consectetur id ipsum sit amet, fersapien risus, commodo eget turpis at, elementum convallis elit enim turpis lorem ipsum dolor sit amet feugiat. Phasellus convallis elit id ullamcorper pulvinar. Duis aliquam turpis mauris, eu ultricies erat malesuada quis. Aliquam dapibus, lacus eget hendrerit bibendum, urna est aliquam sem, sit amet est velit quis lorem.

35 |

Tempus veroeros

36 |

Cep risus aliquam gravida cep ut lacus amet. Adipiscing faucibus nunc placerat. Tempus adipiscing turpis non blandit accumsan eget lacinia nunc integer interdum amet aliquam ut orci non col ut ut praesent. Semper amet interdum mi. Phasellus enim laoreet ac ac commodo faucibus faucibus. Curae ante vestibulum ante. Blandit. Ante accumsan nisi eu placerat gravida placerat adipiscing in risus fusce vitae ac mi accumsan nunc in accumsan tempor blandit aliquet aliquet lobortis. Ultricies blandit lobortis praesent turpis. Adipiscing accumsan adipiscing adipiscing ac lacinia cep. Orci blandit a iaculis adipiscing ac. Vivamus ornare laoreet odio vis praesent nunc lorem mi. Erat. Tempus sem faucibus ac id. Vis in blandit. Nascetur ultricies blandit ac. Arcu aliquam. Accumsan mi eget adipiscing nulla. Non vestibulum ac interdum condimentum semper commodo massa arcu.

37 |
38 | 39 |
40 | 41 | 42 |
43 |
44 |

Aliquam sed mauris

45 |

Sed lorem ipsum dolor sit amet et nullam consequat feugiat consequat magna adipiscing tempus etiam dolore veroeros. eget dapibus mauris. Cras aliquet, nisl ut viverra sollicitudin, ligula erat egestas velit, vitae tincidunt odio.

46 | 49 |
50 |
51 |

Etiam feugiat

52 |
53 |
Address
54 |
1234 Somewhere Road • Nashville, TN 00000 • USA
55 |
Phone
56 |
(000) 000-0000 x 0000
57 |
Email
58 |
information@untitled.tld
59 |
60 | 67 |
68 | 69 |
70 | 71 |
72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /demo-template/images/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 15 | 16 | -------------------------------------------------------------------------------- /demo-template/images/pic01.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silexlabs/drag-drop-stage-component/3e9273608b0a85f7cee55419f8a4d258c9af946b/demo-template/images/pic01.jpg -------------------------------------------------------------------------------- /demo-template/images/pic02.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silexlabs/drag-drop-stage-component/3e9273608b0a85f7cee55419f8a4d258c9af946b/demo-template/images/pic02.jpg -------------------------------------------------------------------------------- /demo-template/images/pic03.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silexlabs/drag-drop-stage-component/3e9273608b0a85f7cee55419f8a4d258c9af946b/demo-template/images/pic03.jpg -------------------------------------------------------------------------------- /demo-template/images/pic04.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silexlabs/drag-drop-stage-component/3e9273608b0a85f7cee55419f8a4d258c9af946b/demo-template/images/pic04.jpg -------------------------------------------------------------------------------- /demo-template/images/pic05.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silexlabs/drag-drop-stage-component/3e9273608b0a85f7cee55419f8a4d258c9af946b/demo-template/images/pic05.jpg -------------------------------------------------------------------------------- /demo-template/images/pic06.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silexlabs/drag-drop-stage-component/3e9273608b0a85f7cee55419f8a4d258c9af946b/demo-template/images/pic06.jpg -------------------------------------------------------------------------------- /demo-template/index.html: -------------------------------------------------------------------------------- 1 | 2 | 7 | 8 | 9 | Stellar by HTML5 UP 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 27 | 28 | 29 | 37 | 38 | 39 |
40 | 41 | 42 |
43 |
44 |
45 |
46 |

Ipsum sed adipiscing

47 |
48 |

Sed lorem ipsum dolor sit amet nullam consequat feugiat consequat magna 49 | adipiscing magna etiam amet veroeros. Lorem ipsum dolor tempus sit cursus. 50 | Tempus nisl et nullam lorem ipsum dolor sit amet aliquam.

51 | 54 |
55 | 56 |
57 |
58 | 59 | 60 |
61 |
62 |

Magna veroeros

63 |
64 |
    65 |
  • 66 | 67 |

    Ipsum consequat

    68 |

    Sed lorem amet ipsum dolor et amet nullam consequat a feugiat consequat tempus veroeros sed consequat.

    69 |
  • 70 |
  • 71 | 72 |

    Amed sed feugiat

    73 |

    Sed lorem amet ipsum dolor et amet nullam consequat a feugiat consequat tempus veroeros sed consequat.

    74 |
  • 75 |
  • 76 | 77 |

    Dolor nullam

    78 |

    Sed lorem amet ipsum dolor et amet nullam consequat a feugiat consequat tempus veroeros sed consequat.

    79 |
  • 80 |
81 | 86 |
87 | 88 | 89 |
90 |
91 |

Ipsum consequat

92 |

Donec imperdiet consequat consequat. Suspendisse feugiat congue
93 | posuere. Nulla massa urna, fermentum eget quam aliquet.

94 |
95 |
    96 |
  • 97 | 98 | 5,120 Etiam 99 |
  • 100 |
  • 101 | 102 | 8,192 Magna 103 |
  • 104 |
  • 105 | 106 | 2,048 Tempus 107 |
  • 108 |
  • 109 | 110 | 4,096 Aliquam 111 |
  • 112 |
  • 113 | 114 | 1,024 Nullam 115 |
  • 116 |
117 |

Nam elementum nisl et mi a commodo porttitor. Morbi sit amet nisl eu arcu faucibus hendrerit vel a risus. Nam a orci mi, elementum ac arcu sit amet, fermentum pellentesque et purus. Integer maximus varius lorem, sed convallis diam accumsan sed. Etiam porttitor placerat sapien, sed eleifend a enim pulvinar faucibus semper quis ut arcu. Ut non nisl a mollis est efficitur vestibulum. Integer eget purus nec nulla mattis et accumsan ut magna libero. Morbi auctor iaculis porttitor. Sed ut magna ac risus et hendrerit scelerisque. Praesent eleifend lacus in lectus aliquam porta. Cras eu ornare dui curabitur lacinia.

118 | 123 |
124 | 125 | 126 |
127 |
128 |

Congue imperdiet

129 |

Donec imperdiet consequat consequat. Suspendisse feugiat congue
130 | posuere. Nulla massa urna, fermentum eget quam aliquet.

131 |
132 | 138 |
139 | 140 |
141 | 142 | 143 |
144 |
145 |

Aliquam sed mauris

146 |

Sed lorem ipsum dolor sit amet et nullam consequat feugiat consequat magna adipiscing tempus etiam dolore veroeros. eget dapibus mauris. Cras aliquet, nisl ut viverra sollicitudin, ligula erat egestas velit, vitae tincidunt odio.

147 | 150 |
151 |
152 |

Etiam feugiat

153 |
154 |
Address
155 |
1234 Somewhere Road • Nashville, TN 00000 • USA
156 |
Phone
157 |
(000) 000-0000 x 0000
158 |
Email
159 |
information@untitled.tld
160 |
161 | 168 |
169 | 170 |
171 | 172 |
173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | -------------------------------------------------------------------------------- /favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/silexlabs/drag-drop-stage-component/3e9273608b0a85f7cee55419f8a4d258c9af946b/favicon.png -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | projects: [ 3 | { 4 | globals: { 5 | 'ts-jest': { 6 | diagnostics: {ignoreCodes: [151001]}, // prevent anoying warning 7 | }, 8 | }, 9 | preset: 'ts-jest', 10 | runner: '@jest-runner/electron/main', 11 | testEnvironment: 'jsdom', 12 | testEnvironmentOptions: { 13 | url: "http://localhost/", 14 | }, 15 | testMatch: [__dirname + '/tests/**/*.test.jsdom.ts'] 16 | }, 17 | { 18 | globals: { 19 | 'ts-jest': { 20 | diagnostics: {ignoreCodes: [151001]}, // prevent anoying warning 21 | }, 22 | }, 23 | preset: 'ts-jest', 24 | runner: '@jest-runner/electron', 25 | testEnvironment: '@jest-runner/electron/environment', 26 | testEnvironmentOptions: { 27 | url: "http://localhost/", 28 | }, 29 | testMatch: [__dirname + '/tests/**/*.test.electron.ts'] 30 | } 31 | ] 32 | }; 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "drag-drop-stage-component", 3 | "version": "1.0.35", 4 | "description": "A component to \"drag'n drop\"-enable your projects, maintained and simple, light on dependencies", 5 | "main": "src/ts/index.ts", 6 | "repository": { 7 | "type": "git", 8 | "url": "https://github.com/silexlabs/drag-drop-stage-component.git" 9 | }, 10 | "engines": { 11 | "node": ">=10.16.3 <17" 12 | }, 13 | "scripts": { 14 | "release": "", 15 | "prepare": "npm run build:demo", 16 | "prepublishOnly": "npm run build:demo", 17 | "lint": "echo NO LINTER YET", 18 | "test": "jest", 19 | "test:watch": "jest --watch", 20 | "test:update-snapshot": "jest --updateSnapshot", 21 | "serve": "http-server . -o", 22 | "build": "mkdir -p pub/js && npm run install:redux && npm run install:requirejs && npm run build:js", 23 | "watch": "npm run build:js -- --watch", 24 | "build:js": "tsc -p tsconfig.json", 25 | "build:demo": "npm run build && tsc -p tsconfig.json && pug src/jade/index.jade -o pub && lessc src/less/demo.less pub/css/demo.css", 26 | "install:requirejs": "cp `node_modules`/requirejs/require.js pub/require.js", 27 | "install:redux": "cp `node_modules`/redux/dist/redux.min.js pub/redux.js" 28 | }, 29 | "author": "Alex Hoyau (https://lexoyo.me/)", 30 | "license": "MIT", 31 | "dependencies": { 32 | "redux": "4.2.0" 33 | }, 34 | "devDependencies": { 35 | "@jest-runner/electron": "3.0.1", 36 | "@types/jest": "27.4.1", 37 | "ts-jest": "^26.0.0", 38 | "jest": "26.0.1", 39 | "electron": "20.0.3", 40 | "less": "4.1.3", 41 | "node_modules-path": "2.0.5", 42 | "pug-cli": "1.0.0-alpha6", 43 | "requirejs": "2.3.6", 44 | "typescript": "4.7.4" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/jade/index.jade: -------------------------------------------------------------------------------- 1 | doctype html 2 | html(lang="en") 3 | head 4 | title Stage demo 5 | link(rel="shortcut icon", href="../favicon.png") 6 | script(type='text/javascript', src='require.js') 7 | link(rel="stylesheet", href="css/demo.css") 8 | body 9 | .bg 10 | section 11 | iframe#iframe 12 | input#output(readonly) 13 | 14 | script(type='text/javascript') 15 | | require(['js/demo'], function() {}); 16 | -------------------------------------------------------------------------------- /src/less/demo.less: -------------------------------------------------------------------------------- 1 | body { 2 | width:100%; 3 | height:100%; 4 | position:absolute; 5 | margin: 0; 6 | padding: 0; 7 | overflow: hidden; 8 | .bg { 9 | width:100%; 10 | height:100%; 11 | position:absolute; 12 | margin: 0; 13 | padding: 0; 14 | background: repeating-linear-gradient( 15 | -45deg, 16 | #222, 17 | #222 10px, 18 | #333 10px, 19 | #333 20px 20 | ); 21 | } 22 | section { 23 | width: 90%; 24 | height: 90%; 25 | position: relative; 26 | margin: auto; 27 | margin-top: 5%; 28 | display: flex; 29 | flex-direction: column; 30 | justify-content: space-around; 31 | input#output { 32 | height: 50px; 33 | background: rgba(255, 255, 255, 0.8); 34 | margin-top: 15px; 35 | padding: 0 15px; 36 | overflow: hidden; 37 | color: #222; 38 | } 39 | iframe#iframe { 40 | flex: 1 1 auto; 41 | border: 1px solid; 42 | background-color: white; 43 | box-shadow: 0px 0px 20px rgba(0, 0, 0, 0.8); 44 | } 45 | button.button-bar-element { 46 | height: 5%; 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/ts/Constants.ts: -------------------------------------------------------------------------------- 1 | 2 | export const MIN_SIZE = 20; 3 | export const STICK_DISTANCE = 5; 4 | -------------------------------------------------------------------------------- /src/ts/Keyboard.ts: -------------------------------------------------------------------------------- 1 | import { StageStore } from './flux/StageStore'; 2 | import { addEvent } from './utils/Events'; 3 | import { setMode } from './flux/UiState'; 4 | import { reset } from './flux/SelectionState'; 5 | import { UiMode, Hooks } from './Types'; 6 | 7 | export class Keyboard { 8 | constructor(private win: Window, private store: StageStore, private hooks: Hooks) { 9 | // events from inside the iframe 10 | this.unsubscribeAll.push( 11 | addEvent(window, 'keydown', (e: KeyboardEvent) => this.onKeyDown(e)), 12 | addEvent(win, 'keydown', (e: KeyboardEvent) => this.onKeyDown(e)), 13 | ); 14 | } 15 | 16 | private unsubscribeAll: Array<() => void> = []; 17 | cleanup() { 18 | this.unsubscribeAll.forEach(u => u()); 19 | } 20 | 21 | /** 22 | * handle shortcuts 23 | */ 24 | private onKeyDown(e: KeyboardEvent) { 25 | const key = e.key; 26 | const state = this.store.getState(); 27 | const target = e.target as HTMLElement; 28 | 29 | if(state.ui.catchingEvents && 30 | target.tagName.toLowerCase() !== 'input' && 31 | target.tagName.toLowerCase() !== 'textarea' && 32 | !target.hasAttribute('contenteditable')) { 33 | switch(key) { 34 | case 'Escape': 35 | if(state.ui.mode !== UiMode.NONE) { 36 | this.store.dispatch(setMode(UiMode.NONE)); 37 | this.store.dispatch(reset()); 38 | } 39 | break; 40 | case 'Enter': 41 | if(this.hooks.onEdit) this.hooks.onEdit(); 42 | break; 43 | default: 44 | return; 45 | } 46 | // only if we catched a shortcut 47 | e.preventDefault(); 48 | e.stopPropagation(); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/ts/README.md: -------------------------------------------------------------------------------- 1 | # About this folder 2 | 3 | > this is an effort to document the work in progress and what I plan to do - [read these issues if you want to help](https://github.com/silexlabs/drag-drop-stage-component/labels/ready) 4 | 5 | This folder contains the classes for the stage component. See the [main readme](../../../../) for a general introduction. 6 | 7 | ## General concept 8 | 9 | There is a store which has the app state. The app state is an [object described here](./ts/Types.ts#L24). It holds the state of the app, the cursor shape, all the elements which are tracked when moving the mouse around, with [the elements metrics and properties](./ts/Types.ts#L46). 10 | 11 | When the state is changed, the observers are notified and apply the change to the UI or the DOM. 12 | 13 | ## Stage 14 | 15 | The [Stage class is defined in index.ts](./index.ts), it is the entry point and the main class. It instanciates the other classes and controls their interactions. 16 | 17 | ## Mouse 18 | 19 | [Mouse](./Mouse.ts) classe handle the mouse and emits high level events. 20 | 21 | It listens to events in the iframe as well as outside the iframe, e.g. when the mouse goes outside the iframe while dragging an element, it keeps tracking the events. It uses the store to change the application state between "NONE", "DRAGGING", "RESIZING" and "DRAWING" states. 22 | 23 | ## Flux package 24 | 25 | This layer is in charge of interfacing with the flux library. It exposes a store, the `StageStore` class, a flux store which is typed and has convenient methods in our specific case. 26 | 27 | ## Handlers package 28 | 29 | Classes which are created when an action starts, and which will handle the move or resize of elements until they are released. 30 | 31 | * [MoveHandler](./handlers/MoveHandler.ts) 32 | * [ResizeHandler](./handlers/ResizeHandler.ts) 33 | * [DrawHandler](./handlers/DrawHandler.ts): this draws a rectangle and selects all the elements which intersect with it 34 | 35 | ## Observers package 36 | 37 | This layer listens to the store and apply the changes to the DOM or the UI. For exapmple if an element's position has changed in the store, [this is where the changes are actually applied to the element](./observers/SelectablesObserver.ts#L57). 38 | 39 | This package is what React handles in a React app. Here the app handles it itself. 40 | -------------------------------------------------------------------------------- /src/ts/Types.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface Hooks { 3 | getId?: (el: HTMLElement) => string; 4 | isSelectable?: (el: HTMLElement) => boolean; 5 | isDraggable?: (el: HTMLElement) => boolean; 6 | isDropZone?: (el: HTMLElement) => boolean; 7 | isResizeable?: (el: HTMLElement) => (Direction | boolean); 8 | useMinHeight?: (el: HTMLElement) => boolean; 9 | canDrop?: (el: HTMLElement, dropZone: HTMLElement) => boolean; 10 | onSelect?: (selectables: Array) => void; 11 | onStartDrag?: (selectables: Array) => void; 12 | onDrag?: (selectables: Array, boundingBox: FullBox) => void; 13 | onDrop?: (selectables: Array) => void; 14 | onStartResize?: (selectables: Array) => void; 15 | onResize?: (selectables: Array, boundingBox: FullBox) => void; 16 | onResizeEnd?: (selectables: Array) => void; 17 | onStartDraw?: () => void; 18 | onDraw?: (selectables: Array, boundingBox: FullBox) => void; 19 | onDrawEnd?: () => void; 20 | onEdit?: () => void; // this occures when the user double clicks or press enter with one or more elements selected 21 | onChange?: (selectables: Array) => void; 22 | } 23 | 24 | /** 25 | * @typedef {{ 26 | * selectables: {Array}, 27 | * mouse: {MouseState}, 28 | * ui: {{ 29 | * mode: UiMode, 30 | * }} 31 | * }} State 32 | */ 33 | export interface State { 34 | selectables: Array 35 | mouse: MouseState 36 | ui: UiState 37 | } 38 | 39 | export interface DropZone { 40 | nextElementSibling?: HTMLElement 41 | parent: HTMLElement 42 | distance?: number 43 | } 44 | 45 | export type Direction = { 46 | top: boolean 47 | left: boolean 48 | bottom: boolean 49 | right: boolean 50 | } 51 | 52 | /** 53 | * @typedef {{ 54 | * el: HTMLElement, 55 | * selected: boolean, 56 | * draggable: boolean, 57 | * resizeable: boolean, 58 | * isDropZone: boolean, 59 | * metrics: ElementMetrics, 60 | * }} SelectableState 61 | */ 62 | export interface SelectableState { 63 | id: string 64 | el: HTMLElement 65 | dropZone?: DropZone // for use by the move handler only 66 | selected: boolean 67 | hovered: boolean 68 | selectable: boolean 69 | draggable: boolean 70 | resizeable: Direction 71 | isDropZone: boolean 72 | useMinHeight: boolean 73 | metrics: ElementMetrics 74 | preventMetrics?: boolean // while being dragged, elements are out of the flow, do not apply styles 75 | translation?: {x: number, y: number} 76 | } 77 | 78 | export interface Box {top: T, left: T, bottom: T, right: T } 79 | export interface FullBox {top: number, left: number, bottom: number, right: number, width: number, height: number } 80 | /** 81 | * @typedef {{ 82 | * position: {string} 83 | * margin: Box 84 | * padding: Box 85 | * border: Box 86 | * computedStyleRect: FullBox 87 | * clientRect: FullBox 88 | * }} ElementMetrics 89 | */ 90 | export interface ElementMetrics { 91 | position: string 92 | margin: Box 93 | padding: Box 94 | border: Box 95 | computedStyleRect: FullBox 96 | clientRect: FullBox 97 | proportions: number 98 | } 99 | 100 | export enum Side { LEFT, RIGHT, TOP, BOTTOM } 101 | export type Sticky = Box; 102 | export const EMPTY_STICKY_BOX: () => Sticky = () => ({top: null, left: null, bottom: null, right: null}); 103 | export const EMPTY_BOX: () => Box = () => ({top: null, left: null, bottom: null, right: null}); 104 | 105 | export interface UiState { 106 | mode: UiMode 107 | refreshing: boolean 108 | catchingEvents: boolean 109 | sticky: Sticky 110 | enableSticky: boolean 111 | } 112 | 113 | /** 114 | * @enum = { 115 | * NONE 116 | * DRAG 117 | * RESIZE 118 | * DRAW 119 | * } UiMode 120 | */ 121 | export enum UiMode { 122 | NONE, 123 | DRAG, 124 | RESIZE, 125 | DRAW, 126 | HIDE, 127 | } 128 | 129 | export interface MouseState { 130 | scrollData: ScrollData 131 | cursorData: CursorData 132 | mouseData: MouseData 133 | } 134 | 135 | export interface ScrollData { 136 | x: number 137 | y: number 138 | } 139 | 140 | export interface CursorData { 141 | x: string 142 | y: string 143 | cursorType: string 144 | } 145 | 146 | export interface MouseData { 147 | movementX: number 148 | movementY: number 149 | mouseX: number 150 | mouseY: number 151 | shiftKey: boolean 152 | target: HTMLElement 153 | hovered: HTMLElement[] 154 | } 155 | -------------------------------------------------------------------------------- /src/ts/demo.ts: -------------------------------------------------------------------------------- 1 | import {Stage} from './index'; 2 | 3 | // find the empty iframe in the page 4 | const iframe = document.querySelector('#iframe') as HTMLIFrameElement; 5 | const output = document.querySelector('#output') as HTMLInputElement; 6 | // load some content in the iframe 7 | iframe.src = '../demo-template/' 8 | iframe.onload = () => { 9 | // create the Stage class 10 | window['stage'] = new Stage(iframe, iframe.contentDocument.querySelectorAll('.stage-element'), { 11 | onDrop: (selectables) => { 12 | const dropElement = selectables[0].dropZone.parent; 13 | const str = `${ selectables.length } elements have been droped to ${ dropElement.tagName.toLocaleLowerCase() }${ dropElement.id ? '#' + dropElement.id : '' }${ Array.from(dropElement.classList).map(c => '.' + c).join('') }`; 14 | // console.log(str); 15 | output.value = str; 16 | }, 17 | onDrag: (selectables, boundingBox) => { 18 | const str = `${ selectables.length } elements are being draged`; 19 | // console.log(str); 20 | output.value = str; 21 | }, 22 | onResize: (selectables, boundingBox) => { 23 | const str = `${ selectables.length } elements have been resizeed to ${JSON.stringify(boundingBox)}`; 24 | // console.log(str); 25 | output.value = str; 26 | }, 27 | onDraw: (selectables, boundingBox) => { 28 | const str = `Drawing region: ${JSON.stringify(boundingBox)}`; 29 | // console.log(str); 30 | output.value = str; 31 | }, 32 | onSelect: (selectables) => { 33 | const str = `${ selectables.length } elements have been (un)selected`; 34 | // console.log(str); 35 | output.value = str; 36 | }, 37 | }); 38 | }; 39 | -------------------------------------------------------------------------------- /src/ts/flux/MouseState.ts: -------------------------------------------------------------------------------- 1 | import * as DomMetrics from '../utils/DomMetrics'; 2 | import * as types from '../Types'; 3 | 4 | const MOUSE_SCROLL = 'MOUSE_SCROLL'; 5 | export const setScroll = (scrollData: types.ScrollData) => ({ 6 | type: MOUSE_SCROLL, 7 | scrollData, 8 | }); 9 | 10 | const MOUSE_CURSOR = 'MOUSE_CURSOR'; 11 | export const setCursorData = (cursorData: types.CursorData) => ({ 12 | type: MOUSE_CURSOR, 13 | cursorData, 14 | }); 15 | 16 | const MOUSE_DATA = 'MOUSE_DATA'; 17 | export const setMouseData = (mouseData: types.MouseData) => ({ 18 | type: MOUSE_DATA, 19 | mouseData, 20 | }); 21 | 22 | export const mouse = (state: types.MouseState=getDefaultState(), action: any) => { 23 | switch(action.type) { 24 | case MOUSE_SCROLL: 25 | return { 26 | ...state, 27 | scrollData: action.scrollData, 28 | }; 29 | case MOUSE_CURSOR: 30 | return { 31 | ...state, 32 | cursorData: action.cursorData, 33 | } 34 | case MOUSE_DATA: 35 | return { 36 | ...state, 37 | mouseData: action.mouseData, 38 | } 39 | default: 40 | return state; 41 | } 42 | }; 43 | 44 | export const getDefaultState = (): types.MouseState => { 45 | return { 46 | scrollData: {x: 0, y: 0}, 47 | cursorData: {x: '', y: '', cursorType: ''}, 48 | mouseData: { 49 | movementX: 0, 50 | movementY: 0, 51 | mouseX: 0, 52 | mouseY: 0, 53 | shiftKey: false, 54 | target: null, 55 | hovered: [], 56 | }, 57 | }; 58 | } 59 | -------------------------------------------------------------------------------- /src/ts/flux/SelectableState.ts: -------------------------------------------------------------------------------- 1 | import { SelectableState } from '../Types'; 2 | 3 | const UPDATE = 'SELECTABLE_UPDATE'; 4 | const RESET = 'SELECTABLE_RESET'; 5 | const CREATE = 'SELECTABLE_CREATE'; 6 | const DELETE = 'SELECTABLE_DELETE'; 7 | 8 | export const updateSelectables = (selectables: Array, preventDispatch: boolean = false) => ({ 9 | type: UPDATE, 10 | selectables, 11 | preventDispatch, 12 | }); 13 | export const resetSelectables = () => ({ 14 | type: RESET, 15 | }); 16 | export const createSelectable = (selectable: SelectableState) => ({ 17 | type: CREATE, 18 | selectable, 19 | }); 20 | export const deleteSelectable = (selectable: SelectableState) => ({ 21 | type: DELETE, 22 | selectable, 23 | }); 24 | 25 | export const selectables = (state: Array=[], action) => { 26 | switch(action.type) { 27 | case CREATE: 28 | return [ 29 | ...state, 30 | action.selectable, 31 | ]; 32 | case RESET: 33 | return []; 34 | case DELETE: 35 | return state.filter((selectable: SelectableState) => selectable.id !== action.selectable.id); 36 | case UPDATE: 37 | return state.map((selectable: SelectableState) => action.selectables.find(s => s.id === selectable.id) || selectable); 38 | default: 39 | return state; 40 | } 41 | }; 42 | -------------------------------------------------------------------------------- /src/ts/flux/SelectionState.ts: -------------------------------------------------------------------------------- 1 | import { SelectableState } from '../Types'; 2 | 3 | const SET = 'SELECTION_SET'; 4 | const RESET = 'SELECTION_RESET'; 5 | const TOGGLE = 'SELECTION_TOGGLE'; 6 | const ADD = 'SELECTION_ADD'; 7 | const REMOVE = 'SELECTION_REMOVE'; 8 | 9 | export const set = (selectables: Array) => ({ 10 | type: SET, 11 | selectables, 12 | }) 13 | export const reset = () => ({ 14 | type: RESET, 15 | }) 16 | export const toggle = (selectable: SelectableState) => ({ 17 | type: TOGGLE, 18 | selectable, 19 | }) 20 | export const add = (selectable: SelectableState) => ({ 21 | type: ADD, 22 | selectable, 23 | }) 24 | export const remove = (selectable: SelectableState) => ({ 25 | type: REMOVE, 26 | selectable, 27 | }) 28 | 29 | /** 30 | * reducer 31 | */ 32 | export const selection = (state: Array=[], action) => { 33 | switch (action.type) { 34 | case TOGGLE: 35 | return state.map(selectable => selectable === action.selectable ? { 36 | ...selectable, 37 | selected: !selectable.selected, 38 | } : selectable); 39 | case REMOVE: 40 | return state.map(selectable => selectable === action.selectable ? { 41 | ...selectable, 42 | selected: false, 43 | } : selectable); 44 | case RESET: 45 | return state.map(selectable => ({ 46 | ...selectable, 47 | selected: false, 48 | })); 49 | case ADD: 50 | return state.map(selectable => selectable === action.selectable ? { 51 | ...selectable, 52 | selected: true, 53 | } : selectable); 54 | case SET: 55 | return state.map(selectable => action.selectables.includes(selectable) ? { 56 | ...selectable, 57 | selected: true, 58 | } : { 59 | ...selectable, 60 | selected: false, 61 | }); 62 | default: 63 | return state; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/ts/flux/StageStore.ts: -------------------------------------------------------------------------------- 1 | import { applyMiddleware, combineReducers, Observable, createStore, Store } from 'redux'; 2 | 3 | import { MouseState, SelectableState, State, UiState } from '../Types'; 4 | import { selection } from './SelectionState'; 5 | import { ui } from './UiState'; 6 | import * as mouseState from './MouseState'; 7 | import * as selectableState from './SelectableState'; 8 | 9 | export class StageStore implements Store { 10 | [Symbol.observable](): Observable { 11 | return this as any as Observable; 12 | }; 13 | /** 14 | * Create a redux store with composed reducers 15 | * @return Store 16 | */ 17 | protected static createStore(): Store { 18 | 19 | const reducer = combineReducers({ 20 | selectables: (state: Array, action) => selectableState.selectables(selection(state, action), action), 21 | ui: (state: UiState, action) => ui(state, action), 22 | mouse: (state: MouseState, action) => mouseState.mouse(state, action), 23 | }); 24 | return createStore(reducer, applyMiddleware(StageStore.preventDispatchDuringRedraw)) as Store; 25 | }; 26 | 27 | // this is unused for now, I used the "refreshing" prop instead, on state.ui 28 | private static preventDispatchDuringRedraw({ getState }) { 29 | return next => action => { 30 | if (action.preventDispatch) { 31 | console.warn('prevent dispatch', action) 32 | } 33 | else { 34 | const returnValue = next(action) 35 | return returnValue 36 | } 37 | return null; 38 | } 39 | } 40 | 41 | /** 42 | * the main redux store 43 | * @type {Store} 44 | */ 45 | protected store: Store = StageStore.createStore(); 46 | 47 | /** 48 | * Subscribe to state changes with the ability to filter by substate 49 | * @param onChange callback to get the state and the previous state 50 | * @param select method to select the sub state 51 | * @return {function()} function to call to unsubscribe 52 | */ 53 | subscribe(onChange: (state:SubState, prevState:SubState) => void, select=(state:State):SubState => (state as any)) { 54 | let currentState = select(this.store.getState()); 55 | 56 | const handleChange = () => { 57 | let nextState = select(this.store.getState()); 58 | if (nextState !== currentState) { 59 | let prevState = currentState; 60 | currentState = nextState; 61 | onChange(currentState, prevState); 62 | } 63 | } 64 | return this.store.subscribe(handleChange); 65 | } 66 | // clone the object, not deep 67 | clone(obj: SubState): SubState { 68 | let res: any; 69 | if(obj instanceof Array) res = (obj as Array).slice() as any as SubState; 70 | else if(obj instanceof Object) res = { 71 | ...(obj as any as Object), 72 | } as SubState; 73 | else res = obj; 74 | if(obj === res) throw 'not cloned'; 75 | return res; 76 | } 77 | dispatch(action: any, cbk: () => void = null): any { 78 | this.store.dispatch(action); 79 | if(cbk) cbk(); 80 | return null; 81 | } 82 | getState(): State { 83 | return this.store.getState(); 84 | } 85 | replaceReducer() { 86 | throw new Error('not implemented'); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/ts/flux/UiState.ts: -------------------------------------------------------------------------------- 1 | import * as types from '../Types' 2 | 3 | export const UI_SET_MODE = 'UI_SET_MODE'; 4 | export const setMode = (mode: types.UiMode) => ({ 5 | type: UI_SET_MODE, 6 | mode, 7 | }); 8 | 9 | export const UI_SET_REFRESHING = 'UI_SET_REFRESHING'; 10 | export const setRefreshing = (refreshing: boolean) => ({ 11 | type: UI_SET_REFRESHING, 12 | refreshing, 13 | }); 14 | 15 | export const UI_SET_CATCHING_EVENTS = 'UI_SET_CATCHING_EVENTS'; 16 | export const setCatchingEvents = (catchingEvents: boolean) => ({ 17 | type: UI_SET_CATCHING_EVENTS, 18 | catchingEvents, 19 | }); 20 | 21 | export const UI_SET_STICKY = 'UI_SET_STICKY'; 22 | export const setSticky = (sticky: types.Sticky) => ({ 23 | type: UI_SET_STICKY, 24 | sticky, 25 | }); 26 | 27 | export const UI_SET_ENABLE_STICKY = 'UI_SET_ENABLE_STICKY'; 28 | export const setEnableSticky = (enableSticky: boolean) => ({ 29 | type: UI_SET_ENABLE_STICKY, 30 | enableSticky, 31 | }); 32 | 33 | /** 34 | * reducer 35 | */ 36 | export const ui = (state=getDefaultState(), action) => { 37 | switch(action.type) { 38 | case UI_SET_MODE: 39 | return { 40 | ...state, 41 | mode: action.mode, 42 | } 43 | case UI_SET_REFRESHING: 44 | return { 45 | ...state, 46 | refreshing: action.refreshing, 47 | } 48 | case UI_SET_CATCHING_EVENTS: 49 | return { 50 | ...state, 51 | catchingEvents: action.catchingEvents, 52 | } 53 | case UI_SET_STICKY: 54 | return { 55 | ...state, 56 | sticky: action.sticky, 57 | } 58 | case UI_SET_ENABLE_STICKY: 59 | return { 60 | ...state, 61 | enableSticky: action.enableSticky, 62 | } 63 | default: 64 | return state; 65 | } 66 | }; 67 | 68 | export const getDefaultState = () => { 69 | return { 70 | mode: types.UiMode.NONE, 71 | refreshing: false, 72 | catchingEvents: true, 73 | sticky: types.EMPTY_STICKY_BOX(), 74 | enableSticky: true, 75 | }; 76 | } 77 | -------------------------------------------------------------------------------- /src/ts/handlers/DrawHandler.ts: -------------------------------------------------------------------------------- 1 | import { StageStore } from '../flux/StageStore'; 2 | import { Hooks, MouseData, SelectableState, Box, FullBox } from '../Types'; 3 | import { MouseHandlerBase } from './MouseHandlerBase'; 4 | import * as selectionState from '../flux/SelectionState'; 5 | import * as mouseState from '../flux/MouseState'; 6 | import * as domMetrics from '../utils/DomMetrics'; 7 | 8 | export class DrawHandler extends MouseHandlerBase { 9 | initialX: number; 10 | initialY: number; 11 | regionMarker: HTMLElement; 12 | 13 | constructor(stageDocument: HTMLDocument, overlayDocument: HTMLDocument, store: StageStore, hooks: Hooks) { 14 | super(stageDocument, overlayDocument, store, hooks); 15 | 16 | // notify the app 17 | if(!!this.hooks.onStartDraw) this.hooks.onStartDraw(); 18 | 19 | const state = store.getState(); 20 | 21 | const scrollData = domMetrics.getScroll(this.stageDocument); 22 | this.initialX = state.mouse.mouseData.mouseX + scrollData.x; 23 | this.initialY = state.mouse.mouseData.mouseY + scrollData.y; 24 | 25 | // create and attach a div to draw the region 26 | // FIXME: the region marker should be outside the iframe 27 | this.regionMarker = overlayDocument.createElement('div'); 28 | this.regionMarker.classList.add('region-marker'); 29 | this.moveRegion({left: -999, top: -999, right: -999, bottom: -999, width: 0, height: 0}); 30 | overlayDocument.body.appendChild(this.regionMarker); 31 | } 32 | 33 | update(mouseData: MouseData) { 34 | super.update(mouseData); 35 | 36 | const scrollData = domMetrics.getScroll(this.stageDocument); 37 | const bb: FullBox = { 38 | left: Math.min(this.initialX, (mouseData.mouseX + scrollData.x)), 39 | top: Math.min(this.initialY, (mouseData.mouseY + scrollData.y)), 40 | right: Math.max(this.initialX, (mouseData.mouseX + scrollData.x)), 41 | bottom: Math.max(this.initialY, (mouseData.mouseY + scrollData.y)), 42 | height: Math.abs(this.initialY - (mouseData.mouseY + scrollData.y)), 43 | width: Math.abs(this.initialX - (mouseData.mouseX + scrollData.x)), 44 | }; 45 | 46 | // update the drawing 47 | this.moveRegion(bb); 48 | 49 | // select all elements which intersect with the region 50 | let newSelection = this.store.getState().selectables 51 | .filter(selectable => { 52 | return selectable.selectable && 53 | selectable.draggable && // do not select the background 54 | selectable.metrics.clientRect.left < bb.right && 55 | selectable.metrics.clientRect.right > bb.left && 56 | selectable.metrics.clientRect.top < bb.bottom && 57 | selectable.metrics.clientRect.bottom > bb.top; 58 | }); 59 | 60 | // handle removed elements 61 | this.selection 62 | .filter(selectable => !newSelection.find(s => selectable.el === s.el)) 63 | .forEach(selectable => { 64 | this.store.dispatch(selectionState.remove(selectable)); 65 | }); 66 | 67 | // handle added elements 68 | newSelection 69 | .filter(selectable => !this.selection.find(s => selectable.el === s.el)) 70 | .forEach(selectable => { 71 | this.store.dispatch(selectionState.add(selectable)); 72 | }); 73 | 74 | // store the new selection 75 | this.selection = newSelection; 76 | 77 | // update scroll 78 | const initialScroll = this.store.getState().mouse.scrollData; 79 | const scroll = domMetrics.getScrollToShow(this.stageDocument, bb); 80 | if(scroll.x !== initialScroll.x || scroll.y !== initialScroll.y) { 81 | this.debounceScroll(scroll); 82 | } 83 | 84 | // notify the app 85 | if(this.hooks.onDraw) this.hooks.onDraw(this.selection, bb); 86 | } 87 | 88 | 89 | release() { 90 | super.release(); 91 | this.regionMarker.parentNode.removeChild(this.regionMarker); 92 | 93 | // notify the app 94 | if(this.hooks.onDrawEnd) this.hooks.onDrawEnd(); 95 | 96 | this.selection = []; 97 | } 98 | 99 | /** 100 | * display the position marker atthe given positionin the dom 101 | */ 102 | moveRegion({left, top, width, height}: FullBox) { 103 | this.regionMarker.style.width = width + 'px'; 104 | this.regionMarker.style.height = height + 'px'; 105 | this.regionMarker.style.transform = `translate(${left}px, ${top}px)`; // scale(${width}, ${height}) 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/ts/handlers/MouseHandlerBase.ts: -------------------------------------------------------------------------------- 1 | import * as types from '../Types'; 2 | import { StageStore } from '../flux/StageStore'; 3 | import { Hooks, MouseData, SelectableState } from '../Types'; 4 | import * as mouseState from '../flux/MouseState'; 5 | 6 | export class MouseHandlerBase { 7 | selection: Array; 8 | unsubsribe: () => void; 9 | private unsubsribeScroll: () => void; 10 | constructor(protected stageDocument: HTMLDocument, private overlayDocument: HTMLDocument, protected store: StageStore, protected hooks: Hooks) { 11 | // store the selection 12 | this.selection = store.getState().selectables 13 | this.selection = this.selection.filter(selectable => selectable.selected); 14 | 15 | // kepp in sync with mouse 16 | this.unsubsribe = store.subscribe( 17 | (state: types.MouseState, prevState: types.MouseState) => this.update(state.mouseData), 18 | (state:types.State) => state.mouse 19 | ); 20 | 21 | // listen for scroll 22 | this.unsubsribeScroll = this.store.subscribe( 23 | (cur: types.ScrollData, prev: types.ScrollData) => this.onScroll(cur, prev), 24 | (state: types.State): types.ScrollData => state.mouse.scrollData 25 | ); 26 | } 27 | update(mouseData: MouseData) {}; 28 | release() { 29 | this.unsubsribeScroll(); 30 | this.unsubsribe(); 31 | }; 32 | 33 | 34 | /** 35 | * Debounce mechanism to handle auto scroll 36 | */ 37 | private debounceScrollPending = false; 38 | private debounceScrollData: types.ScrollData; 39 | protected debounceScroll(scrollData: types.ScrollData) { 40 | if(!this.debounceScrollPending) { 41 | setTimeout(() => { 42 | this.debounceScrollPending = false; 43 | this.store.dispatch(mouseState.setScroll(this.debounceScrollData)); 44 | }, 100); 45 | } 46 | this.debounceScrollPending = true; 47 | this.debounceScrollData = scrollData; 48 | } 49 | 50 | 51 | /** 52 | * move the dragged elements back under the mouse 53 | */ 54 | onScroll(state: types.ScrollData, prev: types.ScrollData) { 55 | const delta = { 56 | x: state.x - prev.x, 57 | y: state.y - prev.y, 58 | } 59 | const mouseData = this.store.getState().mouse.mouseData; 60 | // mouse did not move in the viewport, just in the document coordinate 61 | // the selection need to follow the mouse 62 | this.update({ 63 | ...mouseData, 64 | movementX: delta.x, 65 | movementY: delta.y, 66 | }) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/ts/handlers/ResizeHandler.ts: -------------------------------------------------------------------------------- 1 | import {MouseHandlerBase} from './MouseHandlerBase'; 2 | import { StageStore } from '../flux/StageStore'; 3 | import { Hooks, SelectableState, MouseData, CursorData, FullBox } from '../Types'; 4 | import * as selectableState from '../flux/SelectableState' 5 | import * as mouseState from '../flux/MouseState'; 6 | import * as domMetrics from '../utils/DomMetrics'; 7 | import { setRefreshing } from '../flux/UiState'; 8 | import { MIN_SIZE } from '../Constants'; 9 | 10 | export class ResizeHandler extends MouseHandlerBase { 11 | private cursorData: CursorData; 12 | constructor(stageDocument: HTMLDocument, overlayDocument: HTMLDocument, store: StageStore, hooks: Hooks) { 13 | super(stageDocument, overlayDocument, store, hooks); 14 | 15 | // direction 16 | this.cursorData = this.store.getState().mouse.cursorData; 17 | 18 | // keep only risizeable elements 19 | this.selection = this.selection.filter(s => domMetrics.isResizeable(s.resizeable, this.cursorData)); 20 | 21 | // notify the app 22 | if(!!this.hooks.onStartResize) this.hooks.onStartResize(this.selection); 23 | } 24 | 25 | /** 26 | * Called by the Stage class when mouse moves 27 | */ 28 | update(mouseData: MouseData) { 29 | super.update(mouseData); 30 | 31 | // set a new size 32 | this.selection = this.selection.map((selectable: SelectableState) => { 33 | // handle the width and height computation 34 | const clientRect = { 35 | ...selectable.metrics.clientRect, 36 | }; 37 | const computedStyleRect = { 38 | ...selectable.metrics.computedStyleRect, 39 | }; 40 | switch(this.cursorData.x) { 41 | case '': 42 | break; 43 | case 'left': 44 | computedStyleRect.width -= mouseData.movementX; 45 | clientRect.width -= mouseData.movementX; 46 | break; 47 | case 'right': 48 | computedStyleRect.width += mouseData.movementX; 49 | clientRect.width += mouseData.movementX; 50 | break; 51 | default: throw new Error('unknown direction ' + this.cursorData.x); 52 | } 53 | if(this.cursorData.y != '') { 54 | if(mouseData.shiftKey && this.cursorData.x != '') { 55 | computedStyleRect.height = computedStyleRect.width * selectable.metrics.proportions; 56 | clientRect.height = clientRect.width * selectable.metrics.proportions; 57 | } 58 | else { 59 | if(this.cursorData.y === 'top') { 60 | computedStyleRect.height -= mouseData.movementY; 61 | clientRect.height -= mouseData.movementY; 62 | } 63 | else { 64 | computedStyleRect.height += mouseData.movementY; 65 | clientRect.height += mouseData.movementY; 66 | } 67 | } 68 | } 69 | 70 | // handle the position change 71 | if(this.cursorData.x === 'left') { 72 | // compute the change 73 | computedStyleRect.left += mouseData.movementX; 74 | clientRect.left += mouseData.movementX; 75 | } 76 | if(this.cursorData.y === 'top') { 77 | // compute the change 78 | computedStyleRect.top += mouseData.movementY; 79 | clientRect.top += mouseData.movementY; 80 | } 81 | // handle the case where the resize has not been possible 82 | // either because the content is too big, or a min-whidth/height has overriden our changes 83 | if(this.cursorData.x !== '') { 84 | // store initial data 85 | const initialWidth = selectable.el.style.width; 86 | 87 | // move to the final position will take the new parent offset 88 | selectable.el.style.width = Math.max(MIN_SIZE, computedStyleRect.width) + 'px'; 89 | 90 | // check for the offset and update the metrics 91 | const bb = domMetrics.getBoundingBoxDocument(selectable.el); 92 | const delta = clientRect.width - bb.width; 93 | computedStyleRect.width -= delta; 94 | clientRect.width -= delta; 95 | if(this.cursorData.x === 'left') { 96 | computedStyleRect.left += delta; 97 | clientRect.left += delta; 98 | } 99 | // restore the initial data 100 | selectable.el.style.width = initialWidth; 101 | } 102 | // handle the case where the resize has not been possible 103 | // either because the content is too big, or a min-whidth/height has overriden our changes 104 | if(this.cursorData.y !== '') { 105 | // store initial data 106 | const heightAttr = selectable.useMinHeight ? 'minHeight' : 'height'; 107 | const initialHeight = selectable.el.style[heightAttr]; 108 | 109 | // move to the final position will take the new parent offset 110 | selectable.el.style[heightAttr] = Math.max(MIN_SIZE, computedStyleRect.height) + 'px'; 111 | 112 | // check for the offset and update the metrics 113 | const bb = domMetrics.getBoundingBoxDocument(selectable.el); 114 | const delta = clientRect.height - bb.height; 115 | computedStyleRect.height -= delta; 116 | clientRect.height -= delta; 117 | if(this.cursorData.y === 'top') { 118 | computedStyleRect.top += delta; 119 | clientRect.top += delta; 120 | } 121 | 122 | // restore the initial data 123 | selectable.el.style[heightAttr] = initialHeight; 124 | } 125 | 126 | // update bottom and right 127 | computedStyleRect.right = computedStyleRect.left + computedStyleRect.width; 128 | clientRect.right = clientRect.left + clientRect.width; 129 | computedStyleRect.bottom = computedStyleRect.top + computedStyleRect.height; 130 | clientRect.bottom = clientRect.top + clientRect.height; 131 | 132 | // update the metrics 133 | return { 134 | ...selectable, 135 | metrics: { 136 | ...selectable.metrics, 137 | clientRect, 138 | computedStyleRect, 139 | } 140 | }; 141 | }); 142 | // dispatch all the changes at once 143 | this.store.dispatch(selectableState.updateSelectables(this.selection)); 144 | 145 | // update scroll 146 | const initialScroll = this.store.getState().mouse.scrollData; 147 | const bb: FullBox = { 148 | top: mouseData.mouseY + initialScroll.y, 149 | left: mouseData.mouseX + initialScroll.x, 150 | bottom: mouseData.mouseY + initialScroll.y, 151 | right: mouseData.mouseX + initialScroll.x, 152 | height: 0, 153 | width: 0, 154 | }; 155 | const scroll = domMetrics.getScrollToShow(this.stageDocument, bb); 156 | if(scroll.x !== initialScroll.x || scroll.y !== initialScroll.y) { 157 | this.debounceScroll(scroll); 158 | } 159 | 160 | // notify the app 161 | if(this.hooks.onResize) this.hooks.onResize(this.selection, bb); 162 | } 163 | 164 | 165 | /** 166 | * Called by the Stage class when mouse button is released 167 | */ 168 | release() { 169 | super.release(); 170 | // reset the state of the mouse 171 | // this is useful when the resize has not been taken into account (e.g. content too big) 172 | // and the mouse is not on the edge of the element anymore 173 | const state = this.store.getState(); 174 | const selectable = domMetrics.getSelectable(this.store, state.mouse.mouseData.target); 175 | this.store.dispatch(mouseState.setCursorData(domMetrics.getCursorData(state.mouse.mouseData.mouseX, state.mouse.mouseData.mouseY, state.mouse.scrollData, selectable))); 176 | 177 | // update the real metrics after drop 178 | setTimeout(() => { 179 | // change UI state while selectables metrics are simply updated 180 | this.store.dispatch(setRefreshing(true)); 181 | 182 | const updatedState = this.store.getState().selectables 183 | .map(selectable => { 184 | return { 185 | ...selectable, 186 | metrics: domMetrics.getMetrics(selectable.el), 187 | } 188 | }); 189 | this.store.dispatch(selectableState.updateSelectables(updatedState)); 190 | 191 | // change UI state while selectables metrics are simply updated 192 | this.store.dispatch(setRefreshing(false)); 193 | 194 | // notify the app 195 | if(this.hooks.onResizeEnd) this.hooks.onResizeEnd(this.selection); 196 | }, 0) 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/ts/observers/DomObserver.ts: -------------------------------------------------------------------------------- 1 | import {StageStore} from '../flux/StageStore'; 2 | import { SelectableState } from '../Types'; 3 | import * as types from '../Types'; 4 | 5 | declare class ResizeObserver { 6 | constructor(onChanged: any); 7 | observe: any; 8 | disconnect: any; 9 | takeRecords: any; 10 | }; 11 | 12 | // dom observers instances, exposed for unit tests 13 | export const domObservers = new Map(); 14 | export function initDomObservers(elements, onChanged) { 15 | resetDomObservers(); 16 | elements.forEach((el) => addDomObserver(el, onChanged)); 17 | }; 18 | 19 | export function resetDomObservers() { 20 | Array.from(domObservers.keys()) 21 | .forEach((el) => removeDomObserver(el)); 22 | }; 23 | 24 | export function addDomObserver(el: HTMLElement, onChanged: (entries: Array) => void) { 25 | if (typeof ResizeObserver === 'undefined') { 26 | throw new Error('ResizeObserver is not supported by your browser. The drag and drop features will not work properly'); 27 | } 28 | if (domObservers.has(el)) { 29 | removeDomObserver(el); 30 | } 31 | const resizeObserver = new ResizeObserver(onChanged); 32 | resizeObserver.observe(el, {}); 33 | 34 | const mutationObserver = new MutationObserver(onChanged); 35 | // FIXME: mutation observer is disabled => remove useless mutationObserver 36 | // mutationObserver.observe(el, { 37 | // subtree: true, 38 | // childList: true, 39 | // attributes: true, 40 | // attributeOldValue: false, 41 | // characterData: true, 42 | // characterDataOldValue: false, 43 | // }); 44 | 45 | domObservers.set(el, {mutationObserver, resizeObserver}); 46 | }; 47 | 48 | export function removeDomObserver(el: HTMLElement) { 49 | if (domObservers.has(el)) { 50 | const {mutationObserver, resizeObserver} = domObservers.get(el); 51 | resizeObserver.disconnect(); 52 | mutationObserver.disconnect(); 53 | mutationObserver.takeRecords(); 54 | domObservers.delete(el); 55 | } else { 56 | throw new Error('DOM observer not found for this DOM element'); 57 | } 58 | }; 59 | 60 | /** 61 | * @class This class listens to the store 62 | * and observe the dom elements in order to keep the metrics in sync 63 | * using MutationObserver and ResizeObserver APIs of the browser 64 | */ 65 | export class DomObserver { 66 | constructor(store: StageStore, private cbk: (state: SelectableState, entries: Array) => void) { 67 | this.unsubscribeAll.push( 68 | store.subscribe( 69 | (state: Array, prevState: Array) => this.onStateChanged(state, prevState), 70 | (state:types.State) => state.selectables 71 | ), 72 | store.subscribe( 73 | (state: types.UiState, prevState: types.UiState) => this.onUiChanged(state, prevState), 74 | (state:types.State) => state.ui 75 | ), 76 | ); 77 | } 78 | 79 | private isRefreshing: boolean = false; 80 | private state: Array = [] 81 | private prevState: Array = [] 82 | onUiChanged(state: types.UiState, prevState: types.UiState) { 83 | this.isRefreshing = state.refreshing; 84 | // // update after refresh (bug because isRefreshing is turned on and off many times) 85 | // if (state.refreshing !== prevState.refreshing && state.refreshing === false) { 86 | // this.onStateChanged() 87 | // } 88 | } 89 | 90 | private unsubscribeAll: Array<() => void> = []; 91 | cleanup() { 92 | this.unsubscribeAll.forEach(u => u()); 93 | resetDomObservers(); 94 | } 95 | 96 | onRemoved(state: SelectableState) { 97 | removeDomObserver(state.el); 98 | } 99 | 100 | onAdded(state: SelectableState) { 101 | addDomObserver(state.el, (entries) => this.onChanged(state, entries)); 102 | } 103 | 104 | onChanged(state: SelectableState, entries) { 105 | this.cbk(state, entries); 106 | } 107 | 108 | /** 109 | * handle state changes, detect changes of scroll or metrics or selection 110 | * @param {State} state 111 | * @param {State} prevState the old state obj 112 | */ 113 | onStateChanged(state: Array = this.state, prevState: Array = this.prevState) { 114 | this.state = state 115 | if(!this.isRefreshing) { 116 | this.prevState = prevState 117 | const added = state.filter(s => !prevState.find(s2 => s2.el === s.el)); 118 | added.forEach((state) => this.onAdded(state)) 119 | 120 | const removed = prevState.filter(s => !state.find(s2 => s2.el === s.el)); 121 | removed.forEach((state) => this.onRemoved(state)) 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/ts/observers/MouseObserver.ts: -------------------------------------------------------------------------------- 1 | import * as DomMetrics from '../utils/DomMetrics'; 2 | import * as types from '../Types'; 3 | import {StageStore} from '../flux/StageStore'; 4 | 5 | /** 6 | * @class This class listens to the store 7 | * and apply the state changes to the view 8 | */ 9 | export class MouseObserver { 10 | constructor(private stageDocument: HTMLDocument, private overlayDocument: HTMLDocument, store: StageStore, private hooks: types.Hooks) { 11 | this.unsubscribeAll.push(store.subscribe( 12 | (state: types.MouseState, prevState: types.MouseState) => this.onStateChanged(state, prevState), 13 | (state:types.State) => state.mouse 14 | )); 15 | } 16 | 17 | private unsubscribeAll: Array<() => void> = []; 18 | cleanup() { 19 | this.unsubscribeAll.forEach(u => u()); 20 | } 21 | 22 | /** 23 | * handle state changes, detect changes of scroll or metrics or selection 24 | * @param {State} state 25 | * @param {State} prevState the old state obj 26 | */ 27 | onStateChanged(state: types.MouseState, prevState: types.MouseState) { 28 | if(state.scrollData.x !== prevState.scrollData.x || state.scrollData.y !== prevState.scrollData.y) { 29 | DomMetrics.setScroll(this.stageDocument, state.scrollData); 30 | } 31 | // this is now in Ui.ts 32 | // if(state.cursorData.cursorType !== prevState.cursorData.cursorType) { 33 | // this.doc.body.style.cursor = state.cursorData.cursorType; 34 | // } 35 | } 36 | } -------------------------------------------------------------------------------- /src/ts/observers/SelectablesObserver.ts: -------------------------------------------------------------------------------- 1 | import * as DomMetrics from '../utils/DomMetrics'; 2 | import {StageStore} from '../flux/StageStore'; 3 | import { SelectableState } from '../Types'; 4 | import * as types from '../Types'; 5 | 6 | /** 7 | * @class This class listens to the store 8 | * and apply the state changes to the DOM elements 9 | */ 10 | export class SelectablesObserver { 11 | constructor(private stageDocument: HTMLDocument, private overlayDocument: HTMLDocument, private store: StageStore, private hooks: types.Hooks) { 12 | this.unsubscribeAll.push( 13 | store.subscribe( 14 | (state: Array, prevState: Array) => this.onStateChanged(state, prevState), 15 | (state:types.State) => state.selectables 16 | ), 17 | store.subscribe( 18 | (state: types.UiState, prevState: types.UiState) => this.onUiChanged(state, prevState), 19 | (state:types.State) => state.ui 20 | ), 21 | ); 22 | } 23 | 24 | private isRefreshing: boolean = false; 25 | private state: Array = [] 26 | private prevState: Array = [] 27 | onUiChanged(state: types.UiState, prevState: types.UiState) { 28 | this.isRefreshing = state.refreshing; 29 | // // update after refresh (bug because isRefreshing is turned on and off many times) 30 | // if (state.refreshing !== prevState.refreshing && state.refreshing === false) { 31 | // this.onStateChanged(this.state, this.prevState) 32 | // } 33 | } 34 | 35 | private unsubscribeAll: Array<() => void> = []; 36 | cleanup() { 37 | this.unsubscribeAll.forEach(u => u()); 38 | } 39 | 40 | /** 41 | * handle state changes, detect changes of scroll or metrics or selection 42 | * @param {State} state 43 | * @param {State} prevState the old state obj 44 | */ 45 | onStateChanged(state: Array = this.state, prevState: Array = this.prevState) { 46 | this.state = state 47 | if(!this.isRefreshing) { 48 | this.prevState = prevState 49 | // select selectables which have changed 50 | const filterBy = (propName, selectable) => { 51 | const oldSelectable = prevState.find(old => selectable.el === old.el); 52 | // FIXME: use JSON.stringify to compare? 53 | return !oldSelectable || JSON.stringify(oldSelectable[propName]) !== JSON.stringify(selectable[propName]); 54 | // return !oldSelectable || oldSelectable[propName] !== selectable[propName]; 55 | } 56 | 57 | const removed = prevState.filter(s => !state.find(s2 => s2.el === s.el)); 58 | const metrics = state.filter(selectable => filterBy('metrics', selectable)); 59 | if(removed.length + metrics.length > 0) this.onMetrics(metrics, removed); 60 | 61 | const selection = state.filter(selectable => filterBy('selected', selectable)); 62 | if(selection.length > 0) this.onSelection(selection); 63 | 64 | // const draggable = state.filter(selectable => filterBy('draggable', selectable)); 65 | // if(draggable.length > 0) this.onDraggable(draggable); 66 | 67 | // const resizeable = state.filter(selectable => filterBy('resizeable', selectable)); 68 | // if(resizeable.length > 0) this.onResizeable(resizeable); 69 | 70 | // const isDropZone = state.filter(selectable => filterBy('isDropZone', selectable)); 71 | // if(isDropZone.length > 0) this.onDropZone(isDropZone); 72 | 73 | const translation = state.filter(selectable => filterBy('translation', selectable)); 74 | if(translation.length > 0) this.onTranslation(translation); 75 | } 76 | } 77 | // update elements position and size 78 | onMetrics(selectables: Array, removed: Array) { 79 | selectables.forEach(selectable => { 80 | // while being dragged, elements are out of the flow, do not apply styles 81 | if(!selectable.preventMetrics) { 82 | DomMetrics.setMetrics(selectable.el, selectable.metrics, selectable.useMinHeight); 83 | } 84 | }); 85 | // notify the app 86 | if(this.hooks.onChange) this.hooks.onChange(selectables.concat(removed)); 87 | } 88 | onSelection(selectables: Array) { 89 | // notify the app 90 | if(this.hooks.onSelect) this.hooks.onSelect(selectables); 91 | } 92 | // onDraggable(selectables: Array) {} 93 | // onResizeable(selectables: Array) {} 94 | // onDropZone(selectables: Array) {} 95 | onTranslation(selectables: Array) { 96 | selectables.forEach(selectable => { 97 | if(!!selectable.translation) { 98 | selectable.el.style.transform = `translate(${selectable.translation.x}px, ${selectable.translation.y}px)`; 99 | selectable.el.style.zIndex = '99999999'; 100 | if(selectable.metrics.position === 'static') { 101 | selectable.el.style.top = '0'; 102 | selectable.el.style.left = '0'; 103 | selectable.el.style.position = 'relative'; 104 | } 105 | } 106 | else { 107 | selectable.el.style.transform = ''; 108 | selectable.el.style.zIndex = ''; 109 | selectable.el.style.position = ''; 110 | } 111 | }); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/ts/observers/UiObserver.ts: -------------------------------------------------------------------------------- 1 | import * as types from '../Types'; 2 | import {ResizeHandler} from '../handlers/ResizeHandler'; 3 | import {DrawHandler} from '../handlers/DrawHandler'; 4 | import {MoveHandler} from '../handlers/MoveHandler'; 5 | import {StageStore} from '../flux/StageStore'; 6 | import { MouseHandlerBase } from '../handlers/MouseHandlerBase'; 7 | 8 | /** 9 | * @class This class listens to the store 10 | * and apply the state changes to the DOM elements 11 | */ 12 | export class UiObserver { 13 | public handler: MouseHandlerBase; 14 | constructor(private stageDocument: HTMLDocument, private overlayDocument: HTMLDocument, private store: StageStore, private hooks: types.Hooks) { 15 | this.handler = null; 16 | this.unsubscribeAll.push(store.subscribe( 17 | (state: types.UiState, prevState: types.UiState) => this.onUiStateChanged(state, prevState), 18 | (state:types.State) => state.ui 19 | )); 20 | } 21 | 22 | private unsubscribeAll: Array<() => void> = []; 23 | cleanup() { 24 | this.unsubscribeAll.forEach(u => u()); 25 | } 26 | 27 | /** 28 | * handle state changes, detect changes of scroll or metrics or selection 29 | * @param {State} state 30 | * @param {State} prevState the old state obj 31 | */ 32 | onUiStateChanged(state: types.UiState, prevState: types.UiState) { 33 | if(prevState.mode !== state.mode) { 34 | if(this.handler) { 35 | this.handler.release(); 36 | this.handler = null; 37 | } 38 | // add css class and style 39 | this.overlayDocument.body.classList.remove(...[ 40 | state.mode !== types.UiMode.DRAG ? 'dragging-mode' : 'not-dragging-mode', 41 | state.mode !== types.UiMode.RESIZE ? 'resizing-mode' : 'not-resizing-mode', 42 | state.mode !== types.UiMode.DRAW ? 'drawing-mode' : 'not-drawing-mode', 43 | ]); 44 | this.overlayDocument.body.classList.add(...[ 45 | state.mode === types.UiMode.DRAG ? 'dragging-mode' : 'not-dragging-mode', 46 | state.mode === types.UiMode.RESIZE ? 'resizing-mode' : 'not-resizing-mode', 47 | state.mode === types.UiMode.DRAW ? 'drawing-mode' : 'not-drawing-mode', 48 | ]); 49 | // manage handlers 50 | switch(state.mode){ 51 | case types.UiMode.NONE: 52 | break; 53 | case types.UiMode.DRAG: 54 | this.handler = new MoveHandler(this.stageDocument, this.overlayDocument, this.store, this.hooks); 55 | break; 56 | case types.UiMode.RESIZE: 57 | this.handler = new ResizeHandler(this.stageDocument, this.overlayDocument, this.store, this.hooks); 58 | break; 59 | case types.UiMode.DRAW: 60 | this.handler = new DrawHandler(this.stageDocument, this.overlayDocument, this.store, this.hooks); 61 | break; 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/ts/utils/Events.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * add an event listener and returns an a method to call to remove the listener 4 | */ 5 | export function addEvent(obj: EventTarget, type: string, listener: EventListener, options: any = {}): () => void { 6 | obj.addEventListener(type, listener, options); 7 | return () => obj.removeEventListener(type, listener, options); 8 | } 9 | -------------------------------------------------------------------------------- /src/ts/utils/Polyfill.ts: -------------------------------------------------------------------------------- 1 | export function patchWindow(win: Window) { 2 | if(!win.document.elementsFromPoint) { 3 | // console.warn('Polyfill: polyfill document.elementsFromPoint', win); 4 | win.document.elementsFromPoint = function(x, y) { 5 | // FIXME: the order is important and the 1st element should be the one on top 6 | return Array.from(win.document.body.querySelectorAll('*')).filter(function(el) { 7 | var pos = el.getBoundingClientRect(); 8 | return pos.left <= x && x <= pos.right && pos.top <= y && y <= pos.bottom; 9 | }); 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /tests/__snapshots__/ui.test.electron.ts.snap: -------------------------------------------------------------------------------- 1 | // Jest Snapshot v1, https://goo.gl/fbAQLP 2 | 3 | exports[`Ui should draw the UI: snapshot-draw-ui0 1`] = ` 4 |
8 | 9 | 10 |
13 | 14 | 15 |
18 | 19 | 20 |
23 | 24 | 25 |
28 | 29 | 30 |
31 | `; 32 | 33 | exports[`Ui should draw the UI: snapshot-draw-ui0 2`] = ` 34 |
38 | 39 | 40 |
43 | 44 | 45 |
48 | 49 | 50 |
53 | 54 | 55 |
58 | 59 | 60 |
61 | `; 62 | 63 | exports[`Ui should draw the UI: snapshot-draw-ui1 1`] = ` 64 |
68 | 69 | 70 |
73 | 74 | 75 |
78 | 79 | 80 |
83 | 84 | 85 |
88 | 89 | 90 |
91 | `; 92 | 93 | exports[`Ui should draw the UI: snapshot-draw-ui1 2`] = ` 94 |
98 | 99 | 100 |
103 | 104 | 105 |
108 | 109 | 110 |
113 | 114 | 115 |
118 | 119 | 120 |
121 | `; 122 | 123 | exports[`Ui should draw the UI: snapshot-draw-ui2 1`] = ` 124 |
128 | 129 | 130 |
133 | 134 | 135 |
138 | 139 | 140 |
143 | 144 | 145 |
148 | 149 | 150 |
151 | `; 152 | -------------------------------------------------------------------------------- /tests/flux/StageStoreMock.ts: -------------------------------------------------------------------------------- 1 | import {StageStore} from '../../src/ts/flux/StageStore'; 2 | import * as types from '../../src/ts/Types'; 3 | 4 | export const hooks = { 5 | isSelectable: (el => el.classList.contains('i-am-selectable')), 6 | isDraggable: (el => true), 7 | isDropZone: (el => true), 8 | isResizeable: (el => true), 9 | useMinHeight: (el => true), 10 | canDrop: (el => true), 11 | onEdit: (() => undefined), 12 | }; 13 | 14 | export class StageStoreMock extends StageStore { 15 | static elem1; 16 | static elem2; 17 | 18 | preventDispatch = false; 19 | 20 | cbks = []; 21 | subscribe(onChange: (state:SubState, prevState:SubState) => void, select=(state:types.State):SubState => (state as any)) { 22 | this.cbks.push((state:types.State, prevState:types.State) => onChange(select(state), select(prevState))); 23 | return () => { 24 | // console.log('unsubscribe called') 25 | }; 26 | } 27 | 28 | dispatch(action: any, cbk: () => void = null, idx: number = 0): any { 29 | // console.log('Dispatch', action, 'to', idx+1, '/', this.cbks.length, '(', this.preventDispatch, ')'); 30 | // console.log('===============================================') 31 | if(!this.preventDispatch && this.cbks[idx]) this.cbks[idx](this.getState(), this.initialState); 32 | if(cbk) cbk(); 33 | return null; 34 | } 35 | 36 | selectableElem1: types.SelectableState = { 37 | id: 'elem1ID', 38 | el: StageStoreMock.elem1, 39 | selectable: true, 40 | selected: false, 41 | hovered: false, 42 | draggable: true, 43 | resizeable: { 44 | top: true, 45 | left: true, 46 | bottom: true, 47 | right: true, 48 | }, 49 | isDropZone: true, 50 | useMinHeight: true, 51 | metrics: { 52 | position: 'absolute', 53 | proportions: 1, 54 | margin: {top: 0, left: 0, bottom: 0, right: 0 }, 55 | padding: {top: 0, left: 0, bottom: 0, right: 0 }, 56 | border: {top: 0, left: 0, bottom: 0, right: 0 }, 57 | computedStyleRect: {top: 100, left: 100, bottom: 200, right: 200, width: 100, height: 100 }, 58 | clientRect: {top: 100, left: 100, bottom: 200, right: 200, width: 100, height: 100 }, 59 | }, 60 | }; 61 | selectableElem2: types.SelectableState = { 62 | id: 'elem2ID', 63 | el: StageStoreMock.elem2, 64 | selectable: true, 65 | selected: false, 66 | hovered: false, 67 | draggable: true, 68 | resizeable: { 69 | top: true, 70 | left: true, 71 | bottom: true, 72 | right: true, 73 | }, 74 | isDropZone: true, 75 | useMinHeight: true, 76 | metrics: { 77 | position: 'static', 78 | proportions: 1, 79 | margin: {top: 0, left: 0, bottom: 0, right: 0 }, 80 | padding: {top: 0, left: 0, bottom: 0, right: 0 }, 81 | border: {top: 0, left: 0, bottom: 0, right: 0 }, 82 | computedStyleRect: {top: 10, left: 10, bottom: 20, right: 20, width: 10, height: 10 }, 83 | clientRect: {top: 10, left: 10, bottom: 20, right: 20, width: 10, height: 10 }, 84 | }, 85 | }; 86 | static additionalSelectables: Array = []; 87 | uiState: types.UiState = { 88 | mode: types.UiMode.NONE, 89 | refreshing: false, 90 | catchingEvents: true, 91 | sticky: types.EMPTY_STICKY_BOX(), 92 | enableSticky: false, 93 | }; 94 | mouseState: types.MouseState = { 95 | scrollData: { x: 0, y: 0}, 96 | cursorData: {x: '', y: '', cursorType: ''}, 97 | mouseData: { 98 | movementX: 0, 99 | movementY: 0, 100 | mouseX: 0, 101 | mouseY: 0, 102 | shiftKey: false, 103 | target: null, 104 | hovered: [], 105 | }, 106 | }; 107 | initialState = { 108 | selectables: [this.selectableElem1, this.selectableElem2], 109 | ui: this.uiState, 110 | mouse: this.mouseState, 111 | }; 112 | state = { 113 | selectables: [this.selectableElem1, this.selectableElem2], 114 | ui: this.uiState, 115 | mouse: this.mouseState, 116 | }; 117 | getState(): types.State { 118 | return { 119 | selectables: [...this.state.selectables, ...StageStoreMock.additionalSelectables], 120 | ui: { 121 | ...this.state.ui, 122 | }, 123 | mouse: { 124 | ...this.state.mouse, 125 | }, 126 | }; 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /tests/flux/stagestore.test.jsdom.ts: -------------------------------------------------------------------------------- 1 | import {StageStore} from '../../src/ts/flux/StageStore'; 2 | import { createSelectable } from '../../src/ts/flux/SelectableState'; 3 | 4 | describe('StageStore', function() { 5 | let instance: StageStore; 6 | beforeAll(() => { 7 | document.body.innerHTML = ` 8 |
9 |
10 |
11 | `; 12 | instance = new StageStore(); 13 | Array.from(document.querySelectorAll('.i-am-selectable')) 14 | .forEach((el: HTMLElement) => { 15 | instance.dispatch( 16 | createSelectable({ 17 | el, 18 | id: 'elemXID', 19 | selectable: true, 20 | selected: false, 21 | hovered: false, 22 | draggable: true, 23 | resizeable: { 24 | top: true, 25 | left: true, 26 | bottom: true, 27 | right: true, 28 | }, 29 | isDropZone: true, 30 | useMinHeight: true, 31 | metrics: { 32 | position: 'absolute', 33 | proportions: 1, 34 | margin: {top: 0, left: 0, bottom: 0, right: 0 }, 35 | padding: {top: 0, left: 0, bottom: 0, right: 0 }, 36 | border: {top: 0, left: 0, bottom: 0, right: 0 }, 37 | computedStyleRect: {top: 100, left: 100, bottom: 200, right: 200, width: 100, height: 100 }, 38 | clientRect: {top: 100, left: 100, bottom: 200, right: 200, width: 100, height: 100 }, 39 | } 40 | }) 41 | ); 42 | }); 43 | }); 44 | 45 | it('createStore to create a store with the DOM as selectables', () => { 46 | expect(instance).not.toBeNull(); 47 | expect(instance.getState()).not.toBeNull(); 48 | expect(instance.getState().selectables).not.toBeNull(); 49 | expect(instance.getState().selectables.length).toBe(2); 50 | expect(instance.getState().selectables[0].draggable).toBe(true); 51 | }); 52 | it('dispatch and subscribe', (done) => { 53 | let changeCount = 0; 54 | const unsubscribe = instance.subscribe((current, prev) => { 55 | changeCount++; 56 | }); 57 | let mouseChangeCount = 0; 58 | const mouseUnsubscribe = instance.subscribe((current, prev) => { 59 | mouseChangeCount++; 60 | }, state => state.mouse); 61 | let selectablesChangeCount = 0; 62 | const selectablesUnsubscribe = instance.subscribe((current, prev) => { 63 | selectablesChangeCount++; 64 | }, state => state.selectables); 65 | let uiChangeCount = 0; 66 | const uiUnsubscribe = instance.subscribe((current, prev) => { 67 | uiChangeCount++; 68 | }, state => state.ui); 69 | 70 | instance.dispatch({ 71 | type: 'UI_SET_MODE', 72 | mode: instance.getState().ui.mode, 73 | }); 74 | instance.dispatch({ 75 | type: 'SELECTABLE_UPDATE', 76 | selectables: instance.getState().selectables 77 | }); 78 | setTimeout(() => { 79 | expect(changeCount).toBe(2); 80 | expect(mouseChangeCount).toBe(0); 81 | expect(selectablesChangeCount).toBe(1); 82 | expect(uiChangeCount).toBe(1); 83 | done(); 84 | }, 0); 85 | }); 86 | }); 87 | -------------------------------------------------------------------------------- /tests/handlers/drawhandler.test.jsdom.ts: -------------------------------------------------------------------------------- 1 | import { DrawHandler } from '../../src/ts/handlers/DrawHandler'; 2 | import { hooks, StageStoreMock } from '../flux/StageStoreMock'; 3 | 4 | 5 | 6 | describe('DrawHandler', function() { 7 | 8 | var elem3; 9 | var stageStoreMock: StageStoreMock; 10 | var handler: DrawHandler; 11 | 12 | beforeEach(function () { 13 | document.body.innerHTML = ` 14 |
15 |
16 |
17 | `; 18 | 19 | StageStoreMock.elem1 = document.querySelector('#elem1'); 20 | StageStoreMock.elem2 = document.querySelector('#elem2'); 21 | elem3 = document.querySelector('#elem3'); 22 | 23 | stageStoreMock = new StageStoreMock(); 24 | stageStoreMock.preventDispatch = true; 25 | jest.spyOn(stageStoreMock, 'subscribe'); 26 | jest.spyOn(stageStoreMock, 'dispatch'); 27 | jest.spyOn(stageStoreMock, 'getState'); 28 | 29 | handler = new DrawHandler(document, document, stageStoreMock, hooks); 30 | jest.spyOn(handler, 'update'); 31 | jest.spyOn(handler, 'release'); 32 | jest.spyOn(handler, 'moveRegion'); 33 | }); 34 | 35 | it('should select 1, 2 and 3 elements in the dom', function() { 36 | var mouseData = { 37 | mouseX: 15, 38 | movementX: 1, 39 | mouseY: 15, 40 | movementY: 1, 41 | shiftKey: false, 42 | target: document.body, 43 | hovered: [], 44 | }; 45 | stageStoreMock.mouseState.mouseData = mouseData; 46 | handler.update(mouseData); 47 | expect(stageStoreMock.dispatch).toBeCalledTimes(1); 48 | var calls = stageStoreMock.dispatch['mock'].calls; 49 | var lastAction = calls[calls.length - 1][0]; 50 | expect(lastAction.type).toBe('SELECTION_ADD'); 51 | expect(lastAction.selectable.el).toBe(StageStoreMock.elem2); 52 | stageStoreMock.selectableElem2.selected = true; 53 | 54 | var mouseData = { 55 | mouseX: 115, 56 | movementX: 100, 57 | mouseY: 115, 58 | movementY: 100, 59 | shiftKey: false, 60 | target: document.body, 61 | hovered: [], 62 | }; 63 | stageStoreMock.mouseState.mouseData = mouseData; 64 | handler.update(mouseData); 65 | expect(stageStoreMock.dispatch).toBeCalledTimes(2); 66 | var calls = stageStoreMock.dispatch['mock'].calls; 67 | var lastAction = calls[calls.length - 1][0]; 68 | expect(lastAction.type).toBe('SELECTION_ADD'); 69 | expect(lastAction.selectable.el).toBe(StageStoreMock.elem1); 70 | 71 | stageStoreMock.selectableElem1.selected = true; 72 | 73 | handler.release(); 74 | expect(handler.regionMarker.parentNode).toBeNull(); 75 | expect(handler.selection.length).toBe(0); 76 | }); 77 | 78 | it('should un-select 1, 2 and 3 elements in the dom', function() { 79 | var mouseData = { 80 | mouseX: 115, 81 | movementX: 100, 82 | mouseY: 115, 83 | movementY: 100, 84 | shiftKey: false, 85 | target: document.body, 86 | hovered: [], 87 | }; 88 | stageStoreMock.mouseState.mouseData = mouseData; 89 | handler.update(mouseData); 90 | expect(handler.selection.length).toBe(2); 91 | var calls = stageStoreMock.dispatch['mock'].calls; 92 | var lastAction = calls[calls.length - 1][0]; 93 | expect(lastAction.type).toBe('SELECTION_ADD'); 94 | 95 | var mouseData = { 96 | mouseX: 15, 97 | movementX: -100, 98 | mouseY: 15, 99 | movementY: -100, 100 | shiftKey: false, 101 | target: document.body, 102 | hovered: [], 103 | }; 104 | stageStoreMock.mouseState.mouseData = mouseData; 105 | handler.update(mouseData); 106 | expect(handler.selection.length).toBe(1); 107 | var calls = stageStoreMock.dispatch['mock'].calls; 108 | var lastAction = calls[calls.length - 1][0]; 109 | expect(lastAction.type).toBe('SELECTION_REMOVE'); 110 | expect(lastAction.selectable.el).toBe(StageStoreMock.elem1); 111 | 112 | handler.release(); 113 | }); 114 | }); 115 | -------------------------------------------------------------------------------- /tests/handlers/resizehandler.test.electron.ts: -------------------------------------------------------------------------------- 1 | import {ResizeHandler} from '../../src/ts/handlers/ResizeHandler'; 2 | import { hooks, StageStoreMock } from '../flux/StageStoreMock'; 3 | import { MouseState } from '../../src/ts/Types'; 4 | 5 | describe('ResizeHandler', function() { 6 | 7 | var elem3; 8 | var stageStoreMock: StageStoreMock; 9 | function initHandler(mouseState: MouseState) { 10 | stageStoreMock.mouseState.mouseData = mouseState.mouseData; 11 | stageStoreMock.mouseState.cursorData = mouseState.cursorData; 12 | stageStoreMock.mouseState.scrollData = mouseState.scrollData; 13 | var handler = new ResizeHandler(document, document, stageStoreMock, hooks); 14 | jest.spyOn(handler, 'update'); 15 | jest.spyOn(handler, 'release'); 16 | return handler; 17 | } 18 | 19 | beforeEach(function () { 20 | document.body.innerHTML = ` 21 | 41 |
42 |
43 |
44 |
45 |
46 | `; 47 | 48 | StageStoreMock.elem1 = document.querySelector('#elem1'); 49 | StageStoreMock.elem2 = document.querySelector('#elem2'); 50 | elem3 = document.querySelector('#elem3'); 51 | 52 | stageStoreMock = new StageStoreMock(); 53 | stageStoreMock.preventDispatch = true; 54 | jest.spyOn(stageStoreMock, 'subscribe'); 55 | jest.spyOn(stageStoreMock, 'dispatch'); 56 | jest.spyOn(stageStoreMock, 'getState'); 57 | }); 58 | 59 | it('should resize 1 element from the bottom right corner', function() { 60 | stageStoreMock.selectableElem1.selected = false; 61 | stageStoreMock.selectableElem2.selected = true; 62 | 63 | var mouseState = { 64 | scrollData: {x: 0, y: 0}, 65 | cursorData: {x: 'right', y: 'bottom', cursorType: ''}, 66 | mouseData: { 67 | mouseX: 30, 68 | movementX: 10, 69 | mouseY: 30, 70 | movementY: 10, 71 | shiftKey: false, 72 | target: StageStoreMock.elem2, 73 | hovered: [], 74 | } 75 | }; 76 | var handler = initHandler(mouseState); 77 | expect(handler.selection.length).toBe(1); 78 | expect(handler.selection[0].metrics.clientRect.width).toBe(10); 79 | 80 | var mouseState = { 81 | scrollData: {x: 0, y: 0}, 82 | cursorData: {x: 'right', y: 'bottom', cursorType: ''}, 83 | mouseData: { 84 | mouseX: 30, 85 | movementX: 10, 86 | mouseY: 30, 87 | movementY: 10, 88 | shiftKey: false, 89 | target: StageStoreMock.elem2, 90 | hovered: [], 91 | } 92 | }; 93 | var handler = initHandler(mouseState); 94 | handler.update(mouseState.mouseData); 95 | expect(stageStoreMock.dispatch).toBeCalledTimes(1); 96 | var calls = stageStoreMock.dispatch['mock'].calls; 97 | var lastAction = calls[calls.length - 1][0]; 98 | expect(lastAction.type).toBe('SELECTABLE_UPDATE'); 99 | expect(lastAction.selectables.length).toBe(1); 100 | expect(lastAction.selectables[0].el).toBe(StageStoreMock.elem2); 101 | expect(lastAction.selectables[0].metrics.clientRect.height).toBe(20); 102 | expect(lastAction.selectables[0].metrics.clientRect.width).toBe(20); 103 | }); 104 | 105 | it('should resize 2 elements from the bottom right corner', function() { 106 | stageStoreMock.selectableElem1.selected = true; 107 | stageStoreMock.selectableElem2.selected = true; 108 | 109 | var mouseState = { 110 | scrollData: {x: 0, y: 0}, 111 | cursorData: {x: 'right', y: 'bottom', cursorType: ''}, 112 | mouseData: { 113 | mouseX: 120, 114 | movementX: 100, 115 | mouseY: 120, 116 | movementY: 100, 117 | shiftKey: false, 118 | target: document.body, 119 | hovered: [], 120 | } 121 | }; 122 | var handler = initHandler(mouseState); 123 | expect(handler.selection.length).toBe(2); 124 | 125 | var mouseState = { 126 | scrollData: {x: 0, y: 0}, 127 | cursorData: {x: 'right', y: 'bottom', cursorType: ''}, 128 | mouseData: { 129 | mouseX: 120, 130 | movementX: 100, 131 | mouseY: 120, 132 | movementY: 100, 133 | shiftKey: false, 134 | target: document.body, 135 | hovered: [], 136 | } 137 | }; 138 | var handler = initHandler(mouseState); 139 | handler.update(mouseState.mouseData); 140 | expect(stageStoreMock.dispatch).toBeCalledTimes(1); 141 | var calls = stageStoreMock.dispatch['mock'].calls; 142 | var lastAction = calls[calls.length - 1][0]; 143 | expect(lastAction.type).toBe('SELECTABLE_UPDATE'); 144 | expect(lastAction.selectables.length).toBe(2); 145 | expect(lastAction.selectables[1].el).toBe(StageStoreMock.elem2); 146 | expect(lastAction.selectables[1].metrics.clientRect.width).toBe(110); 147 | expect(lastAction.selectables[1].metrics.clientRect.height).toBe(110); 148 | expect(lastAction.selectables[0].el).toBe(StageStoreMock.elem1); 149 | expect(lastAction.selectables[0].metrics.clientRect.width).toBe(200); 150 | expect(lastAction.selectables[0].metrics.clientRect.height).toBe(200); 151 | }); 152 | 153 | it('should resize 1 element from the top left corner', function() { 154 | stageStoreMock.selectableElem1.selected = false; 155 | stageStoreMock.selectableElem2.selected = true; 156 | 157 | var mouseState = { 158 | scrollData: {x: 0, y: 0}, 159 | cursorData: {x: 'left', y: 'top', cursorType: ''}, 160 | mouseData: { 161 | mouseX: 0, 162 | movementX: -10, 163 | mouseY: 0, 164 | movementY: -10, 165 | shiftKey: false, 166 | target: document.body, 167 | hovered: [], 168 | } 169 | }; 170 | var handler = initHandler(mouseState); 171 | expect(handler.selection.length).toBe(1); 172 | 173 | var mouseState = { 174 | scrollData: {x: 0, y: 0}, 175 | cursorData: {x: 'left', y: 'top', cursorType: ''}, 176 | mouseData: { 177 | mouseX: 0, 178 | movementX: -10, 179 | mouseY: 0, 180 | movementY: -10, 181 | shiftKey: false, 182 | target: document.body, 183 | hovered: [], 184 | } 185 | }; 186 | var handler = initHandler(mouseState); 187 | handler.update(mouseState.mouseData); 188 | expect(stageStoreMock.dispatch).toBeCalledTimes(1); 189 | var calls = stageStoreMock.dispatch['mock'].calls; 190 | var lastAction = calls[calls.length - 1][0]; 191 | expect(lastAction.type).toBe('SELECTABLE_UPDATE'); 192 | expect(lastAction.selectables.length).toBe(1); 193 | expect(lastAction.selectables[0].el).toBe(StageStoreMock.elem2); 194 | expect(lastAction.selectables[0].metrics.clientRect.width).toBe(20); 195 | expect(lastAction.selectables[0].metrics.clientRect.height).toBe(20); 196 | expect(lastAction.selectables[0].metrics.clientRect.left).toBe(0); 197 | expect(lastAction.selectables[0].metrics.clientRect.top).toBe(0); 198 | }); 199 | 200 | it('should resize 1 element and keep proporitions', function() { 201 | stageStoreMock.selectableElem1.selected = false; 202 | stageStoreMock.selectableElem2.selected = true; 203 | 204 | var mouseState = { 205 | scrollData: {x: 0, y: 0}, 206 | cursorData: {x: 'right', y: 'bottom', cursorType: ''}, 207 | mouseData: { 208 | mouseX: 120, 209 | movementX: 100, 210 | mouseY: 20, 211 | movementY: 0, 212 | shiftKey: true, 213 | target: document.body, 214 | hovered: [], 215 | } 216 | }; 217 | var handler = initHandler(mouseState); 218 | expect(handler.selection.length).toBe(1); 219 | 220 | var mouseState = { 221 | scrollData: {x: 0, y: 0}, 222 | cursorData: {x: 'right', y: 'bottom', cursorType: ''}, 223 | mouseData: { 224 | mouseX: 120, 225 | movementX: 100, 226 | mouseY: 20, 227 | movementY: 0, 228 | shiftKey: true, 229 | target: document.body, 230 | hovered: [], 231 | } 232 | }; 233 | var handler = initHandler(mouseState); 234 | handler.update(mouseState.mouseData); 235 | expect(stageStoreMock.dispatch).toBeCalledTimes(1); 236 | var calls = stageStoreMock.dispatch['mock'].calls; 237 | var lastAction = calls[calls.length - 1][0]; 238 | expect(lastAction.type).toBe('SELECTABLE_UPDATE'); 239 | expect(lastAction.selectables.length).toBe(1); 240 | expect(lastAction.selectables[0].el).toBe(StageStoreMock.elem2); 241 | expect(lastAction.selectables[0].metrics.clientRect.width).toBe(110); 242 | expect(lastAction.selectables[0].metrics.clientRect.height).toBe(110); 243 | }); 244 | 245 | it('should try resize to 1 element from the top but can not because of its content and the minimum size', function() { 246 | stageStoreMock.selectableElem1.selected = false; 247 | stageStoreMock.selectableElem2.selected = true; 248 | 249 | var mouseState = { 250 | scrollData: {x: 0, y: 0}, 251 | cursorData: {x: 'left', y: 'top', cursorType: ''}, 252 | mouseData: { 253 | mouseX: 20, 254 | movementX: 10, 255 | mouseY: 20, 256 | movementY: 10, 257 | shiftKey: false, 258 | target: document.body, 259 | hovered: [], 260 | } 261 | }; 262 | var handler = initHandler(mouseState); 263 | expect(handler.selection.length).toBe(1); 264 | 265 | var mouseState = { 266 | scrollData: {x: 0, y: 0}, 267 | cursorData: {x: 'left', y: 'top', cursorType: ''}, 268 | mouseData: { 269 | mouseX: 20, 270 | movementX: 10, 271 | mouseY: 20, 272 | movementY: 10, 273 | shiftKey: false, 274 | target: document.body, 275 | hovered: [], 276 | } 277 | }; 278 | var handler = initHandler(mouseState); 279 | handler.update(mouseState.mouseData); 280 | expect(stageStoreMock.dispatch).toBeCalledTimes(1); 281 | var calls = stageStoreMock.dispatch['mock'].calls; 282 | var lastAction = calls[calls.length - 1][0]; 283 | expect(lastAction.type).toBe('SELECTABLE_UPDATE'); 284 | expect(lastAction.selectables.length).toBe(1); 285 | expect(lastAction.selectables[0].el).toBe(StageStoreMock.elem2); 286 | expect(lastAction.selectables[0].metrics.clientRect.width).toBe(20); 287 | expect(lastAction.selectables[0].metrics.clientRect.height).toBe(20); 288 | expect(lastAction.selectables[0].metrics.clientRect.left).toBe(0); 289 | expect(lastAction.selectables[0].metrics.clientRect.top).toBe(0); 290 | }); 291 | }); 292 | -------------------------------------------------------------------------------- /tests/observers/dom-observer.test.electron.ts: -------------------------------------------------------------------------------- 1 | import {StageStoreMock, hooks} from '../flux/StageStoreMock'; 2 | import { 3 | domObservers, 4 | initDomObservers, 5 | addDomObserver, 6 | removeDomObserver, 7 | DomObserver, 8 | } from '../../src/ts/observers/DomObserver'; 9 | 10 | var stageStoreMock: StageStoreMock; 11 | var observer: DomObserver; 12 | beforeEach(() => { 13 | const fn = jest.fn(); 14 | initDomObservers([], fn) 15 | 16 | stageStoreMock = new StageStoreMock() 17 | 18 | observer = new DomObserver(stageStoreMock, (state) => {}); 19 | jest.spyOn(observer, 'onAdded'); 20 | jest.spyOn(observer, 'onRemoved'); 21 | 22 | }) 23 | 24 | it('initDomObservers', () => { 25 | expect(domObservers).toEqual(new Map()); 26 | 27 | const el1 = document.createElement('div'); 28 | const el2 = document.createElement('div'); 29 | initDomObservers([el1, el2], () => {}) 30 | expect(Array.from(domObservers)).toHaveLength(2); 31 | 32 | initDomObservers([], () => {}) 33 | expect(domObservers).toEqual(new Map()); 34 | }) 35 | 36 | it('addDomObserver and removeDomObserver', () => { 37 | const el1 = document.createElement('div'); 38 | const el2 = document.createElement('div'); 39 | addDomObserver(el1, () => {}); 40 | expect(Array.from(domObservers)).toHaveLength(1); 41 | addDomObserver(el2, () => {}); 42 | expect(Array.from(domObservers)).toHaveLength(2); 43 | addDomObserver(el2, () => {}); 44 | expect(Array.from(domObservers)).toHaveLength(2); 45 | removeDomObserver(el1); 46 | expect(Array.from(domObservers)).toHaveLength(1); 47 | removeDomObserver(el2); 48 | expect(Array.from(domObservers)).toHaveLength(0); 49 | expect(() => removeDomObserver(el1)).toThrow(); 50 | }) 51 | 52 | it('onAdded and onRemoved should not be called on change', function() { 53 | stageStoreMock.state = { 54 | ...stageStoreMock.state, 55 | selectables: [{ 56 | ...stageStoreMock.selectableElem1, 57 | metrics: { 58 | proportions: 1, 59 | position: 'relative', 60 | margin: {top: 0, left: 0, bottom: 0, right: 0 }, 61 | padding: {top: 0, left: 0, bottom: 0, right: 0 }, 62 | border: {top: 0, left: 0, bottom: 0, right: 0 }, 63 | computedStyleRect: {top: 200, left: 200, bottom: 200, right: 200, width: 100, height: 100 }, 64 | clientRect: {top: 200, left: 200, bottom: 200, right: 200, width: 100, height: 100 }, 65 | }, 66 | }, 67 | stageStoreMock.selectableElem2, 68 | ], 69 | }; 70 | stageStoreMock.dispatch(null); 71 | expect(observer.onAdded).toBeCalledTimes(0); 72 | expect(observer.onRemoved).toBeCalledTimes(0); 73 | 74 | }); 75 | 76 | it('onRemoved', function() { 77 | stageStoreMock.state = { 78 | ...stageStoreMock.state, 79 | selectables: [], 80 | } 81 | initDomObservers(stageStoreMock.state.selectables.map((s) => s.el), () => {}) 82 | expect(() => stageStoreMock.dispatch(null)).toThrow(); 83 | expect(observer.onAdded).toBeCalledTimes(0); 84 | expect(observer.onRemoved).toBeCalledTimes(1); 85 | 86 | }); 87 | 88 | it('onAdded', function() { 89 | const newDomEl = document.createElement('div'); 90 | document.body.appendChild(newDomEl); 91 | stageStoreMock.state = { 92 | ...stageStoreMock.state, 93 | selectables: [ 94 | ...stageStoreMock.state.selectables, 95 | { 96 | ...stageStoreMock.selectableElem2, 97 | id: 'elemAdded', 98 | el: newDomEl, 99 | }, 100 | ], 101 | }; 102 | stageStoreMock.dispatch(null); 103 | expect(observer.onRemoved).toBeCalledTimes(0); 104 | expect(observer.onAdded).toBeCalledTimes(1); 105 | }); 106 | 107 | 108 | -------------------------------------------------------------------------------- /tests/observers/mouse-observer.test.electron.ts: -------------------------------------------------------------------------------- 1 | import {MouseObserver} from '../../src/ts/observers/MouseObserver'; 2 | import {StageStoreMock, hooks} from '../flux/StageStoreMock'; 3 | 4 | describe('MouseObserver', function() { 5 | var observer; 6 | var stageStoreMock: StageStoreMock; 7 | 8 | beforeEach(function () { 9 | document.body.innerHTML = ` 10 | 18 |
19 |
20 |
21 |
22 | `; 23 | 24 | StageStoreMock.elem1 = document.querySelector('#elem1'); 25 | StageStoreMock.elem2 = document.querySelector('#elem2'); 26 | 27 | stageStoreMock = new StageStoreMock() 28 | jest.spyOn(stageStoreMock, 'subscribe'); 29 | jest.spyOn(stageStoreMock, 'dispatch'); 30 | jest.spyOn(stageStoreMock, 'getState'); 31 | 32 | observer = new MouseObserver(document, document, stageStoreMock, hooks); 33 | jest.spyOn(observer, 'onStateChanged'); 34 | }); 35 | 36 | it('init', function() { 37 | expect(stageStoreMock.subscribe).toHaveBeenCalledTimes(1); 38 | expect(stageStoreMock.getState().mouse.scrollData.x).toBe(0); 39 | 40 | expect(window.scrollY).toBe(0); 41 | }); 42 | 43 | it('onStateChanged', function() { 44 | // scroll 45 | stageStoreMock.state = { 46 | ...stageStoreMock.state, 47 | mouse: { 48 | ...stageStoreMock.state.mouse, 49 | scrollData: { 50 | x: 0, 51 | y: 100, 52 | } 53 | } 54 | }; 55 | stageStoreMock.dispatch(null); 56 | expect(window.scrollX).toBe(0); 57 | expect(window.scrollY).toBe(100); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /tests/observers/selectables-observer.test.jsdom.ts: -------------------------------------------------------------------------------- 1 | import {SelectablesObserver} from '../../src/ts/observers/SelectablesObserver'; 2 | import {StageStoreMock, hooks} from '../flux/StageStoreMock'; 3 | 4 | describe('SelectablesObserver', function() { 5 | var observer: SelectablesObserver; 6 | var stageStoreMock: StageStoreMock; 7 | 8 | beforeEach(function () { 9 | document.body.innerHTML = ` 10 | 18 |
19 |
20 |
21 |
22 | `; 23 | 24 | StageStoreMock.elem1 = document.querySelector('#elem1'); 25 | StageStoreMock.elem2 = document.querySelector('#elem2'); 26 | 27 | stageStoreMock = new StageStoreMock() 28 | jest.spyOn(stageStoreMock, 'subscribe'); 29 | jest.spyOn(stageStoreMock, 'dispatch'); 30 | jest.spyOn(stageStoreMock, 'getState'); 31 | 32 | observer = new SelectablesObserver(document, document, stageStoreMock, hooks); 33 | jest.spyOn(observer, 'onStateChanged'); 34 | jest.spyOn(observer, 'onMetrics'); 35 | jest.spyOn(observer, 'onSelection'); 36 | }); 37 | 38 | it('init', function() { 39 | expect(stageStoreMock.subscribe).toHaveBeenCalledTimes(2); 40 | expect(stageStoreMock.getState().selectables.length).toBe(2); 41 | expect(stageStoreMock.getState().selectables[0]).toBe(stageStoreMock.selectableElem1); 42 | }); 43 | 44 | it('onStateChanged', function() { 45 | stageStoreMock.dispatch(null); 46 | expect(observer.onStateChanged).toBeCalledTimes(1); 47 | expect(observer.onMetrics).toBeCalledTimes(0); 48 | expect(observer.onSelection).toBeCalledTimes(0); 49 | }); 50 | 51 | it('onMetrics', function() { 52 | stageStoreMock.state = { 53 | ...stageStoreMock.state, 54 | selectables: [{ 55 | ...stageStoreMock.selectableElem1, 56 | metrics: { 57 | proportions: 1, 58 | position: 'relative', 59 | margin: {top: 0, left: 0, bottom: 0, right: 0 }, 60 | padding: {top: 0, left: 0, bottom: 0, right: 0 }, 61 | border: {top: 0, left: 0, bottom: 0, right: 0 }, 62 | computedStyleRect: {top: 200, left: 200, bottom: 200, right: 200, width: 100, height: 100 }, 63 | clientRect: {top: 200, left: 200, bottom: 200, right: 200, width: 100, height: 100 }, 64 | }, 65 | }, 66 | stageStoreMock.selectableElem2, 67 | ], 68 | }; 69 | stageStoreMock.dispatch(null); 70 | expect(StageStoreMock.elem1.style.top).toBe('200px'); 71 | expect(observer.onStateChanged).toBeCalledTimes(1); 72 | expect(observer.onMetrics).toBeCalledTimes(1); 73 | expect(observer.onSelection).toBeCalledTimes(0); 74 | }); 75 | it('onSelection', function() { 76 | const state = stageStoreMock.getState(); 77 | stageStoreMock.state = { 78 | ...stageStoreMock.state, 79 | selectables: [{ 80 | ...stageStoreMock.selectableElem1, 81 | selected: true, 82 | }, 83 | stageStoreMock.selectableElem2, 84 | ], 85 | }; 86 | stageStoreMock.dispatch(null); 87 | expect(observer.onStateChanged).toBeCalledTimes(1); 88 | expect(observer.onMetrics).toBeCalledTimes(0); 89 | expect(observer.onSelection).toBeCalledTimes(1); 90 | }); 91 | it('onDraggable', function() { 92 | const state = stageStoreMock.getState(); 93 | stageStoreMock.state = { 94 | ...stageStoreMock.state, 95 | selectables: [{ 96 | ...stageStoreMock.selectableElem1, 97 | draggable: false, 98 | }, 99 | stageStoreMock.selectableElem2, 100 | ], 101 | }; 102 | stageStoreMock.dispatch(null); 103 | expect(observer.onStateChanged).toBeCalledTimes(1); 104 | expect(observer.onMetrics).toBeCalledTimes(0); 105 | expect(observer.onSelection).toBeCalledTimes(0); 106 | }); 107 | it('onResizeable', function() { 108 | const state = stageStoreMock.getState(); 109 | stageStoreMock.state = { 110 | ...stageStoreMock.state, 111 | selectables: [{ 112 | ...stageStoreMock.selectableElem1, 113 | resizeable: { 114 | top: false, 115 | left: false, 116 | bottom: false, 117 | right: false, 118 | }, 119 | }, 120 | stageStoreMock.selectableElem2, 121 | ], 122 | }; 123 | stageStoreMock.dispatch(null); 124 | expect(observer.onStateChanged).toBeCalledTimes(1); 125 | expect(observer.onMetrics).toBeCalledTimes(0); 126 | expect(observer.onSelection).toBeCalledTimes(0); 127 | }); 128 | it('onDropZone', function() { 129 | const state = stageStoreMock.getState(); 130 | stageStoreMock.state = { 131 | ...stageStoreMock.state, 132 | selectables: [{ 133 | ...stageStoreMock.selectableElem1, 134 | isDropZone: false, 135 | }, 136 | stageStoreMock.selectableElem2, 137 | ], 138 | }; 139 | stageStoreMock.dispatch(null); 140 | expect(observer.onStateChanged).toBeCalledTimes(1); 141 | expect(observer.onMetrics).toBeCalledTimes(0); 142 | expect(observer.onSelection).toBeCalledTimes(0); 143 | }); 144 | }); 145 | -------------------------------------------------------------------------------- /tests/observers/ui-observer.test.jsdom.ts: -------------------------------------------------------------------------------- 1 | import * as types from '../../src/ts/Types'; 2 | import {UiObserver} from '../../src/ts/observers/UiObserver'; 3 | import {StageStoreMock, hooks} from '../flux/StageStoreMock'; 4 | import { DrawHandler } from '../../src/ts/handlers/DrawHandler'; 5 | 6 | describe('UiObserver', function() { 7 | var observer; 8 | var stageStoreMock: StageStoreMock; 9 | 10 | beforeEach(function () { 11 | document.body.innerHTML = ` 12 | 20 |
21 |
22 |
23 |
24 | `; 25 | 26 | StageStoreMock.elem1 = document.querySelector('#elem1'); 27 | StageStoreMock.elem2 = document.querySelector('#elem2'); 28 | 29 | stageStoreMock = new StageStoreMock() 30 | jest.spyOn(stageStoreMock, 'subscribe'); 31 | jest.spyOn(stageStoreMock, 'dispatch'); 32 | jest.spyOn(stageStoreMock, 'getState'); 33 | 34 | observer = new UiObserver(document, document, stageStoreMock, hooks); 35 | jest.spyOn(observer, 'onUiStateChanged'); 36 | }); 37 | 38 | it('init', function() { 39 | expect(stageStoreMock.subscribe).toHaveBeenCalledTimes(1); 40 | expect(stageStoreMock.getState().ui.mode).toBe(types.UiMode.NONE); 41 | }); 42 | 43 | it('onUiStateChanged UiMode', function() { 44 | expect(observer.handler).toBeNull(); 45 | 46 | stageStoreMock.state = { 47 | ...stageStoreMock.state, 48 | ui: { 49 | mode: types.UiMode.DRAW, 50 | catchingEvents: true, 51 | refreshing: false, 52 | sticky: types.EMPTY_STICKY_BOX(), 53 | enableSticky: false, 54 | } 55 | }; 56 | stageStoreMock.dispatch(null); 57 | expect(observer.onUiStateChanged).toBeCalledTimes(1); 58 | expect(observer.handler).not.toBeNull(); 59 | expect(observer.handler).toBeInstanceOf(DrawHandler); 60 | 61 | stageStoreMock.initialState = { 62 | ...stageStoreMock.state, 63 | ui: { 64 | mode: types.UiMode.DRAW, 65 | catchingEvents: true, 66 | refreshing: false, 67 | sticky: types.EMPTY_STICKY_BOX(), 68 | enableSticky: false, 69 | } 70 | }; 71 | stageStoreMock.state = { 72 | ...stageStoreMock.state, 73 | ui: { 74 | mode: types.UiMode.NONE, 75 | catchingEvents: true, 76 | refreshing: false, 77 | sticky: types.EMPTY_STICKY_BOX(), 78 | enableSticky: false, 79 | } 80 | }; 81 | stageStoreMock.dispatch(null); 82 | expect(observer.onUiStateChanged).toBeCalledTimes(2); 83 | expect(observer.handler).toBeNull(); 84 | }); 85 | 86 | it('onUiStateChanged scrollData', function() { 87 | stageStoreMock.state = { 88 | ...stageStoreMock.state, 89 | ui: { 90 | mode: types.UiMode.DRAW, 91 | catchingEvents: true, 92 | refreshing: false, 93 | sticky: types.EMPTY_STICKY_BOX(), 94 | enableSticky: false, 95 | } 96 | }; 97 | stageStoreMock.state = { 98 | ...stageStoreMock.state, 99 | mouse: { 100 | ...stageStoreMock.state.mouse, 101 | scrollData: {x: 0, y: 100}, 102 | } 103 | }; 104 | stageStoreMock.dispatch(null); 105 | expect(observer.onUiStateChanged).toBeCalledTimes(1); 106 | }); 107 | 108 | }); 109 | -------------------------------------------------------------------------------- /tests/stage.test.electron.ts: -------------------------------------------------------------------------------- 1 | import {Stage} from '../src/ts/index'; 2 | 3 | class StageTest extends Stage { 4 | getIframe() { return this.iframe } 5 | getContentDocument() { return this.contentDocument } 6 | getContentWindow() { return this.contentWindow } 7 | } 8 | 9 | describe('Stage', () => { 10 | beforeEach(() => { 11 | document.body.innerHTML = '