├── src ├── sandbox │ ├── sandbox.scss │ ├── sandbox.coffee │ └── api.coffee ├── scripts │ ├── utils │ │ └── sortable.coffee │ ├── ui │ │ ├── interfaces │ │ │ ├── interfaces.coffee │ │ │ ├── make-snippet-local.coffee │ │ │ ├── make-snippet-global.coffee │ │ │ ├── snippet-settings.coffee │ │ │ ├── list-snippets.coffee │ │ │ ├── add-snippet.coffee │ │ │ └── order-snippets.coffee │ │ ├── draws.coffee │ │ ├── flows.coffee │ │ ├── toggle.coffee │ │ ├── snippets.coffee │ │ ├── inlays.coffee │ │ └── fields.coffee │ ├── models │ │ ├── flows.coffee │ │ └── snippets.coffee │ ├── namespace.coffee │ ├── api │ │ └── base.coffee │ └── flow-mgr.coffee └── styles │ ├── build.scss │ ├── ui │ ├── _ui.scss │ ├── _flows.scss │ ├── _draws.scss │ ├── _fields.scss │ ├── _toggle.scss │ ├── _inlays.scss │ └── _snippets.scss │ └── _content-flow.scss ├── .gitignore ├── sandbox ├── sandbox.css ├── index.html └── sandbox.js ├── README.md ├── package.json ├── LICENSE ├── Gruntfile.coffee └── externals └── selection.json /src/sandbox/sandbox.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/scripts/utils/sortable.coffee: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | npm-debug.log 2 | .sass-cache 3 | jasmine 4 | node_modules 5 | 6 | .DS_Store 7 | -------------------------------------------------------------------------------- /sandbox/sandbox.css: -------------------------------------------------------------------------------- 1 | /*! content-flow v0.0.11 by Anthony Blackshaw (https://github.com/anthonyjb) */ 2 | -------------------------------------------------------------------------------- /src/styles/build.scss: -------------------------------------------------------------------------------- 1 | @import '../../node_modules/ContentTools/src/styles/content-tools'; 2 | 3 | @import 'content-flow' -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ContentFlow 2 | 3 | There's a short video previewing and rambling through how ContentFlow works here: https://youtu.be/HQaNrI3yJwo 4 | -------------------------------------------------------------------------------- /src/styles/ui/_ui.scss: -------------------------------------------------------------------------------- 1 | @import 'draws'; 2 | @import 'fields'; 3 | @import 'flows'; 4 | @import 'inlays'; 5 | @import 'snippets'; 6 | @import 'toggle'; -------------------------------------------------------------------------------- /src/sandbox/sandbox.coffee: -------------------------------------------------------------------------------- 1 | 2 | window.addEventListener 'load', () -> 3 | 4 | # Get handles to the CT editor and CF flow manager 5 | editor = ContentTools.EditorApp.get() 6 | flowMgr = ContentFlow.FlowMgr.get() 7 | 8 | # Configure and initialize the CT editor 9 | editor.init('[data-cf-snippet], [data-fixture]', 'data-cf-snippet') 10 | 11 | # Configure and initialize the CF flow manager 12 | flowMgr.init(flowsQuery='[data-cf-flow]', api=new MockAPI()) -------------------------------------------------------------------------------- /src/scripts/ui/interfaces/interfaces.coffee: -------------------------------------------------------------------------------- 1 | 2 | class ContentFlow.InterfaceUI extends ContentFlow.InlayUI 3 | 4 | # A base interface UI component used for managing content flows. 5 | 6 | constructor: (heading) -> 7 | super(heading) 8 | 9 | init: () -> 10 | # Initialize the interface 11 | 12 | # Read-only 13 | 14 | safeToClose: () -> 15 | # Returns true if the content flow manager app can be safely closed 16 | # while this view is open. 17 | return false -------------------------------------------------------------------------------- /src/scripts/models/flows.coffee: -------------------------------------------------------------------------------- 1 | 2 | class ContentFlow.FlowModel 3 | 4 | # Content flows allow the flow of content within different sections of a 5 | # page to be managed through smaller sections of prefabricated content 6 | # know as snippets. 7 | 8 | constructor: (id, label=null, snippetTypes=[]) -> 9 | 10 | # A unique Id for the flow (at least within the page) 11 | @id = id 12 | 13 | # A human readable label for the flow 14 | @label = label or id 15 | 16 | # A list of snippet types available in the flow 17 | @snippetTypes = snippetTypes 18 | 19 | # Methods 20 | 21 | getSnippetTypeById: (snippetTypeId) -> 22 | # Return the snippet type 23 | for snippetType in @snippetTypes 24 | if snippetTypeId is snippetType.id 25 | return snippetType -------------------------------------------------------------------------------- /src/styles/ui/_flows.scss: -------------------------------------------------------------------------------- 1 | .ct-flows { 2 | background-color: rgba(0, 0, 0, 0.1); 3 | 4 | &__select { 5 | background-color: transparent; 6 | background-image: 7 | linear-gradient(45deg, transparent 50%, rgba(0, 0, 0, 0.9) 50%), 8 | linear-gradient(135deg, rgba(0, 0, 0, 0.9) 50%, transparent 50%); 9 | background-repeat: no-repeat; 10 | background-position: 11 | calc(100% - 15px) calc(14px), 12 | calc(100% - 10px) calc(14px), 13 | 100% 0; 14 | background-size: 15 | 5px 5px, 16 | 5px 5px, 17 | 32px 32px; 18 | border: none; 19 | line-height: 32px; 20 | outline: none; 21 | padding: 0 10px; 22 | width: 100%; 23 | 24 | -webkit-appearance: none; 25 | -moz-appearance: none; 26 | appearance: none; 27 | } 28 | } -------------------------------------------------------------------------------- /src/styles/ui/_draws.scss: -------------------------------------------------------------------------------- 1 | $draw-width: 250px; 2 | 3 | .ct-widget { 4 | &.ct-draw { 5 | background-color: #fff; 6 | box-shadow: 0 0 3px 0 rgba(0, 0, 0, 0.35), inset -1px 0 0 0 rgba(#fff, 0.5); 7 | display: flex; 8 | flex-direction: column; 9 | font-family: arial, sans-serif; 10 | font-size: 14px; 11 | font-weight: 400; 12 | height: 100%; 13 | line-height: 22px; 14 | opacity: 1; 15 | position: fixed; 16 | right: #{0 - $draw-width}; 17 | top: 0; 18 | width: $draw-width; 19 | z-index: 9998; 20 | 21 | @include transition-property(right); 22 | @include transition-duration(0.25s); 23 | @include transition-timing-function(ease-out); 24 | 25 | &--open { 26 | right: 0; 27 | 28 | @include transition-property(right); 29 | @include transition-duration(0.25s); 30 | @include transition-timing-function(ease-in); 31 | } 32 | } 33 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "content-flow", 3 | "description": "An extension to the ContentTools WYSIWYG editor to manage content flows", 4 | "version": "0.1.5", 5 | "keywords": [ 6 | "wysiwyg", 7 | "inline", 8 | "html", 9 | "editor" 10 | ], 11 | "author": { 12 | "name": "Anthony Blackshaw", 13 | "email": "ant@getme.co.uk", 14 | "url": "https://github.com/anthonyjb" 15 | }, 16 | "main": "build/content-flow.js", 17 | "devDependencies": { 18 | "es6-promise": "^3.2.1", 19 | "es6-require": "^0.2.1", 20 | "grunt": "~0.4.5", 21 | "grunt-contrib-clean": "^0.6.0", 22 | "grunt-contrib-coffee": "^0.11.1", 23 | "grunt-contrib-concat": "^0.5.0", 24 | "grunt-contrib-jasmine": "^0.9.2", 25 | "grunt-contrib-sass": "^0.8.1", 26 | "grunt-contrib-uglify": "^0.7.0", 27 | "grunt-contrib-watch": "^0.6.1", 28 | "grunt-cssnano": "^2.1.0" 29 | }, 30 | "dependencies": { 31 | "ContentTools": "^1.4.0" 32 | }, 33 | "scripts": { 34 | "test": "grunt jasmine --verbose" 35 | }, 36 | "repository": { 37 | "type": "git", 38 | "url": "https://github.com/GetmeUK/ContentFlow.git" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Getme Limited (http://getme.co.uk) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/styles/_content-flow.scss: -------------------------------------------------------------------------------- 1 | $image-path-prefix: '../build/images/' default; 2 | 3 | @import './ui/ui'; 4 | 5 | 6 | // Settings 7 | 8 | $flow-mgr-width: 250px default; 9 | 10 | 11 | // Overiding styles for elements not specific to ContentFlow 12 | 13 | body { 14 | @include transition-property(padding-right); 15 | @include transition-duration(0.25s); 16 | @include transition-timing-function(ease-out); 17 | 18 | &.cf--flow-mgr-open { 19 | padding-right: $draw-width; 20 | 21 | @include transition-property(padding-right); 22 | @include transition-duration(0.25s); 23 | @include transition-timing-function(ease-in); 24 | 25 | .ct-widget { 26 | &.ct-ignition { 27 | right: 378px; 28 | 29 | @include transition-property(all); 30 | @include transition-duration(0.25s); 31 | @include transition-timing-function(ease-in); 32 | } 33 | } 34 | } 35 | } 36 | 37 | .ct-widget { 38 | 39 | // Override the position of the ignition switch 40 | &.ct-ignition { 41 | left: auto; 42 | right: 128px; 43 | top: 23px; 44 | 45 | @include transition-property(all); 46 | @include transition-duration(0.25s); 47 | @include transition-timing-function(ease-out); 48 | } 49 | } 50 | 51 | .cf--highlight-snippet { 52 | position: relative; 53 | 54 | &::after { 55 | background: $edit-action-color; 56 | content: ''; 57 | display: block; 58 | height: 100%; 59 | left: 0; 60 | opacity: 0.33; 61 | position: absolute; 62 | top: 0; 63 | width: 100%; 64 | z-index: 9999; 65 | } 66 | } -------------------------------------------------------------------------------- /src/scripts/ui/draws.coffee: -------------------------------------------------------------------------------- 1 | 2 | class ContentFlow.DrawUI extends ContentTools.ComponentUI 3 | 4 | # A UI component that slides in/out of the screen (by default from the 5 | # right side of the screen). 6 | # 7 | # Draws allow user interfaces to be hidden when not needed and easily 8 | # accessed/revealed when required. 9 | 10 | constructor: () -> 11 | super() 12 | 13 | # The state of the draw 14 | @_state = 'closed' 15 | 16 | # Methods 17 | 18 | open: () -> 19 | # Open the draw 20 | if @dispatchEvent(@createEvent('open')) 21 | @state('open') 22 | 23 | close: () -> 24 | # Close the draw 25 | if @dispatchEvent(@createEvent('close')) 26 | @state('closed') 27 | 28 | mount: () -> 29 | super() 30 | 31 | # Create the DOM element for the draw and mount it 32 | @_domElement = @constructor.createDiv([ 33 | 'ct-widget', 34 | 'ct-draw', 35 | 'ct-draw--closed' 36 | ]) 37 | @parent().domElement().appendChild(@_domElement) 38 | @_addDOMEventListeners() 39 | 40 | # Mount children 41 | for child in @children() 42 | child.mount() 43 | 44 | state: (state) -> 45 | # Get/set the state of the draw 46 | 47 | # If no state value is provided return the current state 48 | if state is undefined 49 | return state 50 | 51 | # If the state hasn't changed there's nothing to do so return 52 | if @state is state 53 | return 54 | 55 | # Dispatch the `statechange` event with details of the change 56 | unless @dispatchEvent(@createEvent('statechange', {state: state})) 57 | return 58 | 59 | # Modify the draw state 60 | @_state = state 61 | 62 | # Remove existing state modifiers 63 | if @isMounted() 64 | @removeCSSClass('ct-draw--open') 65 | @removeCSSClass('ct-draw--closed') 66 | 67 | if @_state is 'open' 68 | @addCSSClass('ct-draw--open') 69 | else 70 | @addCSSClass('ct-draw--closed') 71 | -------------------------------------------------------------------------------- /src/scripts/ui/interfaces/make-snippet-local.coffee: -------------------------------------------------------------------------------- 1 | 2 | class ContentFlow.MakeSnippetLocaUI extends ContentFlow.InterfaceUI 3 | 4 | # Ask a user to confirm they wish to make a snippet local 5 | 6 | constructor: () -> 7 | super('Make local') 8 | 9 | # Add `confirm` and `cancel` tools to the header 10 | @_tools = { 11 | confirm: new ContentFlow.InlayToolUI('confirm', 'Confirm', true), 12 | cancel: new ContentFlow.InlayToolUI('cancel', 'Cancel', true) 13 | } 14 | @_header.tools().attach(@_tools.confirm) 15 | @_header.tools().attach(@_tools.cancel) 16 | 17 | # Handle interactions 18 | 19 | # Confirm 20 | @_tools.confirm.addEventListener 'click', (ev) => 21 | 22 | # Call the API to request the snippet is made local 23 | flowMgr = ContentFlow.FlowMgr.get() 24 | result = flowMgr.api().changeSnippetScope( 25 | flowMgr.flow(), 26 | @_snippet, 27 | 'local' 28 | ) 29 | result.addEventListener 'load', (ev) => 30 | ContentFlow.FlowMgr.get().loadInterface('list-snippets') 31 | 32 | # Cancel 33 | @_tools.cancel.addEventListener 'click', (ev) => 34 | ContentFlow.FlowMgr.get().loadInterface('list-snippets') 35 | 36 | init: (snippet) -> 37 | super() 38 | 39 | # The snippet to change the scope of 40 | @_snippet = snippet 41 | 42 | # Provide some context on local snippets to the user 43 | note = new ContentFlow.InlayNoteUI(ContentEdit._(''' 44 | This action replaces the global snippet within the page with a 45 | local version of the snippet. Changes to local snippets are only 46 | applied to that instance of the snippet. 47 | ''' 48 | )) 49 | @_body.attach(note) 50 | 51 | # (Re)mount the body 52 | @_body.unmount() 53 | @_body.mount() 54 | 55 | 56 | # Register the interface with the content flow manager 57 | ContentFlow.FlowMgr.getCls().registerInterface( 58 | 'make-snippet-local', 59 | ContentFlow.MakeSnippetLocaUI 60 | ) -------------------------------------------------------------------------------- /src/styles/ui/_fields.scss: -------------------------------------------------------------------------------- 1 | .ct-widget { 2 | .ct-field { 3 | padding: 5px 10px; 4 | 5 | &:first-child { 6 | padding-top: 15px; 7 | } 8 | 9 | &--optional { 10 | .ct-field__label::after { 11 | content: ' (Optional)'; 12 | display: inline; 13 | } 14 | } 15 | 16 | &__label { 17 | color: #999; 18 | display: block; 19 | font-size: 14px; 20 | } 21 | 22 | &__error { 23 | color: #e74c3c; 24 | padding: 2px 0; 25 | } 26 | 27 | &__input { 28 | &:not(.ct-field__input--boolean) { 29 | background-color: #fff; 30 | border: none; 31 | border-radius: 2px; 32 | box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.15); 33 | color: #666; 34 | display: block; 35 | font-family: inherit; 36 | font-size: inherit; 37 | height: 40px; 38 | line-height: 40px; 39 | margin: 0; 40 | margin-top: 5px; 41 | padding: 0 10px; 42 | text-align: left; 43 | width: 100%; 44 | } 45 | 46 | &--boolean { 47 | margin-top: 10px; 48 | transform: scale(1.25); 49 | } 50 | 51 | &--select { 52 | background-color: transparent; 53 | background-image: 54 | linear-gradient(45deg, transparent 50%, #666 50%), 55 | linear-gradient(135deg, #666 50%, transparent 50%); 56 | background-repeat: no-repeat; 57 | background-position: 58 | calc(100% - 15px) calc(18px), 59 | calc(100% - 10px) calc(18px), 60 | 100% 0; 61 | background-size: 62 | 5px 5px, 63 | 5px 5px, 64 | 40px 40px; 65 | 66 | -webkit-appearance: none; 67 | -moz-appearance: none; 68 | appearance: none; 69 | } 70 | } 71 | } 72 | } -------------------------------------------------------------------------------- /src/styles/ui/_toggle.scss: -------------------------------------------------------------------------------- 1 | .ct-widget { 2 | 3 | &.ct-toggle { 4 | position: fixed; 5 | right: 64px; 6 | top: 23px; 7 | 8 | @include transition-property(right); 9 | @include transition-duration(0.25s); 10 | @include transition-timing-function(ease-out); 11 | 12 | &--on .ct-toggle__button--off, 13 | &--off .ct-toggle__button--on { 14 | display: block; 15 | } 16 | 17 | &--disabled { 18 | .ct-toggle__button, .ct-toggle__button:hover { 19 | background: #ccc; 20 | } 21 | } 22 | } 23 | 24 | .ct-toggle { 25 | $bkg-color: $edit-action-color; 26 | $button-size: 48px; 27 | 28 | &__button { 29 | // Set the base appeance of the button 30 | background: $bkg-color; 31 | border-radius: $button-size / 2; 32 | cursor: pointer; 33 | display: none; 34 | height: $button-size; 35 | line-height: $button-size; 36 | opacity: 0.9; 37 | position: absolute; 38 | text-align: center; 39 | width: $button-size; 40 | 41 | @include type-icons($font-size: 24px); 42 | 43 | $bkg-color: $edit-action-color; 44 | 45 | &--on { 46 | &:before { 47 | content: '\ea68' 48 | } 49 | } 50 | 51 | &--off { 52 | &:before { 53 | content: '\ea3c' 54 | } 55 | } 56 | 57 | &:before { 58 | background-position: center center; 59 | background-repeat: no-repeat; 60 | background-size: 24px 24px; 61 | color: white; 62 | display: block; 63 | height: $button-size; 64 | width: $button-size; 65 | } 66 | 67 | &:hover { 68 | background: lighten($bkg-color, 5%); 69 | } 70 | } 71 | } 72 | } 73 | 74 | .cf--flow-mgr-open { 75 | .ct-widget { 76 | &.ct-toggle { 77 | right: 314px; 78 | 79 | @include transition-property(right); 80 | @include transition-duration(0.25s); 81 | @include transition-timing-function(ease-in); 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /sandbox/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Sandbox - Content flow 6 | 7 | 8 | 9 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 |
25 |

Content flow

26 |
27 |
34 |
35 |

Data snippet example: basic

36 |
37 |
38 |

Data snippet example: advanced

39 |
40 |
41 | 54 |
55 | 56 | 57 | 58 | 59 | -------------------------------------------------------------------------------- /src/scripts/namespace.coffee: -------------------------------------------------------------------------------- 1 | 2 | ContentFlow = 3 | 4 | dimSnippetDOMElement: (flow, snippet) -> 5 | # Remove the highlight from a snippet within the page 6 | element = ContentFlow.getSnippetDOMElement(flow, snippet) 7 | if element 8 | element.classList.remove('cf--highlight-snippet') 9 | 10 | dimAllSnippetDOMElements: () -> 11 | # Remove the highlight from all snippets within the page 12 | for element in document.querySelectorAll('.cf--highlight-snippet') 13 | element.classList.remove('cf--highlight-snippet') 14 | 15 | getFlowCls: () -> 16 | # Return the content flow model class to use for the application 17 | return ContentFlow.FlowModel 18 | 19 | getFlowDOMelement: (flow) -> 20 | # Return the DOM element represented by a content flow 21 | return document.querySelector("[data-cf-flow='#{ flow.id or flow }']") 22 | 23 | getFlowIdFromDOMElement: (element) -> 24 | # Return the Id of a content flow from a DOM element 25 | return element.getAttribute('data-cf-flow') 26 | 27 | getFlowLabelFromDOMElement: (element) -> 28 | # Return the label of a content flow from a DOM element 29 | return element.getAttribute('data-cf-flow-label') 30 | 31 | getSnippetCls: (flow) -> 32 | # Return the snippet model class to use for the application 33 | return ContentFlow.SnippetModel 34 | 35 | getSnippetDOMElement: (flow, snippet) -> 36 | # Return the DOM element represented by a snippet 37 | return document.querySelector("[data-cf-snippet='#{ snippet.id }']") 38 | 39 | getSnippetIdFromDOMElement: (element) -> 40 | # Return the Id of a snippet from a DOM element 41 | return element.getAttribute('data-cf-snippet') 42 | 43 | getSnippetTypeCls: (flow) -> 44 | # Return the snippet type model class to use for the application 45 | return ContentFlow.SnippetTypeModel 46 | 47 | highlightSnippetDOMElement: (flow, snippet) -> 48 | # Highlight a snippet within the page 49 | element = ContentFlow.getSnippetDOMElement(flow, snippet) 50 | if element 51 | element.classList.add('cf--highlight-snippet') 52 | 53 | 54 | # Export the namespace 55 | 56 | # Browser (via window) 57 | if typeof window != 'undefined' 58 | window.ContentFlow = ContentFlow 59 | 60 | # Node/Browserify 61 | if typeof module != 'undefined' and module.exports 62 | exports = module.exports = ContentFlow -------------------------------------------------------------------------------- /src/scripts/ui/flows.coffee: -------------------------------------------------------------------------------- 1 | 2 | class ContentFlow.FlowsUI extends ContentTools.ComponentUI 3 | 4 | # A UI component that displays a list of content flows a user can choose 5 | # from manage. 6 | 7 | constructor: (flows=[]) -> 8 | super() 9 | 10 | # A list of flows available to manage 11 | @_flows = flows 12 | 13 | # Read-only 14 | 15 | domSelect: () -> 16 | return @_domSelect 17 | 18 | # Methods 19 | 20 | flows: (flows, force=false) -> 21 | # Get/set the flows 22 | 23 | # If no flows value is provided return the current value 24 | if flows is undefined 25 | return @_flows.slice() 26 | 27 | # If the flows hasn't changed there's nothing to do so return 28 | if not force and JSON.stringify(@_flows) is JSON.stringify(flows) 29 | return 30 | 31 | # Set the new list of flows 32 | @_flows = flows 33 | 34 | if @isMounted() 35 | # Remove existing select options 36 | while @_domSelect.options.length > 0 37 | @_domSelect.remove(0) 38 | 39 | # Add new options inline with list of flows provided 40 | for flow in @_flows 41 | domOption = document.createElement('option') 42 | domOption.setAttribute('value', flow.id) 43 | domOption.textContent = flow.label 44 | @_domSelect.appendChild(domOption) 45 | 46 | mount: () -> 47 | super() 48 | 49 | # Create flows and the select field DOM elements 50 | @_domElement = @constructor.createDiv(['ct-flows']) 51 | @_domSelect = document.createElement('select') 52 | @_domSelect.classList.add('ct-flows__select') 53 | @_domSelect.setAttribute('name', 'flows') 54 | 55 | @flows(@_flows, force=true) 56 | @_domElement.appendChild(@_domSelect) 57 | 58 | # Mount flows to the DOM 59 | @parent().domElement().appendChild(@_domElement) 60 | @_addDOMEventListeners() 61 | 62 | select: (flow) -> 63 | # Select the given flow 64 | @_domSelect.value = flow.id 65 | 66 | unmount: () -> 67 | super() 68 | 69 | # Clear reference to the select element 70 | @_domSelect = null 71 | 72 | # Private methods 73 | 74 | _addDOMEventListeners: () -> 75 | 76 | # Handle the selection of a flow 77 | @_domSelect.addEventListener 'change', (ev) => 78 | id = @_domSelect.value 79 | for flow in @_flows 80 | if flow.id == id 81 | @dispatchEvent(@createEvent('select', {flow: flow})) -------------------------------------------------------------------------------- /src/scripts/models/snippets.coffee: -------------------------------------------------------------------------------- 1 | 2 | class ContentFlow.SnippetModel 3 | 4 | # Content flows are made up of snippets of HTML. Snippets allow different 5 | # prefabricated sections of HTML to be entered into a flow before being 6 | # updated via the ContentTools editor. 7 | 8 | constructor: ( 9 | id, 10 | type, 11 | scope='local', 12 | settings={}, 13 | globalId=null, 14 | globalLabel=null 15 | ) -> 16 | 17 | # A unique Id (at least within the flow) for the snippet 18 | @id = id 19 | 20 | # The type of snippet (see SnippetType) 21 | @type = type 22 | 23 | # The scope of the snippet ('local', 'global') 24 | @scope = scope 25 | 26 | # A table of settings for the snippet 27 | @settings = settings 28 | 29 | # If the scope of the snippet is global it will have a separate global 30 | # Id and a label. 31 | @globalId = globalId 32 | 33 | # The label (which should be unique within the global snippets 34 | # available to the content flow) makes it easy for users to identify 35 | # global snippets of the same type. 36 | @globalLabel = globalLabel 37 | 38 | # Class methods 39 | 40 | @fromJSONType: (flow, jsonTypeData) -> 41 | # Convert a JSON type object to a `Snippet` instance 42 | snippetTypeClass = ContentFlow.getSnippetTypeCls(flow) 43 | return new ContentFlow.SnippetModel( 44 | jsonTypeData.id, 45 | snippetTypeClass.fromJSONType(flow, jsonTypeData.type), 46 | jsonTypeData.scope, 47 | jsonTypeData.settings, 48 | jsonTypeData.global_id, 49 | jsonTypeData.global_label 50 | ) 51 | 52 | 53 | class ContentFlow.SnippetTypeModel 54 | 55 | # Snippets are created based on a SnippetType. The snippet type defines 56 | 57 | constructor: (id, label, imageURL=null) -> 58 | 59 | # A unique Id for the snippet type (at least within the flows on the 60 | # page). 61 | @id = id 62 | 63 | # A descriptive label for the snippet type 64 | @label = label 65 | 66 | # An (optional) preview image for the snippet type 67 | @imageURL = imageURL 68 | 69 | toSnippet: () -> 70 | # Convert the snippet type to a shell snippet 71 | return new ContentFlow.SnippetModel('', this) 72 | 73 | # Class methods 74 | 75 | @fromJSONType: (flow, jsonTypeData) -> 76 | # Convert a JSON type object to a `SnippetType` instance 77 | return new ContentFlow.SnippetTypeModel( 78 | jsonTypeData.id, 79 | jsonTypeData.label, 80 | jsonTypeData.image_url 81 | ) 82 | -------------------------------------------------------------------------------- /src/scripts/ui/interfaces/make-snippet-global.coffee: -------------------------------------------------------------------------------- 1 | 2 | class ContentFlow.MakeSnippetGlobalUI extends ContentFlow.InterfaceUI 3 | 4 | # Ask a user to confirm they wish to make a snippet global 5 | 6 | constructor: () -> 7 | super('Make global') 8 | 9 | # Add `confirm` and `cancel` tools to the header 10 | @_tools = { 11 | confirm: new ContentFlow.InlayToolUI('confirm', 'Confirm', true), 12 | cancel: new ContentFlow.InlayToolUI('cancel', 'Cancel', true) 13 | } 14 | @_header.tools().attach(@_tools.confirm) 15 | @_header.tools().attach(@_tools.cancel) 16 | 17 | # Handle interactions 18 | 19 | # Confirm 20 | @_tools.confirm.addEventListener 'click', (ev) => 21 | 22 | # Call the API to request the snippet is made global 23 | flowMgr = ContentFlow.FlowMgr.get() 24 | result = flowMgr.api().changeSnippetScope( 25 | flowMgr.flow(), 26 | @_snippet, 27 | 'global', 28 | @_labelField.value() 29 | ) 30 | result.addEventListener 'load', (ev) => 31 | 32 | # Unpack the response 33 | response = JSON.parse(ev.target.responseText) 34 | 35 | # Handle the response 36 | if response.status is 'success' 37 | ContentFlow.FlowMgr.get().loadInterface('list-snippets') 38 | else 39 | @_labelField.errors([response.payload.errors.label]) 40 | 41 | # Cancel 42 | @_tools.cancel.addEventListener 'click', (ev) => 43 | ContentFlow.FlowMgr.get().loadInterface('list-snippets') 44 | 45 | init: (snippet) -> 46 | super() 47 | 48 | # The snippet to change the scope of 49 | @_snippet = snippet 50 | 51 | # Global snippets require a unique label so we present the user with a 52 | # field to enter a label in. 53 | @_labelField = new ContentFlow.TextFieldUI('label', 'Label', true) 54 | @_body.attach(@_labelField) 55 | 56 | # Provide some context on global snippets to the user 57 | note = new ContentFlow.InlayNoteUI(ContentEdit._(''' 58 | Once you make a snippet global it can be inserted into other pages 59 | and changes made to the snippet's content or settings will be 60 | applied to all instances of the snippet. 61 | ''' 62 | )) 63 | @_body.attach(note) 64 | 65 | # (Re)mount the body 66 | @_body.unmount() 67 | @_body.mount() 68 | 69 | 70 | # Register the interface with the content flow manager 71 | ContentFlow.FlowMgr.getCls().registerInterface( 72 | 'make-snippet-global', 73 | ContentFlow.MakeSnippetGlobalUI 74 | ) -------------------------------------------------------------------------------- /src/scripts/ui/toggle.coffee: -------------------------------------------------------------------------------- 1 | 2 | class ContentFlow.ToggleUI extends ContentTools.WidgetUI 3 | 4 | # An toggle switch widget 5 | 6 | constructor: () -> 7 | super() 8 | 9 | # The state of the toggle switch 10 | @_state = 'off' 11 | 12 | # Flag indicating if the toggle switch is currently enabled 13 | @_enabled = true 14 | 15 | # Read-only 16 | 17 | enabled: () -> 18 | return @_enabled 19 | 20 | # Methods 21 | 22 | disable: () -> 23 | # Disable the toggle switch 24 | if @dispatchEvent(@createEvent('disable')) 25 | @_enabled = false 26 | if @isMounted() 27 | @addCSSClass('ct-toggle--disabled') 28 | 29 | enable: () -> 30 | # Enable the toggle switch 31 | if @dispatchEvent(@createEvent('enable')) 32 | @_enabled = true 33 | if @isMounted() 34 | @removeCSSClass('ct-toggle--disabled') 35 | 36 | mount: () -> 37 | super() 38 | 39 | # Create the DOM elements for the switch, on and off components and 40 | # mount them. 41 | @_domElement = @constructor.createDiv([ 42 | 'ct-widget', 43 | 'ct-toggle', 44 | 'ct-toggle--off' 45 | ]) 46 | @_domOff = @constructor.createDiv([ 47 | 'ct-toggle__button' 48 | 'ct-toggle__button--off' 49 | ]) 50 | @_domElement.appendChild(@_domOff) 51 | @_domOn = @constructor.createDiv([ 52 | 'ct-toggle__button' 53 | 'ct-toggle__button--on' 54 | ]) 55 | @_domElement.appendChild(@_domOn) 56 | @parent().domElement().appendChild(@_domElement) 57 | @_addDOMEventListeners() 58 | 59 | off: () -> 60 | # Switch the toggle switch to the 'off' state 61 | if @dispatchEvent(@createEvent('off')) 62 | @state('off') 63 | 64 | on: () -> 65 | # Switch the toggle switch to the 'on' state 66 | if @dispatchEvent(@createEvent('on')) 67 | @state('on') 68 | 69 | state: (state) -> 70 | # Get/set the state of the toggle switch 71 | 72 | # If no state value is provided return the current state 73 | if state is undefined 74 | return state 75 | 76 | # If the state hasn't changed there's nothing to do so return 77 | if @state is state 78 | return 79 | 80 | # Dispatch the `statechange` event with details of the change 81 | unless @dispatchEvent(@createEvent('statechange', {state: state})) 82 | return 83 | 84 | # Modify the toggle state 85 | @_state = state 86 | 87 | # Remove existing state modifiers 88 | if @isMounted() 89 | @removeCSSClass('ct-toggle--off') 90 | @removeCSSClass('ct-toggle--on') 91 | 92 | if @_state is 'on' 93 | @addCSSClass('ct-toggle--on') 94 | else 95 | @addCSSClass('ct-toggle--off') 96 | 97 | toggle: () -> 98 | # Togg the state of the switch 99 | if @_state is 'on' 100 | @off() 101 | else 102 | @on() 103 | 104 | unmount: () -> 105 | super() 106 | 107 | # Remove references to other elements 108 | this._domOn = null 109 | this._domOff = null 110 | 111 | # Private methods 112 | 113 | _addDOMEventListeners: () -> 114 | super() 115 | 116 | # Handle click events for the off/on buttons 117 | 118 | @_domOff.addEventListener 'click', (ev) => 119 | ev.preventDefault() 120 | if @_enabled 121 | @off() 122 | 123 | @_domOn.addEventListener 'click', (ev) => 124 | ev.preventDefault() 125 | if @_enabled 126 | @on() -------------------------------------------------------------------------------- /src/styles/ui/_inlays.scss: -------------------------------------------------------------------------------- 1 | .ct-widget { 2 | .ct-inlay { 3 | 4 | &__header { 5 | align-items: center; 6 | box-shadow: inset 0 -1px 0 0 rgba(0, 0, 0, 0.1); 7 | display: flex; 8 | padding: 20px 10px; 9 | } 10 | 11 | &__heading { 12 | color: #6a6a6a; 13 | flex: 1; 14 | font-size: 22px; 15 | line-height: 30px; 16 | @include ellipsis(); 17 | 18 | } 19 | 20 | &__tools { 21 | display: flex; 22 | flex: 0 0 auto; 23 | } 24 | 25 | &__body { 26 | height: calc(100vh - 104px); 27 | overflow-x: hidden; 28 | overflow-y: auto; 29 | 30 | &::-webkit-scrollbar-track { 31 | background-color: rgba(0, 0, 0, 0.1); 32 | } 33 | 34 | &::-webkit-scrollbar { 35 | width: 5px; 36 | } 37 | 38 | &::-webkit-scrollbar-thumb { 39 | background-color: rgba(0, 0, 0, 0.25); 40 | } 41 | 42 | &--sorting { 43 | .ct-snippet { 44 | cursor: -moz-grabbing; 45 | cursor: -webkit-grabbing; 46 | cursor: grabbing; 47 | } 48 | } 49 | } 50 | } 51 | 52 | .ct-inlay-tool { 53 | $button-size: 32px; 54 | 55 | align-items: center; 56 | border-radius: 2px; 57 | cursor: pointer; 58 | display: flex; 59 | height: 32px; 60 | justify-content: center; 61 | margin-right: 8px; 62 | position: relative; 63 | width: 32px; 64 | 65 | &:last-child { 66 | margin-right: 0; 67 | } 68 | 69 | &:hover { 70 | background-color: rgba(#000, 0.1); 71 | 72 | &::before { 73 | opacity: 0.8; 74 | visibility: visible; 75 | @include transition(opacity 0s ease-in); 76 | @include transition-delay(2s); 77 | } 78 | 79 | &::after { 80 | color: $edit-action-color; 81 | } 82 | } 83 | 84 | &:active, 85 | &:focus { 86 | background-color: rgba(#000, 0.1); 87 | box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.25); 88 | } 89 | 90 | &::before { 91 | background-color: #000; 92 | border-radius: 3px 3px 0 3px; 93 | color: #fff; 94 | content: attr(data-ct-tooltip); 95 | display: block; 96 | font-family: arial, sans-serif; 97 | font-size: 12px; 98 | hyphens: auto; 99 | line-height: 20px; 100 | opacity: 0; 101 | padding: 0 8px; 102 | text-align: center; 103 | visibility: hidden; 104 | width: 85px; 105 | word-break: break-word; 106 | z-index: 1; 107 | @include position(absolute, null null 24px -94px); 108 | } 109 | 110 | &::after { 111 | background-position: center center; 112 | background-repeat: no-repeat; 113 | background-size: 24px 24px; 114 | color: #999; 115 | display: block; 116 | height: $button-size; 117 | line-height: $button-size; 118 | text-align: center; 119 | width: $button-size; 120 | 121 | @include type-icons($font-size: 16px); 122 | } 123 | 124 | // Set of tool icon modifiers 125 | 126 | &--add::after { 127 | content: '\ea0a'; 128 | } 129 | 130 | &--cancel::after { 131 | content: '\ea0f'; 132 | } 133 | 134 | &--confirm::after { 135 | content: '\ea10'; 136 | } 137 | 138 | &--order::after { 139 | content: '\e9bd'; 140 | } 141 | } 142 | 143 | .ct-inlay-section { 144 | &__heading { 145 | box-shadow: inset 0 -1px 0 0 rgba(0, 0, 0, 0.1); 146 | color: #6a6a6a; 147 | font-size: 16px; 148 | line-height: 20px; 149 | @include ellipsis(); 150 | padding: 20px 10px 10px; 151 | width: 100%; 152 | } 153 | } 154 | 155 | .ct-inlay-note { 156 | color: #999; 157 | font-size: 14px; 158 | padding: 20px 10px; 159 | } 160 | } -------------------------------------------------------------------------------- /src/scripts/api/base.coffee: -------------------------------------------------------------------------------- 1 | 2 | class ContentFlow.BaseAPI 3 | 4 | # ContentFlow interfaces use API classes to request information or changes 5 | # (typically from a remote server). Each application ContentFlow is 6 | # integrated into will likely have its own API class or will implement its 7 | # API around the base API for ContentFlow. 8 | 9 | constructor: (baseURL='/', baseParams={}) -> 10 | 11 | # A base URL to be prepended to all API endpoints 12 | @baseURL = baseURL 13 | 14 | # A base table of parameters that will be attached to all API requests 15 | @baseParams = baseParams 16 | 17 | # API methods 18 | 19 | # GET 20 | 21 | getGlobalSnippets: (flow) -> 22 | # Request a list of gloval snippets for the given content flow 23 | return @_callEndpoint('GET', 'global-snippets', {flow: flow.id}) 24 | 25 | getSnippets: (flow) -> 26 | # Request a list of snippets for the given content flow 27 | return @_callEndpoint('GET', 'snippets', {flow: flow.id}) 28 | 29 | getSnippetTypes: (flow) -> 30 | # Request a list of snippet types for the given content flow 31 | return @_callEndpoint('GET', 'snippet-types', {flow: flow.id}) 32 | 33 | getSnippetSettingsForm: (flow, snippet) -> 34 | # Request a list of fields for the the snippet settings form 35 | return @_callEndpoint( 36 | 'GET', 37 | 'update-snippet-settings', 38 | {flow: flow.id, snippet: snippet.id} 39 | ) 40 | 41 | # POST 42 | 43 | addSnippet: (flow, snippetType) -> 44 | # Add a new snippet of the given type to the content flow 45 | return @_callEndpoint( 46 | 'POST', 47 | 'add-snippet', 48 | {flow: flow.id, snippet_type: snippetType.id} 49 | ) 50 | 51 | addGlobalSnippet: (flow, globalSnippet) -> 52 | # Add a global snippet to the content flow 53 | return @_callEndpoint( 54 | 'POST', 55 | 'add-global-snippet', 56 | {flow: flow.id, global_snippet: globalSnippet.globalId} 57 | ) 58 | 59 | changeSnippetScope: (flow, snippet, scope, label=null) -> 60 | # Change the scope of the given snippet 61 | return @_callEndpoint( 62 | 'POST', 63 | 'change-snippet-scope', 64 | {flow: flow.id, snippet: snippet.id, scope: scope, label: label} 65 | ) 66 | 67 | updateSnippetSettings: (flow, snippet, settings) -> 68 | # Update the settings for the given snippet 69 | params = {flow: flow.id, snippet: snippet.id} 70 | for k, v of settings 71 | params[k] = v 72 | return @_callEndpoint('POST', 'update-snippet-settings', params) 73 | 74 | deleteSnippet: (flow, snippet) -> 75 | # Delete a snippet from the given content flow 76 | return @_callEndpoint( 77 | 'POST', 78 | 'delete-snippet', 79 | {flow: flow.id, snippet: snippet.id} 80 | ) 81 | 82 | orderSnippets: (flow, snippets) -> 83 | # Order the snippets in the given content flow 84 | return @_callEndpoint( 85 | 'POST', 86 | 'order-snippets', 87 | {flow: flow.id, snippets: (s.id for s in snippets)} 88 | ) 89 | 90 | # Private methods 91 | 92 | _callEndpoint: (method, endpoint, params={}) -> 93 | # Call an API endpoint and return the result 94 | 95 | xhr = new XMLHttpRequest() 96 | formData = null 97 | paramsStr = '' 98 | 99 | # Merge params and base params 100 | for k, v of @baseParams 101 | if params[k] is undefined 102 | params[k] = v 103 | 104 | switch method.toLowerCase() 105 | when 'get' 106 | pairs = Object.keys(params).map (p) -> 107 | if Array.isArray(params[p]) 108 | params[p] = JSON.stringify(params[p]) 109 | return [p, params[p]].map(encodeURIComponent).join('=') 110 | paramsStr = "?#{ pairs.join("&") }&_=#{ Date.now() }" 111 | 112 | when 'delete', 'post', 'post' 113 | formData = new FormData() 114 | for k, v of params 115 | if Array.isArray(v) 116 | v = JSON.stringify(v) 117 | formData.append(k, v) 118 | 119 | xhr.open(method, "#{ @baseURL }#{ endpoint }#{ paramsStr }") 120 | xhr.send(formData) 121 | 122 | return xhr 123 | -------------------------------------------------------------------------------- /Gruntfile.coffee: -------------------------------------------------------------------------------- 1 | module.exports = (grunt) -> 2 | 3 | require('es6-promise').polyfill() 4 | 5 | # Project configuration 6 | grunt.initConfig({ 7 | 8 | pkg: grunt.file.readJSON('package.json') 9 | 10 | coffee: 11 | options: 12 | join: true 13 | 14 | build: 15 | files: 16 | 'build/content-flow.js': [ 17 | 'src/scripts/namespace.coffee', 18 | 'src/scripts/flow-mgr.coffee', 19 | 'src/scripts/api/base.coffee', 20 | 'src/scripts/models/flows.coffee', 21 | 'src/scripts/models/snippets.coffee', 22 | 'src/scripts/ui/draws.coffee', 23 | 'src/scripts/ui/fields.coffee', 24 | 'src/scripts/ui/flows.coffee', 25 | 'src/scripts/ui/inlays.coffee', 26 | 'src/scripts/ui/snippets.coffee', 27 | 'src/scripts/ui/toggle.coffee', 28 | 'src/scripts/ui/interfaces/interfaces.coffee', 29 | 'src/scripts/ui/interfaces/add-snippet.coffee', 30 | 'src/scripts/ui/interfaces/list-snippets.coffee', 31 | 'src/scripts/ui/interfaces/make-snippet-global.coffee', 32 | 'src/scripts/ui/interfaces/make-snippet-local.coffee', 33 | 'src/scripts/ui/interfaces/order-snippets.coffee', 34 | 'src/scripts/ui/interfaces/snippet-settings.coffee' 35 | ] 36 | 37 | sandbox: 38 | files: 39 | 'src/tmp/sandbox.js': [ 40 | 'src/sandbox/api.coffee' 41 | 'src/sandbox/sandbox.coffee' 42 | ] 43 | 44 | sass: 45 | options: 46 | banner: '/*! <%= pkg.name %> v<%= pkg.version %> by <%= pkg.author.name %> <<%= pkg.author.email %>> (<%= pkg.author.url %>) */' 47 | sourcemap: 'none' 48 | 49 | build: 50 | files: 51 | 'build/content-flow.min.css': 52 | 'src/styles/build.scss' 53 | 54 | sandbox: 55 | files: 56 | 'sandbox/sandbox.css': 'src/sandbox/sandbox.scss' 57 | 58 | cssnano: 59 | options: 60 | sourcemap: false 61 | 62 | build: 63 | files: 64 | 'build/content-flow.min.css': 65 | 'build/content-flow.min.css' 66 | 67 | uglify: 68 | options: 69 | banner: '/*! <%= pkg.name %> v<%= pkg.version %> by <%= pkg.author.name %> <<%= pkg.author.email %>> (<%= pkg.author.url %>) */\n' 70 | mangle: true 71 | 72 | build: 73 | src: 'build/content-flow.js' 74 | dest: 'build/content-flow.min.js' 75 | 76 | concat: 77 | sandbox: 78 | src: [ 79 | 'src/tmp/sandbox.js' 80 | ] 81 | dest: 'sandbox/sandbox.js' 82 | 83 | clean: 84 | build: ['src/tmp'] 85 | 86 | watch: 87 | build: 88 | files: [ 89 | 'src/scripts/**/*.coffee', 90 | 'src/styles/**/*.scss' 91 | ] 92 | tasks: ['build'] 93 | 94 | sandbox: 95 | files: [ 96 | 'src/sandbox/*.coffee', 97 | 'src/sandbox/*.scss' 98 | ] 99 | tasks: ['sandbox'] 100 | }) 101 | 102 | # Plug-ins 103 | grunt.loadNpmTasks 'grunt-contrib-clean' 104 | grunt.loadNpmTasks 'grunt-contrib-coffee' 105 | grunt.loadNpmTasks 'grunt-contrib-concat' 106 | grunt.loadNpmTasks 'grunt-contrib-jasmine' 107 | grunt.loadNpmTasks 'grunt-contrib-sass' 108 | grunt.loadNpmTasks 'grunt-contrib-uglify' 109 | grunt.loadNpmTasks 'grunt-contrib-watch' 110 | grunt.loadNpmTasks 'grunt-cssnano' 111 | 112 | # Tasks 113 | grunt.registerTask 'build', [ 114 | 'coffee:build' 115 | 'sass:build' 116 | 'cssnano:build' 117 | 'uglify:build' 118 | 'clean:build' 119 | ] 120 | 121 | grunt.registerTask 'sandbox', [ 122 | 'coffee:sandbox' 123 | 'concat:sandbox' 124 | 'sass:sandbox' 125 | ] 126 | 127 | grunt.registerTask 'watch-build', ['watch:build'] 128 | grunt.registerTask 'watch-sandbox', ['watch:sandbox'] 129 | -------------------------------------------------------------------------------- /src/scripts/ui/interfaces/snippet-settings.coffee: -------------------------------------------------------------------------------- 1 | 2 | class ContentFlow.SnippetSettingsUI extends ContentFlow.InterfaceUI 3 | 4 | # Display the settings for a snippet and allow them to be changed 5 | 6 | constructor: () -> 7 | super('Settings') 8 | 9 | # Add `confirm` and `cancel` tools to the header 10 | @_tools = { 11 | confirm: new ContentFlow.InlayToolUI('confirm', 'Confirm', true), 12 | cancel: new ContentFlow.InlayToolUI('cancel', 'Cancel', true) 13 | } 14 | @_header.tools().attach(@_tools.confirm) 15 | @_header.tools().attach(@_tools.cancel) 16 | 17 | # Handle interactions 18 | 19 | # Confirm 20 | @_tools.confirm.addEventListener 'click', (ev) => 21 | 22 | # Check there are settings that can be changed for the snippet 23 | unless @_fields 24 | ContentFlow.FlowMgr.get().loadInterface('list-snippets') 25 | return 26 | 27 | # Convert the form values into a settings object 28 | settings = {} 29 | for _, field of @_fields 30 | settings[field.name()] = field.value() 31 | 32 | # Call the API to request the change to the snippet's settings 33 | flowMgr = ContentFlow.FlowMgr.get() 34 | result = flowMgr.api().updateSnippetSettings( 35 | flowMgr.flow(), 36 | @_snippet, 37 | settings 38 | ) 39 | result.addEventListener 'load', (ev) => 40 | 41 | # Unpack the response 42 | response = JSON.parse(ev.target.responseText) 43 | 44 | # Handle the response 45 | if response.status is 'success' 46 | flow = ContentFlow.FlowMgr.get().flow() 47 | 48 | # Find current snippet element in the DOM 49 | originalElement = ContentFlow.getSnippetDOMElement( 50 | flow, 51 | @_snippet 52 | ) 53 | 54 | # Build the new element 55 | newElement = document.createElement('div') 56 | newElement.innerHTML = response.payload['html'] 57 | newElement = newElement.children[0] 58 | 59 | # Replace the current element with the new one 60 | originalElement.parentNode.replaceChild( 61 | newElement, 62 | originalElement 63 | ) 64 | 65 | # Done! Load the snippets listing 66 | ContentFlow.FlowMgr.get().loadInterface('list-snippets') 67 | 68 | else 69 | for fieldName, errors of response.payload.errors 70 | if @_fields[fieldName] 71 | @_fields[fieldName].errors(errors) 72 | 73 | # Cancel 74 | @_tools.cancel.addEventListener 'click', (ev) => 75 | ContentFlow.FlowMgr.get().loadInterface('list-snippets') 76 | 77 | init: (snippet) -> 78 | 79 | # The snippet we'll be changing the settings for 80 | @_snippet = snippet 81 | 82 | # Load the list of the snippet's setting form 83 | flowMgr = ContentFlow.FlowMgr.get() 84 | result = flowMgr.api().getSnippetSettingsForm(flowMgr.flow(), snippet) 85 | result.addEventListener 'load', (ev) => 86 | 87 | # Unpack the response 88 | payload = JSON.parse(ev.target.responseText).payload 89 | 90 | # Check there's at least one field in the settings form 91 | unless payload.fields 92 | # Flag the form as empty 93 | @_fields = null 94 | 95 | # Add a note letting the user know there's no settings to 96 | # change. 97 | note = ContentFlow.InlayNoteUI( 98 | 'There are no settings defined for this snippet.' 99 | ) 100 | 101 | # (Re)mount the body 102 | @_body.unmount() 103 | @_body.mount() 104 | 105 | return 106 | 107 | # Build the form 108 | @_fields = {} 109 | for fieldData in payload.fields 110 | field = ContentFlow.FieldUI.fromJSONType(fieldData) 111 | @_fields[field.name()] = field 112 | @_body.attach(field) 113 | 114 | # (Re)mount the body 115 | @_body.unmount() 116 | @_body.mount() 117 | 118 | # Populate the fields 119 | for k, v in @_snippet.settings 120 | if @_fields[k] 121 | @_fields[k].value(v) 122 | 123 | 124 | # Register the interface with the content flow manager 125 | ContentFlow.FlowMgr.getCls().registerInterface( 126 | 'snippet-settings', 127 | ContentFlow.SnippetSettingsUI 128 | ) -------------------------------------------------------------------------------- /src/scripts/ui/snippets.coffee: -------------------------------------------------------------------------------- 1 | 2 | class ContentFlow.SnippetUI extends ContentTools.ComponentUI 3 | 4 | # A UI component representing a snippet within a content flow 5 | 6 | constructor: (snippet, behaviour, flags) -> 7 | super() 8 | 9 | # The snippet the component represents 10 | @_snippet = snippet 11 | 12 | # The behaviour the snippet will support: 13 | # 14 | # - 'pick' allow the snippet to be picked (you pick a snippet when 15 | # adding one). 16 | # - 'manage' allow the snippet to be managed (settings, scrope and 17 | # delete tools will be displayed within the snippet). 18 | # - 'order' allow the snippet to be dragged within a its siblings to 19 | # changes its position (the order). 20 | @_behaviour = behaviour 21 | 22 | # A set of flags which further modify the behaviour of the snippet 23 | @_flags = flags 24 | 25 | # Methods 26 | 27 | mount: () -> 28 | super() 29 | 30 | # Create the DOM elements for the snippet, preview image, label and 31 | # tools 32 | 33 | # Snippet 34 | this._domElement = @constructor.createDiv([ 35 | 'ct-snippet', 36 | 'ct-snippet--behaviour-' + @_behaviour, 37 | 'ct-snippet--scope-' + @_snippet.scope 38 | ]) 39 | this._domElement.setAttribute('data-snippet-id', @_snippet.id) 40 | 41 | # Preview image 42 | @_domPreview = @constructor.createDiv(['ct-snippet__preview']) 43 | if @_snippet.type.imageURL 44 | bkgURL = "url(#{ @_snippet.type.imageURL })" 45 | @_domPreview.style.backgroundImage = bkgURL 46 | @_domElement.appendChild(@_domPreview) 47 | 48 | # Label 49 | @_domLabel = @constructor.createDiv(['ct-snippet__label']) 50 | if @_snippet.globalLabel 51 | @_domLabel.textContent = @_snippet.globalLabel 52 | else 53 | @_domLabel.textContent = @_snippet.type.label 54 | @_domElement.appendChild(@_domLabel) 55 | 56 | # Tools (settings, scope, delete) 57 | if @_behaviour is 'manage' 58 | 59 | @_domTools = @constructor.createDiv(['ct-snippet__tools']) 60 | 61 | # Settings 62 | @_domSettingsTool = @constructor.createDiv([ 63 | 'ct-snippet__tool', 64 | 'ct-snippet__tool--settings' 65 | ]) 66 | @_domSettingsTool.setAttribute( 67 | 'data-ct-tooltip', 68 | ContentEdit._('Settings') 69 | ) 70 | @_domTools.appendChild(@_domSettingsTool) 71 | 72 | # Scope 73 | @_domScopeTool = @constructor.createDiv([ 74 | 'ct-snippet__tool', 75 | 'ct-snippet__tool--scope' 76 | ]) 77 | @_domScopeTool.setAttribute( 78 | 'data-ct-tooltip', 79 | ContentEdit._('Scope') 80 | ) 81 | @_domTools.appendChild(@_domScopeTool) 82 | 83 | # Delete 84 | if @_flags.permanent 85 | @_domElement.classList.add('ct-snippet--permanent') 86 | 87 | else 88 | @_domElement.classList.remove('ct-snippet--permanent') 89 | 90 | @_domDeleteTool = @constructor.createDiv([ 91 | 'ct-snippet__tool', 92 | 'ct-snippet__tool--delete' 93 | ]) 94 | @_domDeleteTool.setAttribute( 95 | 'data-ct-tooltip', 96 | ContentEdit._('Delete') 97 | ) 98 | @_domTools.appendChild(@_domDeleteTool) 99 | 100 | @_domElement.appendChild(@_domTools) 101 | 102 | # Mount the snippet to the DOM 103 | @parent().domElement().appendChild(@_domElement) 104 | @_addDOMEventListeners() 105 | 106 | # Private methods 107 | 108 | _addDOMEventListeners: () -> 109 | super() 110 | 111 | # Add common event handlers (over/out) 112 | @_domElement.addEventListener 'mouseover', (ev) => 113 | @dispatchEvent(@createEvent('over', {snippet: @_snippet})) 114 | 115 | @_domElement.addEventListener 'mouseout', (ev) => 116 | @dispatchEvent(@createEvent('out', {snippet: @_snippet})) 117 | 118 | if @_behaviour is 'manage' 119 | 120 | # Add event handlers for manage (settings, scope and delete) 121 | @_domSettingsTool.addEventListener 'click', (ev) => 122 | @dispatchEvent(@createEvent('settings', {snippet: @_snippet})) 123 | 124 | @_domScopeTool.addEventListener 'click', (ev) => 125 | @dispatchEvent(@createEvent('scope', {snippet: @_snippet})) 126 | 127 | if @_domDeleteTool 128 | @_domDeleteTool.addEventListener 'click', (ev) => 129 | @dispatchEvent( 130 | @createEvent('delete', {snippet: @_snippet}) 131 | ) 132 | 133 | else if @_behaviour is 'pick' 134 | # Add event handlers for pick 135 | @_domElement.addEventListener 'click', (ev) => 136 | @dispatchEvent(@createEvent('pick', {snippet: @_snippet})) -------------------------------------------------------------------------------- /src/styles/ui/_snippets.scss: -------------------------------------------------------------------------------- 1 | .ct-widget { 2 | .ct-snippet { 3 | box-shadow: inset 0 -1px 0 0 rgba(0, 0, 0, 0.1); 4 | padding: 10px 10px 0; 5 | position: relative; 6 | 7 | &--ghost { 8 | background: #f8f8f8 !important; 9 | color: rgba(0, 0, 0, 0.6) !important; 10 | } 11 | 12 | &--helper { 13 | background: #fff !important; 14 | z-index: 9999 !important; 15 | } 16 | 17 | &--scope-global { 18 | .ct-snippet__label::before { 19 | background: #999; 20 | border-radius: 8px; 21 | color: #fff; 22 | content: 'G'; 23 | display: inline-block; 24 | font-weight: bold; 25 | height: 16px; 26 | line-height: 16px; 27 | margin-right: 5px; 28 | text-align: center; 29 | width: 16px; 30 | } 31 | } 32 | 33 | &--behaviour-manage { 34 | &:hover { 35 | .ct-snippet__tools { 36 | opacity: 1.0; 37 | @include transition(opacity 0.5s ease-in); 38 | } 39 | } 40 | } 41 | 42 | &--behaviour-order { 43 | -moz-user-select: none; 44 | -ms-user-select: none; 45 | -webkit-user-select: none; 46 | 47 | cursor: -moz-grab; 48 | cursor: -webkit-grab; 49 | cursor: grab; 50 | user-select: none; 51 | } 52 | 53 | &--behaviour-pick { 54 | cursor: pointer; 55 | 56 | &:hover { 57 | .ct-snippet__label { 58 | color: $edit-action-color; 59 | 60 | &:before { 61 | background: $edit-action-color; 62 | } 63 | } 64 | } 65 | } 66 | 67 | &__label { 68 | color: #999; 69 | font-size: 14px; 70 | margin-top: 5px; 71 | @include ellipsis(); 72 | @include flex(1); 73 | } 74 | 75 | &__preview { 76 | background: rgba(0, 0, 0, 0.025); 77 | background-position: center center; 78 | background-repeat: no-repeat; 79 | background-size: contain; 80 | height: 50px; 81 | } 82 | 83 | &__tools { 84 | background: rgba(#fff, 0.55); 85 | bottom: 0; 86 | height: 100%; 87 | left: 0; 88 | opacity: 0.0; 89 | position: absolute; 90 | width: 100%; 91 | @include transition(opacity 0.5s ease-out); 92 | } 93 | 94 | &__tool { 95 | $button-size: 32px; 96 | 97 | align-items: center; 98 | bottom: 8px; 99 | cursor: pointer; 100 | display: flex; 101 | height: $button-size; 102 | justify-content: center; 103 | right: 8px; 104 | position: absolute; 105 | width: $button-size; 106 | z-index: 1; 107 | 108 | &:hover { 109 | &::before { 110 | opacity: 0.8; 111 | visibility: visible; 112 | @include transition(opacity 0s ease-in); 113 | @include transition-delay(2s); 114 | } 115 | 116 | &::after { 117 | color: $edit-action-color; 118 | } 119 | } 120 | 121 | &::before { 122 | background-color: #000; 123 | border-radius: 3px 3px 0 3px; 124 | color: #fff; 125 | content: attr(data-ct-tooltip); 126 | display: block; 127 | font-family: arial, sans-serif; 128 | font-size: 12px; 129 | hyphens: auto; 130 | line-height: 20px; 131 | opacity: 0; 132 | padding: 0 8px; 133 | text-align: center; 134 | visibility: hidden; 135 | width: 85px; 136 | word-break: break-word; 137 | z-index: 1; 138 | @include position(absolute, null null 24px -94px); 139 | } 140 | 141 | &::after { 142 | background-position: center center; 143 | background-repeat: no-repeat; 144 | background-size: 24px 24px; 145 | color: #999; 146 | display: block; 147 | height: $button-size; 148 | line-height: $button-size; 149 | text-align: center; 150 | width: $button-size; 151 | 152 | @include type-icons($font-size: 16px); 153 | } 154 | 155 | &--tooltip-left { 156 | &::before { 157 | @include position(absolute, null null 6px -100px); 158 | } 159 | } 160 | 161 | &--settings { 162 | right: 84px; 163 | 164 | &::after { 165 | content: '\e993'; 166 | } 167 | } 168 | 169 | &--scope { 170 | right: 46px; 171 | 172 | &::after { 173 | content: '\e9ca'; 174 | } 175 | } 176 | 177 | &--delete { 178 | &::after { 179 | content: '\e9ac'; 180 | } 181 | } 182 | } 183 | 184 | &--permanent { 185 | .ct-snippet__tool { 186 | 187 | &--settings { 188 | right: 46px; 189 | } 190 | 191 | &--scope { 192 | right: 8px; 193 | } 194 | } 195 | } 196 | } 197 | } 198 | 199 | .ct-snippet { 200 | &--helper { 201 | background: #fff !important; 202 | z-index: 9999 !important; 203 | } 204 | } -------------------------------------------------------------------------------- /src/scripts/ui/interfaces/list-snippets.coffee: -------------------------------------------------------------------------------- 1 | 2 | class ContentFlow.ListSnippetsUI extends ContentFlow.InterfaceUI 3 | 4 | # Display a list of snippets for the content flow 5 | 6 | constructor: () -> 7 | super('Snippets') 8 | 9 | # Add `order` and `add` tools to the header 10 | @_tools = { 11 | order: new ContentFlow.InlayToolUI('order', 'Order', true), 12 | add: new ContentFlow.InlayToolUI('add', 'Add', true) 13 | } 14 | 15 | # Handle interactions 16 | 17 | @_tools.order.addEventListener 'click', (ev) => 18 | ContentFlow.FlowMgr.get().loadInterface('order-snippets') 19 | 20 | @_tools.add.addEventListener 'click', (ev) => 21 | ContentFlow.FlowMgr.get().loadInterface('add-snippet') 22 | 23 | init: () -> 24 | super() 25 | 26 | # Dim any highlighted snippets 27 | ContentFlow.dimAllSnippetDOMElements() 28 | 29 | # Load the list of the snippets within the content flow 30 | flowMgr = ContentFlow.FlowMgr.get() 31 | result = flowMgr.api().getSnippets(flowMgr.flow()) 32 | result.addEventListener 'load', (ev) => 33 | flow = ContentFlow.FlowMgr.get().flow() 34 | 35 | # Unpack the response 36 | payload = JSON.parse(ev.target.responseText).payload 37 | 38 | # Remove existing snippets from the interface 39 | for child in @_body.children 40 | @_body.detach(child) 41 | 42 | # Set up the tools available for the flow 43 | flowElm = ContentFlow.getFlowDOMelement(flow) 44 | maxSnippets = parseInt(flowElm.dataset.cfFlowMaxSnippets) or 0 45 | 46 | # Detatch all tools 47 | for child in @_header.tools().children 48 | @_header.tools().detatch(child) 49 | 50 | # Add the 'add' tool if the flow isn't full 51 | if maxSnippets == 0 or payload.snippets.length < maxSnippets 52 | @_header.tools().attach(@_tools.add) 53 | 54 | # Add the 'order' tool if the flow isn't frozen 55 | if flowElm.dataset.cfFlowFrozen == undefined 56 | @_header.tools().attach(@_tools.order) 57 | 58 | @_header.unmount() 59 | @_header.mount() 60 | 61 | # Add snippets 62 | snippetCls = ContentFlow.getSnippetCls(flow) 63 | for snippetData in payload.snippets 64 | snippet = snippetCls.fromJSONType(flow, snippetData) 65 | snippetElm = ContentFlow.getSnippetDOMElement(flow, snippet) 66 | 67 | uiSnippet = new ContentFlow.SnippetUI( 68 | snippet, 69 | 'manage', 70 | { 71 | 'permanent': 72 | snippetElm.dataset.cfSnippetPermanent != undefined 73 | } 74 | ) 75 | @_body.attach(uiSnippet) 76 | 77 | # Handle interactions 78 | 79 | # Common 80 | uiSnippet.addEventListener 'over', (ev) -> 81 | ContentFlow.highlightSnippetDOMElement( 82 | ContentFlow.FlowMgr.get().flow(), 83 | ev.detail().snippet 84 | ) 85 | 86 | uiSnippet.addEventListener 'out', (ev) -> 87 | ContentFlow.dimSnippetDOMElement( 88 | ContentFlow.FlowMgr.get().flow(), 89 | ev.detail().snippet 90 | ) 91 | 92 | # Managing of snippets (settings, scope, delete) 93 | 94 | # Settings 95 | uiSnippet.addEventListener 'settings', (ev) -> 96 | ContentFlow.FlowMgr.get().loadInterface( 97 | 'snippet-settings', 98 | ev.detail().snippet 99 | ) 100 | 101 | # Scope 102 | uiSnippet.addEventListener 'scope', (ev) -> 103 | scope = 'local' 104 | if ev.detail().snippet.scope is 'local' 105 | scope = 'global' 106 | ContentFlow.FlowMgr.get().loadInterface( 107 | "make-snippet-#{ scope }", 108 | ev.detail().snippet 109 | ) 110 | 111 | # Delete 112 | uiSnippet.addEventListener 'delete', (ev) => 113 | msg = ContentEdit._( 114 | 'Are you sure you want to delete this snippet?' 115 | ) 116 | if confirm(msg) 117 | # Call the API to remove the snippet 118 | flowMgr = ContentFlow.FlowMgr.get() 119 | result = flowMgr.api().deleteSnippet( 120 | flowMgr.flow(), 121 | ev.detail().snippet 122 | ) 123 | 124 | _removeSnippet = (flow, snippet) -> 125 | return (ev) -> 126 | # Remove the snippet from the page 127 | domSnippet = ContentFlow.getSnippetDOMElement( 128 | flow, 129 | snippet 130 | ) 131 | domSnippet.remove() 132 | 133 | # Show the list of snippets now in the flow. We 134 | # re-sync the page flows in case a flow was 135 | # removed as part of the snippet, then we force 136 | # reselect this flow which ensures the correct 137 | # flow is selected and triggers a list-snippets 138 | # interface load. 139 | flowMgr.syncFlows() 140 | flowMgr.flow(flow, force=true) 141 | 142 | result.addEventListener( 143 | 'load', 144 | _removeSnippet(flowMgr.flow(), ev.detail().snippet) 145 | ) 146 | 147 | # (Re)mount the body 148 | @_body.unmount() 149 | @_body.mount() 150 | 151 | # Read-only 152 | 153 | safeToClose: () -> 154 | return true 155 | 156 | # Register the interface with the content flow manager 157 | ContentFlow.FlowMgr.getCls().registerInterface( 158 | 'list-snippets', 159 | ContentFlow.ListSnippetsUI 160 | ) -------------------------------------------------------------------------------- /src/scripts/ui/interfaces/add-snippet.coffee: -------------------------------------------------------------------------------- 1 | 2 | class ContentFlow.AddSnippetUI extends ContentFlow.InterfaceUI 3 | 4 | # Present a list of snippet types to the user to select from in order to 5 | # add a new snippet to the content flow. 6 | 7 | constructor: () -> 8 | super('Add') 9 | 10 | # Add `cancel` tool to the header 11 | @_tools = { 12 | cancel: new ContentFlow.InlayToolUI('cancel', 'Cancel', true) 13 | } 14 | @_header.tools().attach(@_tools.cancel) 15 | 16 | # Handle interactions 17 | 18 | @_tools.cancel.addEventListener 'click', (ev) => 19 | ContentFlow.FlowMgr.get().loadInterface('list-snippets') 20 | 21 | init: () -> 22 | super() 23 | 24 | # Load the list of snippet types, then globals snippets for the user 25 | # to pick from. 26 | flowMgr = ContentFlow.FlowMgr.get() 27 | 28 | # Snippet types 29 | result = flowMgr.api().getSnippetTypes(flowMgr.flow()) 30 | result.addEventListener 'load', (ev) => 31 | flow = ContentFlow.FlowMgr.get().flow() 32 | 33 | # Unpack the response 34 | payload = JSON.parse(ev.target.responseText).payload 35 | 36 | # Remove existing snippets from the interface 37 | for child in @_body.children 38 | @_body.detach(child) 39 | 40 | # Add a list of snippet types to choose from 41 | @_local = new ContentFlow.InlaySectionUI('Local scope') 42 | for snippetTypeData in payload['snippet_types'] 43 | 44 | # Convert the snippet type to a model 45 | snippetType = ContentFlow.SnippetTypeModel.fromJSONType( 46 | flow, 47 | snippetTypeData 48 | ) 49 | 50 | # Add a snippet UI component for the snippet type 51 | uiSnippet = new ContentFlow.SnippetUI( 52 | snippetType.toSnippet(), 53 | 'pick' 54 | ) 55 | @_local.attach(uiSnippet) 56 | 57 | # Handle the user picking a snippet type 58 | uiSnippet.addEventListener 'pick', (ev) -> 59 | 60 | # Call the API to add the new snippet 61 | flowMgr = ContentFlow.FlowMgr.get() 62 | result = flowMgr.api().addSnippet( 63 | flowMgr.flow(), 64 | ev.detail().snippet.type 65 | ) 66 | result.addEventListener 'load', (ev) => 67 | flow = flowMgr.flow() 68 | 69 | # Unpack the response 70 | payload = JSON.parse(ev.target.responseText).payload 71 | 72 | # Insert the new snippets HTML into the page 73 | domSnippet = document.createElement('div') 74 | domSnippet.innerHTML = payload['html'] 75 | domSnippet = domSnippet.children[0] 76 | domFlow = ContentFlow.getFlowDOMelement(flow) 77 | domFlow.appendChild(domSnippet) 78 | 79 | # Show the list of snippets now in the flow. We re-sync 80 | # the page flows in case a new flow was added as part 81 | # of the snippet, then we force reselect this flow which 82 | # ensures the correct flow is selected and triggers a 83 | # list snippets interface load. 84 | flowMgr.syncFlows() 85 | flowMgr.flow(flow, force=true) 86 | 87 | @_body.attach(@_local) 88 | 89 | # Global snippets 90 | result = flowMgr.api().getGlobalSnippets(flowMgr.flow()) 91 | result.addEventListener 'load', (ev) => 92 | 93 | # Unpack the response 94 | payload = JSON.parse(ev.target.responseText).payload 95 | 96 | # Add a list of global snippets to choose from 97 | flow = ContentFlow.FlowMgr.get().flow() 98 | snippetCls = ContentFlow.getSnippetCls(flow) 99 | @_global = new ContentFlow.InlaySectionUI('Global scope') 100 | for snippetData in payload.snippets 101 | 102 | # Convert the global snippet data to a model 103 | snippet = snippetCls.fromJSONType(flow, snippetData) 104 | 105 | # Add a snippet UI component for the global snippet 106 | uiSnippet = new ContentFlow.SnippetUI(snippet, 'pick') 107 | @_global.attach(uiSnippet) 108 | 109 | # Handle the user picking a global snippet 110 | uiSnippet.addEventListener 'pick', (ev) -> 111 | 112 | # Call the API to add the new snippet 113 | flowMgr = ContentFlow.FlowMgr.get() 114 | result = flowMgr.api().addGlobalSnippet( 115 | flowMgr.flow(), 116 | ev.detail().snippet 117 | ) 118 | result.addEventListener 'load', (ev) => 119 | flow = flowMgr.flow() 120 | 121 | # Unpack the response 122 | payload = JSON.parse(ev.target.responseText).payload 123 | 124 | # Insert the new snippets HTML into the page 125 | domSnippet = document.createElement('div') 126 | domSnippet.innerHTML = payload['html'] 127 | domSnippet = domSnippet.children[0] 128 | domFlow = ContentFlow.getFlowDOMelement(flow) 129 | domFlow.appendChild(domSnippet) 130 | 131 | # Show the list of snippets now in the flow. We 132 | # re-sync the page flows in case a new flow was 133 | # added as part of the snippet, then we force 134 | # reselect this flow which ensures the correct flow 135 | # is selected and triggers a list snippets interface 136 | # load. 137 | flowMgr.syncFlows() 138 | flowMgr.flow(flow, force=true) 139 | 140 | if @_global.children().length > 0 141 | @_body.attach(@_global) 142 | 143 | # (Re)mount the body 144 | @_body.unmount() 145 | @_body.mount() 146 | 147 | 148 | # Register the interface with the content flow manager 149 | ContentFlow.FlowMgr.getCls().registerInterface( 150 | 'add-snippet', 151 | ContentFlow.AddSnippetUI 152 | ) -------------------------------------------------------------------------------- /src/scripts/ui/inlays.coffee: -------------------------------------------------------------------------------- 1 | 2 | class ContentFlow.InlayUI extends ContentTools.ComponentUI 3 | 4 | # An inlay panel (typically displayed within a DrawUI component) 5 | 6 | constructor: (heading) -> 7 | super() 8 | 9 | # Inlays are made up of a head and body section which we add below. 10 | 11 | # Attach the header component to the inlay 12 | @_header = new ContentFlow.InlayHeaderUI(heading) 13 | @attach(@_header) 14 | 15 | # Attach the body component to the inlay 16 | @_body = new ContentFlow.InlayBodyUI() 17 | @attach(@_body) 18 | 19 | # Read-only 20 | 21 | body: () -> 22 | return @_body 23 | 24 | header: () -> 25 | return @_header 26 | 27 | # Methods 28 | 29 | mount: () -> 30 | super() 31 | 32 | # Create the DOM element for the inlay and mount it 33 | @_domElement = @constructor.createDiv(['ct-inlay']) 34 | @parent().domElement().appendChild(@_domElement) 35 | @_addDOMEventListeners() 36 | 37 | # Mount the header and body components 38 | @_header.mount() 39 | @_body.mount() 40 | 41 | 42 | class ContentFlow.InlayBodyUI extends ContentTools.ComponentUI 43 | 44 | # The body component within an InlayUI component 45 | 46 | mount: () -> 47 | super() 48 | 49 | # Create the DOM element for the body and mount it 50 | @_domElement = @constructor.createDiv(['ct-inlay__body']) 51 | @parent().domElement().appendChild(@_domElement) 52 | @_addDOMEventListeners() 53 | 54 | # Mount children 55 | for child in @children() 56 | child.mount() 57 | 58 | 59 | class ContentFlow.InlayHeaderToolsUI extends ContentTools.ComponentUI 60 | 61 | # A tools component within an InlayHeaderUI that tools are mounted to 62 | 63 | mount: () -> 64 | super() 65 | 66 | # Create the DOM element for the tools and mount it 67 | @_domElement = @constructor.createDiv(['ct-inlay__tools']) 68 | @parent().domElement().appendChild(@_domElement) 69 | @_addDOMEventListeners() 70 | 71 | # Mount children 72 | for child in @children() 73 | child.mount() 74 | 75 | 76 | class ContentFlow.InlayHeaderUI extends ContentTools.ComponentUI 77 | 78 | # The header component within an InlayUI component 79 | 80 | constructor: (heading) -> 81 | super() 82 | 83 | # The heading text that will be displayed in the component 84 | @_heading = heading 85 | 86 | # Attach a tools component to allow tools to be mounted in the header 87 | @_tools = new ContentFlow.InlayHeaderToolsUI() 88 | @attach(@_tools) 89 | 90 | # Read-only 91 | 92 | tools: () -> 93 | return @_tools 94 | 95 | # Methods 96 | 97 | heading: (heading) -> 98 | # Get/set the heading for the header 99 | 100 | # If no heading value is provided return the current heading 101 | if heading is undefined 102 | return @_heading 103 | 104 | # Update the heading 105 | @_heading = heading 106 | if @isMounted() 107 | @_domHeading.textContent = ContentEdit._(heading) 108 | 109 | # Methods 110 | 111 | mount: () -> 112 | super() 113 | 114 | # Create the DOM elements for the header and heading and mount them 115 | @_domElement = @constructor.createDiv(['ct-inlay__header']) 116 | @_domHeading = @constructor.createDiv(['ct-inlay__heading']) 117 | @_domHeading.textContent = ContentEdit._(@_heading) 118 | @_domElement.appendChild(@_domHeading) 119 | @parent().domElement().appendChild(@_domElement) 120 | @_addDOMEventListeners() 121 | 122 | # Mount the tools 123 | @_tools.mount() 124 | 125 | unmount: () -> 126 | super() 127 | 128 | # Remove references to other elements 129 | this._domHeading = null 130 | 131 | 132 | class ContentFlow.InlayNoteUI extends ContentTools.ComponentUI 133 | 134 | # A note displayed within body or a section within the body of the InlayUI 135 | # component. 136 | 137 | constructor: (content) -> 138 | super() 139 | 140 | # The content of the note 141 | @_content = content 142 | 143 | # Methods 144 | 145 | content: (content) -> 146 | # Get/set the content for the note 147 | 148 | # If no content value is provided return the current content 149 | if content is undefined 150 | return @_content 151 | 152 | # Update the heading 153 | @_content = content 154 | if @isMounted() 155 | @_domElement.innerHTML = content 156 | 157 | mount: () -> 158 | super() 159 | 160 | # Create the DOM element for the note and mount it 161 | @_domElement = @constructor.createDiv(['ct-inlay-note']) 162 | @_domElement.innerHTML = @_content 163 | @parent().domElement().appendChild(@_domElement) 164 | @_addDOMEventListeners() 165 | 166 | 167 | class ContentFlow.InlaySectionUI extends ContentTools.ComponentUI 168 | 169 | # A section component with an InlayBodyUI component used to help separate 170 | # blocks of content into related sections. 171 | 172 | constructor: (heading) -> 173 | super() 174 | 175 | # The heading text that will be displayed in the component 176 | @_heading = heading 177 | 178 | # Methods 179 | 180 | heading: (heading) -> 181 | # Get/set the heading for the section 182 | 183 | # If no heading value is provided return the current heading 184 | if heading is undefined 185 | return @_heading 186 | 187 | # Update the heading 188 | @_heading = heading 189 | if @isMounted() 190 | @_domHeading.textContent = ContentEdit._(heading) 191 | 192 | mount: () -> 193 | super() 194 | 195 | # Create the DOM elements for the header and heading and mount them 196 | @_domElement = @constructor.createDiv(['ct-inlay-section']) 197 | @_domHeading = @constructor.createDiv(['ct-inlay-section__heading']) 198 | @_domHeading.textContent = ContentEdit._(@_heading) 199 | @_domElement.appendChild(@_domHeading) 200 | @parent().domElement().appendChild(@_domElement) 201 | @_addDOMEventListeners() 202 | 203 | # Mount children 204 | for child in @children() 205 | child.mount() 206 | 207 | unmount: () -> 208 | super() 209 | 210 | # Remove references to other elements 211 | this._domHeading = null 212 | 213 | 214 | class ContentFlow.InlayToolUI extends ContentTools.ComponentUI 215 | 216 | # A tool component mounted to the tools section of an InlayHeaderUI 217 | 218 | constructor: (toolName, tooltip) -> 219 | super() 220 | 221 | # The name of the tools (should be the same as the CSS modifier) 222 | @_toolName = toolName 223 | 224 | # The tooltip displayed for the tool 225 | @_tooltip = tooltip 226 | 227 | # Read-only 228 | 229 | toolName: () -> 230 | return @_toolName 231 | 232 | tooltip: () -> 233 | return @_tooltip 234 | 235 | # Methods 236 | 237 | mount: () -> 238 | super() 239 | 240 | # Create the DOM elements for the header and heading and mount them 241 | @_domElement = @constructor.createDiv([ 242 | 'ct-inlay__tool', 243 | 'ct-inlay-tool', 244 | "ct-inlay-tool--#{ @_toolName }" 245 | ]) 246 | @_domElement.setAttribute('data-ct-tooltip', ContentEdit._(@_tooltip)) 247 | @parent().domElement().appendChild(@_domElement) 248 | @_addDOMEventListeners() 249 | 250 | # Private methods 251 | 252 | _addDOMEventListeners: () -> 253 | super() 254 | 255 | # Click 256 | @_domElement.addEventListener 'click', (ev) => 257 | @dispatchEvent(@createEvent('click')) 258 | -------------------------------------------------------------------------------- /src/scripts/flow-mgr.coffee: -------------------------------------------------------------------------------- 1 | 2 | class _FlowMgr extends ContentTools.ComponentUI 3 | 4 | # The content flow manager 5 | 6 | # A map of UI interfaces the manager can load 7 | @_uiInterfaces = {} 8 | 9 | constructor: () -> 10 | super() 11 | 12 | # The API instance used by the manager 13 | @_api = null 14 | 15 | # The content flow currently being managed 16 | @_flow = null 17 | 18 | # The CSS query or list of DOM elements used to select the flows within 19 | # the page. 20 | @_flowQuery = null 21 | 22 | # Flag indicating if the app is currently open 23 | @_open = false 24 | 25 | # Attach draw, flows and toggle 26 | @_draw = new ContentFlow.DrawUI() 27 | @attach(@_draw) 28 | 29 | @_flows = new ContentFlow.FlowsUI() 30 | @_draw.attach(@_flows) 31 | 32 | @_toggle = new ContentFlow.ToggleUI() 33 | @attach(@_toggle) 34 | 35 | # Handle interactions 36 | @_flows.addEventListener 'select', (ev) => 37 | @flow(ev.detail().flow) 38 | 39 | init: (flowQuery='[data-cf-flow]', api=null) -> 40 | 41 | # Initialize the manager 42 | editor = ContentTools.EditorApp.get() 43 | 44 | # Set the API 45 | @_api = api or new ContentFlow.BaseAPI() 46 | 47 | # Handle toggling the manager open/closed 48 | @_toggle.addEventListener 'on', (ev) => 49 | @open() 50 | 51 | @_toggle.addEventListener 'off', (ev) => 52 | @close() 53 | 54 | # Hide the toggle when the editor is active and show it when not 55 | editor.addEventListener 'start', (ev) => 56 | @_toggle.hide() 57 | 58 | editor.addEventListener 'stopped', (ev) => 59 | @_toggle.show() 60 | 61 | # Sync the flows with the page 62 | @syncFlows(flowQuery) 63 | 64 | # Mount the manager within the DOM 65 | if @_domFlows.length > 0 66 | @mount() 67 | @_toggle.show() 68 | 69 | # Select the first flow found 70 | @flow(@_flows.flows()[0]) 71 | 72 | # Read-only 73 | 74 | api: () -> 75 | return @_api 76 | 77 | isOpen: () -> 78 | return @_open 79 | 80 | # Methods 81 | 82 | close: () -> 83 | # Close the content flow manager 84 | unless @dispatchEvent(@createEvent('close')) 85 | return 86 | 87 | # Remove flag that the flow manager is open from the body of the page 88 | document.body.classList.remove('cf--flow-mgr-open') 89 | 90 | # Close manager's UI 91 | @_draw.close() 92 | 93 | # Allow the editor to be started now the manager is closed 94 | editor = ContentTools.EditorApp.get() 95 | 96 | # Only show the editor tool if there are regions to edit 97 | editor.syncRegions() 98 | if editor.domRegions().length 99 | editor.ignition().show() 100 | 101 | flow: (flow, force=false) -> 102 | # Get/set the current content flow being managed 103 | 104 | # If no flow is provided return the current flow 105 | if flow is undefined 106 | return @_flow 107 | 108 | # If the flow hasn't changed there's nothing to do so return 109 | if not force and @_flow is flow 110 | return 111 | 112 | # Update the flow and load the list snippets interface 113 | @_flow = flow 114 | @_flows.select(flow) 115 | ContentFlow.FlowMgr.get().loadInterface('list-snippets') 116 | 117 | loadInterface: (name, args...) -> 118 | # Load an interface 119 | 120 | # Find the named interface 121 | uiInterface = new @constructor._uiInterfaces[name]() 122 | unless uiInterface 123 | return 124 | 125 | if uiInterface.safeToClose() 126 | @_toggle.enable() 127 | else 128 | @_toggle.disable() 129 | 130 | # Detatch the current interface 131 | if @_draw.children().length > 1 132 | child = @_draw.children()[1] 133 | child.unmount() 134 | @_draw.detach(child) 135 | 136 | # Attach and initialize the new interface 137 | @_draw.attach(uiInterface) 138 | uiInterface.mount() 139 | uiInterface.init(args...) 140 | 141 | mount: () -> 142 | # Mount the content flow manager 143 | @_domElement = @constructor.createDiv(['cf-flow-mgr']) 144 | document.body.insertBefore(@_domElement, null) 145 | @_addDOMEventListeners() 146 | 147 | # Mount children 148 | for child in @children() 149 | child.mount() 150 | 151 | open: () -> 152 | # Open the content flow manager 153 | unless @dispatchEvent(@createEvent('open')) 154 | return 155 | 156 | # Flag that the flow manager is open against the body of the page 157 | document.body.classList.add('cf--flow-mgr-open') 158 | 159 | # Prevent the CT editor from being started while the manager is open 160 | ContentTools.EditorApp.get().ignition().hide() 161 | 162 | # Open manager's UI 163 | @_draw.open() 164 | 165 | syncFlows: (flowQuery) -> 166 | # Synchronize the flows within the page 167 | 168 | # If a new query or set of DOM elements have been provided set them 169 | if flowQuery 170 | @_flowQuery = flowQuery 171 | 172 | # Select content flows within the page 173 | @_domFlows = [] 174 | if @_flowQuery 175 | 176 | # If a string is provided attempt to select the DOM flows using a 177 | # CSS selector. 178 | if typeof @_flowQuery == 'string' or 179 | @_flowQuery instanceof String 180 | @_domFlows = document.querySelectorAll(@_flowQuery) 181 | 182 | # Otherwise assume a valid list of DOM elements has been provided 183 | else 184 | @_domFlows = @_flowQuery 185 | 186 | # Sort the flows based on their position attribute 187 | @_domFlows = Array.from(@_domFlows) 188 | 189 | cmp = (a, b) -> 190 | aPos = parseFloat(a.dataset.cfFlowPosition) 191 | if isNaN(aPos) 192 | aPos = 999999 193 | bPos = parseFloat(b.dataset.cfFlowPosition) 194 | if isNaN(bPos) 195 | bPos = 999999 196 | return aPos - bPos 197 | 198 | @_domFlows.sort(cmp) 199 | 200 | # Convert the flows found in the DOM into models and populate the 201 | # flows UI compontent. 202 | flows = [] 203 | for domFlow in @_domFlows 204 | flows.push(new ContentFlow.FlowModel( 205 | ContentFlow.getFlowIdFromDOMElement(domFlow), 206 | ContentFlow.getFlowLabelFromDOMElement(domFlow) 207 | )) 208 | @_flows.flows(flows) 209 | 210 | unmount: () -> 211 | # Unmount the content flow manager 212 | unless @isMounted() 213 | return 214 | 215 | # Remove the manager 216 | @_domElement.parentNode.removeChild(@_domElement) 217 | 218 | # Remove any DOM event bindings 219 | @_removeDOMEventListeners() 220 | 221 | # Clear any child references 222 | @_draw = null 223 | @_flowSelect = null 224 | @_toggle = null 225 | 226 | # Class methods 227 | 228 | @registerInterface: (name, cls) -> 229 | # Register an interface with the manager 230 | @_uiInterfaces[name] = cls 231 | 232 | 233 | class ContentFlow.FlowMgr 234 | 235 | # The `ContentFlow.FlowMgr` class is a singleton, this code provides 236 | # access to the singleton instance of the protected `_FlowMgr` class 237 | # which is initialized the first time the class method `get` is called. 238 | 239 | # Storage for the singleton instance that will be created for the manager 240 | instance = null 241 | 242 | @get: () -> 243 | cls = ContentFlow.FlowMgr.getCls() 244 | instance ?= new cls() 245 | 246 | @getCls: () -> 247 | return _FlowMgr 248 | -------------------------------------------------------------------------------- /src/scripts/ui/fields.coffee: -------------------------------------------------------------------------------- 1 | 2 | class ContentFlow.FieldUI extends ContentTools.ComponentUI 3 | 4 | # A field (data entry) component 5 | 6 | constructor: (name, label, required, initialValue) -> 7 | super() 8 | 9 | # The name of the field (used as the parameter name when submitted as 10 | # a web request). 11 | @_name = name 12 | 13 | # The label displayed for the field 14 | @_label = label 15 | 16 | # A flag indicating if the field is required 17 | @_required = required 18 | 19 | # The initial value of the field 20 | @_initialValue = initialValue 21 | 22 | # Read-only 23 | 24 | name: () -> 25 | return @_name 26 | 27 | label: () -> 28 | return @_label 29 | 30 | required: () -> 31 | return @_required 32 | 33 | initialValue: () -> 34 | return @_initialValue 35 | 36 | # Methods 37 | 38 | errors: (errors) -> 39 | # Get/set the errors for the field 40 | 41 | # The value is managed via the input DOM element(s) and therefore the 42 | # component must be mounted for this function to be called. 43 | unless @isMounted() 44 | throw Error('Cannot set error for unmounted field') 45 | 46 | # If no errors are provided return the current errors 47 | if errors is undefined 48 | errors = [] 49 | for domError in @_domErrors.querySelector('.ct-field__error') 50 | errors.push(domError.textContent) 51 | return errors 52 | 53 | # Set the errors 54 | 55 | # Clear any existing errors 56 | @_domErrors.innerHTML = '' 57 | @_domErrors.classList.add('ct-field-errors__empty') 58 | 59 | if errors 60 | # Populate the errors DOM element 61 | for error in errors 62 | domError = @constructor.createDiv(['ct-field__error']) 63 | domError.textContent = ContentEdit._(error) 64 | @_domErrors.appendChild(domError) 65 | @_domErrors.classList.remove('ct-field-errors__empty') 66 | 67 | mount: () -> 68 | super() 69 | 70 | # Create the DOM element for the field 71 | @_domElement = @constructor.createDiv([ 72 | 'ct-field', 73 | if @_required then 'ct-field--required' else 'ct-field--optional' 74 | ]) 75 | 76 | # Create the DOM element for the field label and attach it 77 | @_domLabel = document.createElement('label') 78 | @_domLabel.classList.add('ct-field__label') 79 | @_domLabel.setAttribute('for', @_name) 80 | @_domLabel.textContent = ContentEdit._(@_label) 81 | @_domElement.appendChild(@_domLabel) 82 | 83 | # Create the DOM element for the field input 84 | @mount_input() 85 | 86 | # Create the DOM element for the field errors 87 | @_domErrors = @constructor.createDiv([ 88 | 'ct-field_errors', 89 | 'ct-field_errors__empty' 90 | ]) 91 | @_domElement.appendChild(@_domErrors) 92 | 93 | # Mount the field to the DOM 94 | @parent().domElement().appendChild(@_domElement) 95 | @_addDOMEventListeners() 96 | 97 | mount_input: () -> 98 | # Mount the input element for the field 99 | 100 | value: (value) -> 101 | # Get/set the value for the field 102 | 103 | # The value is managed via the input DOM element(s) and therefore the 104 | # component must be mounted for this function to be called. 105 | unless @isMounted() 106 | throw Error('Cannot set value for unmounted field') 107 | 108 | # If no heading value is provided return the current heading 109 | if value is undefined 110 | return @_domInput.value 111 | 112 | # Set the value 113 | @_domInput.value = value 114 | 115 | unmount: () -> 116 | super() 117 | 118 | # Remove references to other elements 119 | this._domErrors = null 120 | this._domInput = null 121 | this._domLabel = null 122 | 123 | # Class methods 124 | 125 | @fromJSONType: (jsonTypeData) -> 126 | # Convert a JSON type object to a field UI component 127 | 128 | switch jsonTypeData['type'] 129 | when 'boolean' 130 | return new ContentFlow.BooleanFieldUI( 131 | jsonTypeData['name'], 132 | jsonTypeData['label'], 133 | jsonTypeData['required'], 134 | jsonTypeData['value'] 135 | ) 136 | 137 | when 'select' 138 | return new ContentFlow.SelectFieldUI( 139 | jsonTypeData['name'], 140 | jsonTypeData['label'], 141 | jsonTypeData['required'], 142 | jsonTypeData['value'], 143 | jsonTypeData['choices'] 144 | ) 145 | 146 | return new ContentFlow.TextFieldUI( 147 | jsonTypeData['name'], 148 | jsonTypeData['label'], 149 | jsonTypeData['required'], 150 | jsonTypeData['value'] 151 | ) 152 | 153 | 154 | class ContentFlow.BooleanFieldUI extends ContentFlow.FieldUI 155 | 156 | # A boolean field component 157 | 158 | mount_input: () -> 159 | # Mount the input element for the field 160 | @_domInput = document.createElement('input') 161 | @_domInput.classList.add('ct-field__input') 162 | @_domInput.classList.add('ct-field__input--boolean') 163 | @_domInput.setAttribute('id', @_name) 164 | @_domInput.setAttribute('name', @_name) 165 | @_domInput.setAttribute('type', 'checkbox') 166 | if @_initialValue 167 | @_domInput.setAttribute('checked', true) 168 | @_domElement.appendChild(@_domInput) 169 | 170 | value: (value) -> 171 | # Get/set the value for the field 172 | 173 | # The value is managed via the input DOM element(s) and therefore the 174 | # component must be mounted for this function to be called. 175 | unless @isMounted() 176 | throw Error('Cannot set value for unmounted field') 177 | 178 | # If no heading value is provided return the current heading 179 | if value is undefined 180 | if @_domInput.checked 181 | return @name() 182 | return '' 183 | 184 | # Set the value 185 | @_domInput.removeAttribute('checked') 186 | if value 187 | @_domInput.setAttribute('checked', true) 188 | 189 | 190 | class ContentFlow.SelectFieldUI extends ContentFlow.FieldUI 191 | 192 | # A select field component 193 | 194 | constructor: (name, label, required, initialValue, choices) -> 195 | super(name, label, required, initialValue) 196 | 197 | # The choices for the select field 198 | @_choices = choices 199 | 200 | # Read-only 201 | 202 | choices: () -> 203 | return @choices 204 | 205 | # Methods 206 | 207 | mount_input: () -> 208 | # Add the select element 209 | @_domInput = document.createElement('select') 210 | @_domInput.classList.add('ct-field__input') 211 | @_domInput.classList.add('ct-field__input--select') 212 | @_domInput.setAttribute('id', @_name) 213 | @_domInput.setAttribute('name', @_name) 214 | 215 | # Add the choices 216 | for choice in @_choices 217 | domOption = document.createElement('option') 218 | domOption.setAttribute('value', choice[0]) 219 | domOption.textContent = ContentEdit._(choice[1]) 220 | if @_initialValue is choice[0] 221 | domOption.setAttribute('selected', true) 222 | @_domInput.appendChild(domOption) 223 | 224 | @_domElement.appendChild(@_domInput) 225 | 226 | 227 | class ContentFlow.TextFieldUI extends ContentFlow.FieldUI 228 | 229 | # A text input field component 230 | 231 | mount_input: () -> 232 | # Add the input element 233 | @_domInput = document.createElement('input') 234 | @_domInput.classList.add('ct-field__input') 235 | @_domInput.classList.add('ct-field__input--text') 236 | @_domInput.setAttribute('id', @_name) 237 | @_domInput.setAttribute('name', @_name) 238 | @_domInput.setAttribute('type', 'text') 239 | @_domInput.setAttribute( 240 | 'value', 241 | if @_initialValue then @_initialValue else '' 242 | ) 243 | @_domElement.appendChild(@_domInput) 244 | -------------------------------------------------------------------------------- /src/scripts/ui/interfaces/order-snippets.coffee: -------------------------------------------------------------------------------- 1 | 2 | class ContentFlow.OrderSnippetsUI extends ContentFlow.InterfaceUI 3 | 4 | # Display an orderable list of snippets in the content flow 5 | 6 | constructor: () -> 7 | super('Order') 8 | 9 | # Add `confirm` and `cancel` tools to the header 10 | @_tools = { 11 | confirm: new ContentFlow.InlayToolUI('confirm', 'Confirm', true), 12 | cancel: new ContentFlow.InlayToolUI('cancel', 'Cancel', true) 13 | } 14 | @_header.tools().attach(@_tools.confirm) 15 | @_header.tools().attach(@_tools.cancel) 16 | 17 | # Handle interactions 18 | 19 | # Sorting 20 | @_grabbed = null 21 | @_grabbedHelper = null 22 | @_grabbedOffset = null 23 | 24 | document.addEventListener('mousedown', @_grab) 25 | document.addEventListener('mousemove', @_drag) 26 | document.addEventListener('mouseup', @_drop) 27 | 28 | # Confirm 29 | @_tools.confirm.addEventListener 'click', (ev) => 30 | 31 | # Call the API to request the new order for the snippets within the 32 | # content flows. 33 | flowMgr = ContentFlow.FlowMgr.get() 34 | result = flowMgr.api().orderSnippets( 35 | flowMgr.flow(), 36 | @_newSnippetOrder 37 | ) 38 | result.addEventListener 'load', (ev) => 39 | ContentFlow.FlowMgr.get().loadInterface('list-snippets') 40 | 41 | # Cancel 42 | @_tools.cancel.addEventListener 'click', (ev) => 43 | @_orderSnippetsOnPage(@_originalSnippetOrder) 44 | ContentFlow.FlowMgr.get().loadInterface('list-snippets') 45 | 46 | init: () -> 47 | super() 48 | 49 | # Load the list of the snippets within the content flow 50 | flowMgr = ContentFlow.FlowMgr.get() 51 | result = flowMgr.api().getSnippets(flowMgr.flow()) 52 | result.addEventListener 'load', (ev) => 53 | 54 | # We'll store a table of snippets within the flow for easy 55 | # lookups, the original order so that we can restore the order 56 | # if the user cancels the action and the new order set by the user 57 | # so that we can save it. 58 | @_snippets = {} 59 | @_originalSnippetOrder = [] 60 | @_newSnippetOrder = [] 61 | 62 | # Unpack the response 63 | payload = JSON.parse(ev.target.responseText).payload 64 | 65 | # Remove existing snippets from the interface 66 | for child in @_body.children 67 | @_body.detach(child) 68 | 69 | # Add snippets 70 | flow = ContentFlow.FlowMgr.get().flow() 71 | snippetCls = ContentFlow.getSnippetCls(flow) 72 | for snippetData in payload.snippets 73 | snippet = snippetCls.fromJSONType(flow, snippetData) 74 | @_snippets[snippet.id] = snippet 75 | @_originalSnippetOrder.push(snippet) 76 | @_newSnippetOrder.push(snippet) 77 | @_body.attach(new ContentFlow.SnippetUI(snippet, 'order')) 78 | 79 | # (Re)mount the body 80 | @_body.unmount() 81 | @_body.mount() 82 | 83 | # Methods 84 | 85 | unmount: () -> 86 | super() 87 | 88 | # Remove document event listeners 89 | document.removeEventListener('mousedown', @_grab) 90 | document.removeEventListener('mousemove', @_drag) 91 | document.removeEventListener('mouseup', @_drop) 92 | 93 | # Private methods 94 | 95 | _drag: (ev) => 96 | # Handle snippet being dragged to a new position 97 | unless @_grabbed 98 | return 99 | 100 | # Get the position of the pointer 101 | pos = @_getEventPos(ev) 102 | 103 | # Move the helper inline with the pointer 104 | offset = [window.pageXOffset, window.pageYOffset] 105 | left = "#{offset[0] + pos[0] - @_grabbedOffset[0]}px" 106 | @_grabbedHelper.style.left = left 107 | top = "#{offset[1] + pos[1] - @_grabbedOffset[1]}px" 108 | @_grabbedHelper.style.top = top 109 | 110 | # Is the pointer over sibling of the grabbed snippet? 111 | target = document.elementFromPoint(pos[0], pos[1]) 112 | domSibling = null 113 | for child in @_body.children() 114 | domChild = child.domElement() 115 | 116 | # Ignore the currently grabbed snippet 117 | if domChild is @_grabbed 118 | continue 119 | 120 | if domChild.contains(target) 121 | domSibling = domChild 122 | break 123 | 124 | if not domSibling 125 | return 126 | 127 | # Move the grabbed snippet into its new position 128 | rect = domSibling.getBoundingClientRect() 129 | overlap = [pos[0] - rect.left, pos[1] - rect.top] 130 | @_body.domElement().removeChild(@_grabbed) 131 | if overlap[1] >= (rect.height / 2) 132 | domSibling = domSibling.nextElementSibling 133 | @_body.domElement().insertBefore(@_grabbed, domSibling) 134 | 135 | _drop: (ev) => 136 | # Handle snippet being dropped into a new position 137 | unless @_grabbed 138 | return 139 | 140 | # Remove the ghost class from the grabbed element 141 | @_grabbed.classList.remove('ct-snippet--ghost') 142 | @_grabbed = null 143 | @_grabbedOffset = null 144 | 145 | # Remove the helper element 146 | document.body.removeChild(@_grabbedHelper) 147 | @_grabbedHelper = null 148 | 149 | # Remove the sorting class from the container 150 | @_body.domElement().classList.remove('ct-inlay__body--sorting') 151 | 152 | # Update the order of the snippets 153 | @_newSnippetOrder = [] 154 | for domChild in @_body.domElement().childNodes 155 | unless domChild.nodeType is 1 # (Node.ELEMENT_NODE) 156 | continue 157 | 158 | id = domChild.dataset.snippetId 159 | @_newSnippetOrder.push(@_snippets[id]) 160 | 161 | @_orderSnippetsOnPage(@_newSnippetOrder) 162 | 163 | _getEventPos: (ev) -> 164 | # Return the `[x, y]` coordinates for an event 165 | return [ev.pageX - window.pageXOffset, ev.pageY - window.pageYOffset] 166 | 167 | _grab: (ev) => 168 | # Handle the grabbing of a snippet to sort 169 | 170 | # If this is a mouse down event then we check that the user pressed the 171 | # primary mouse button (left). 172 | if ev.type.toLowerCase() is 'mousedown' and not (ev.which is 1) 173 | return 174 | 175 | # Determine if the target of the event relates to the grabber for a 176 | # sortable child. 177 | grabbed = null 178 | for child in @_body.children() 179 | domChild = child.domElement() 180 | if domChild.contains(ev.target) 181 | grabbed = domChild 182 | break 183 | 184 | unless grabbed 185 | return 186 | 187 | # Store the grabbed snippet element 188 | @_grabbed = grabbed 189 | 190 | # Get x, y for event origin 191 | pos = @_getEventPos(ev) 192 | 193 | # Store the offset at which we grabbed the snippet 194 | rect = @_grabbed.getBoundingClientRect() 195 | @_grabbedOffset = [pos[0] - rect.left, pos[1] - rect.top] 196 | 197 | # Create a helper to represent the grabbed child being dragged 198 | @_grabbedHelper = grabbed.cloneNode(true) 199 | 200 | # Copy CSS styles 201 | css = document.defaultView.getComputedStyle(grabbed, '').cssText 202 | @_grabbedHelper.style.cssText = css 203 | 204 | # Set the position of the helper element to be absolute 205 | @_grabbedHelper.style.position = 'absolute' 206 | 207 | # Prevent the capture of pointer events 208 | @_grabbedHelper.style['pointer-events'] = 'none' 209 | 210 | # Move the helper inline with the pointer 211 | @_grabbedHelper.style.left = "#{pos[0] - @_grabbedOffset[0]}px" 212 | @_grabbedHelper.style.top = "#{pos[1] - @_grabbedOffset[1]}px" 213 | 214 | # Add a helper class to the clone 215 | @_grabbedHelper.classList.add('ct-snippet--helper') 216 | 217 | # Add the helper 218 | document.body.appendChild(@_grabbedHelper) 219 | 220 | # Add the ghost class to the grabbed child to change its appearance 221 | # within the list. 222 | @_grabbed.classList.add('ct-snippet--ghost') 223 | 224 | # Add a class to the container to indicate that the user is sorting 225 | # the list. 226 | @_body.domElement().classList.add('ct-inlay__body--sorting') 227 | 228 | _orderSnippetsOnPage: (snippets) -> 229 | # Set the DOM elements represented as snippets within the page to the 230 | # order of the list of snippets provided. 231 | flow = ContentFlow.FlowMgr.get().flow() 232 | 233 | for snippet, i in snippets 234 | if i is 0 235 | continue 236 | 237 | domLastSnippet = ContentFlow.getSnippetDOMElement( 238 | flow, 239 | snippets[i - 1] 240 | ) 241 | domSnippet = ContentFlow.getSnippetDOMElement(flow, snippet) 242 | 243 | # Check we need to move the snippet 244 | if domLastSnippet.nextSibling is domSnippet 245 | continue 246 | 247 | # Move the snippet 248 | domSnippet.parentNode.removeChild(domSnippet) 249 | domLastSnippet.parentNode.insertBefore( 250 | domSnippet, 251 | domLastSnippet.nextSibling 252 | ) 253 | 254 | 255 | # Register the interface with the content flow manager 256 | ContentFlow.FlowMgr.getCls().registerInterface( 257 | 'order-snippets', 258 | ContentFlow.OrderSnippetsUI 259 | ) -------------------------------------------------------------------------------- /sandbox/sandbox.js: -------------------------------------------------------------------------------- 1 | (function() { 2 | var MockAPI, MockRequest, 3 | __hasProp = {}.hasOwnProperty, 4 | __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; 5 | 6 | MockRequest = (function() { 7 | function MockRequest(responseText) { 8 | var mockLoad; 9 | this._responseText = responseText; 10 | this._listener = null; 11 | mockLoad = (function(_this) { 12 | return function() { 13 | if (_this._listener) { 14 | return _this._listener({ 15 | target: { 16 | responseText: responseText 17 | } 18 | }); 19 | } 20 | }; 21 | })(this); 22 | setTimeout(mockLoad, 0); 23 | } 24 | 25 | MockRequest.prototype.addEventListener = function(eventType, listener) { 26 | return this._listener = listener; 27 | }; 28 | 29 | return MockRequest; 30 | 31 | })(); 32 | 33 | MockAPI = (function(_super) { 34 | __extends(MockAPI, _super); 35 | 36 | MockAPI._autoInc = 0; 37 | 38 | function MockAPI(baseURL, baseParams) { 39 | if (baseURL == null) { 40 | baseURL = '/'; 41 | } 42 | if (baseParams == null) { 43 | baseParams = {}; 44 | } 45 | MockAPI.__super__.constructor.call(this, baseURL = '/', baseParams = {}); 46 | this._snippetTypes = { 47 | 'article-body': [ 48 | { 49 | 'id': 'basic', 50 | 'label': 'Basic' 51 | }, { 52 | 'id': 'advanced', 53 | 'label': 'Advanced' 54 | } 55 | ], 56 | 'article-related': [ 57 | { 58 | 'id': 'basic', 59 | 'label': 'Basic' 60 | }, { 61 | 'id': 'archive', 62 | 'label': 'Archive' 63 | } 64 | ] 65 | }; 66 | this._snippets = { 67 | 'article-body': [ 68 | { 69 | 'id': this.constructor._getId(), 70 | 'type': this._snippetTypes['article-body'][0], 71 | 'scope': 'local', 72 | 'settings': {} 73 | }, { 74 | 'id': this.constructor._getId(), 75 | 'type': this._snippetTypes['article-body'][1], 76 | 'scope': 'local', 77 | 'settings': {} 78 | } 79 | ], 80 | 'article-related': [ 81 | { 82 | 'id': this.constructor._getId(), 83 | 'type': this._snippetTypes['article-related'][1], 84 | 'scope': 'local', 85 | 'settings': {} 86 | }, { 87 | 'id': this.constructor._getId(), 88 | 'type': this._snippetTypes['article-related'][0], 89 | 'scope': 'local', 90 | 'settings': {} 91 | } 92 | ] 93 | }; 94 | this._globalSnippets = { 95 | 'article-body': [ 96 | { 97 | 'id': this.constructor._getId(), 98 | 'type': this._snippetTypes['article-body'][0], 99 | 'scope': 'global', 100 | 'settings': {}, 101 | 'global_id': this.constructor._getId(), 102 | 'label': 'Client logos' 103 | } 104 | ], 105 | 'article-related': [] 106 | }; 107 | } 108 | 109 | MockAPI._getId = function() { 110 | this._autoInc += 1; 111 | return this._autoInc; 112 | }; 113 | 114 | MockAPI.prototype._callEndpoint = function(method, endpoint, params) { 115 | var fields, globalId, globalSnippet, id, newSnippets, otherSnippet, snippet, snippetType, snippets, _i, _j, _k, _l, _len, _len1, _len2, _len3, _len4, _len5, _m, _n, _ref, _ref1, _ref2, _ref3, _ref4; 116 | if (params == null) { 117 | params = {}; 118 | } 119 | switch (endpoint) { 120 | case 'add-snippet': 121 | snippetType = null; 122 | _ref = this._snippetTypes[params['flow']]; 123 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 124 | snippetType = _ref[_i]; 125 | if (snippetType.id === params['snippet_type']) { 126 | break; 127 | } 128 | } 129 | snippet = { 130 | 'id': this.constructor._getId(), 131 | 'type': snippetType, 132 | 'scope': 'local', 133 | 'settings': {} 134 | }; 135 | this._snippets[params['flow']].push(snippet); 136 | return this._mockResponse({ 137 | 'html': "
\n

This is a new snippet

\n \n \n
" 138 | }); 139 | case 'add-global-snippet': 140 | globalSnippet = null; 141 | _ref1 = this._globalSnippets[params['flow']]; 142 | for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { 143 | globalSnippet = _ref1[_j]; 144 | if (globalSnippet.global_id === params['global_snippet']) { 145 | break; 146 | } 147 | } 148 | snippet = { 149 | 'id': this.constructor._getId(), 150 | 'type': globalSnippet.type, 151 | 'scope': globalSnippet.scope, 152 | 'settings': globalSnippet.settings, 153 | 'global_id': globalSnippet.id, 154 | 'label': globalSnippet.label 155 | }; 156 | this._snippets[params['flow']].push(snippet); 157 | return this._mockResponse({ 158 | 'html': "
\n

This is a global snippet: " + snippet.label + "

\n
" 159 | }); 160 | case 'delete-snippet': 161 | snippets = this._snippets[params['flow']]; 162 | newSnippets = []; 163 | for (_k = 0, _len2 = snippets.length; _k < _len2; _k++) { 164 | snippet = snippets[_k]; 165 | if (snippet.id !== params['snippet']) { 166 | newSnippets.push(snippet); 167 | } 168 | } 169 | this._snippets[params['flow']] = newSnippets; 170 | return this._mockResponse(); 171 | case 'global-snippets': 172 | return this._mockResponse({ 173 | 'snippets': this._globalSnippets[params['flow']] 174 | }); 175 | case 'order-snippets': 176 | snippets = {}; 177 | _ref2 = this._snippets[params['flow']]; 178 | for (_l = 0, _len3 = _ref2.length; _l < _len3; _l++) { 179 | snippet = _ref2[_l]; 180 | snippets[snippet.id] = snippet; 181 | } 182 | newSnippets = []; 183 | _ref3 = params['snippets']; 184 | for (_m = 0, _len4 = _ref3.length; _m < _len4; _m++) { 185 | id = _ref3[_m]; 186 | newSnippets.push(snippets[id]); 187 | } 188 | this._snippets[params['flow']] = newSnippets; 189 | return this._mockResponse(); 190 | case 'snippets': 191 | return this._mockResponse({ 192 | 'snippets': this._snippets[params['flow']] 193 | }); 194 | case 'change-snippet-scope': 195 | snippet = null; 196 | _ref4 = this._snippets[params['flow']]; 197 | for (_n = 0, _len5 = _ref4.length; _n < _len5; _n++) { 198 | otherSnippet = _ref4[_n]; 199 | if (otherSnippet.id === params['snippet']) { 200 | snippet = otherSnippet; 201 | break; 202 | } 203 | } 204 | if (params['scope'] === 'local') { 205 | snippet.scope = 'local'; 206 | delete snippet.global_id; 207 | delete snippet.label; 208 | return this._mockResponse(); 209 | } else { 210 | if (!params['label']) { 211 | return this._mockError({ 212 | 'label': 'This field is required' 213 | }); 214 | } 215 | globalId = this.constructor._getId(); 216 | this._globalSnippets[params['flow']].push({ 217 | 'id': this.constructor._getId(), 218 | 'type': snippet.type, 219 | 'scope': 'global', 220 | 'settings': snippet.settigns, 221 | 'global_id': globalId, 222 | 'label': params['label'] 223 | }); 224 | snippet.scope = 'global'; 225 | snippet.global_id = globalId; 226 | snippet.label = params['label']; 227 | return this._mockResponse(); 228 | } 229 | break; 230 | case 'update-snippet-settings': 231 | if (method.toLowerCase() === 'get') { 232 | fields = [ 233 | { 234 | 'type': 'boolean', 235 | 'name': 'boolean_example', 236 | 'label': 'Boolean example', 237 | 'required': false, 238 | 'value': true 239 | }, { 240 | 'type': 'select', 241 | 'name': 'select_example', 242 | 'label': 'Select example', 243 | 'required': true, 244 | 'value': 1, 245 | 'choices': [[1, 'One'], [2, 'Two'], [3, 'Three']] 246 | }, { 247 | 'type': 'text', 248 | 'name': 'Text_example', 249 | 'label': 'Text example', 250 | 'required': true, 251 | 'value': 'foo' 252 | } 253 | ]; 254 | return this._mockResponse({ 255 | 'fields': fields 256 | }); 257 | } else { 258 | 259 | } 260 | return this._mockResponse({ 261 | 'html': "
\n

This is a snippet with updated settings

\n
" 262 | }); 263 | case 'snippet-types': 264 | return this._mockResponse({ 265 | 'snippet_types': this._snippetTypes[params['flow']] 266 | }); 267 | } 268 | }; 269 | 270 | MockAPI.prototype._mockError = function(errors) { 271 | var response; 272 | response = { 273 | 'status': 'fail' 274 | }; 275 | if (errors) { 276 | response['errors'] = errors; 277 | } 278 | return new MockRequest(JSON.stringify(response)); 279 | }; 280 | 281 | MockAPI.prototype._mockResponse = function(payload) { 282 | var response; 283 | response = { 284 | 'status': 'success' 285 | }; 286 | if (payload) { 287 | response['payload'] = payload; 288 | } 289 | return new MockRequest(JSON.stringify(response)); 290 | }; 291 | 292 | return MockAPI; 293 | 294 | })(ContentFlow.BaseAPI); 295 | 296 | window.addEventListener('load', function() { 297 | var api, editor, flowMgr, flowsQuery; 298 | editor = ContentTools.EditorApp.get(); 299 | flowMgr = ContentFlow.FlowMgr.get(); 300 | editor.init('[data-cf-snippet], [data-fixture]', 'data-cf-snippet'); 301 | return flowMgr.init(flowsQuery = '[data-cf-flow]', api = new MockAPI()); 302 | }); 303 | 304 | }).call(this); 305 | -------------------------------------------------------------------------------- /src/sandbox/api.coffee: -------------------------------------------------------------------------------- 1 | 2 | # Mock API classes 3 | 4 | class MockRequest 5 | 6 | constructor: (responseText) -> 7 | # The response to the request 8 | @_responseText = responseText 9 | 10 | # The listener to call with the response 11 | @_listener = null 12 | 13 | # Create a mock load event 14 | mockLoad = () => 15 | if @_listener 16 | @_listener({target: {responseText: responseText}}) 17 | 18 | setTimeout(mockLoad, 0) 19 | 20 | addEventListener: (eventType, listener) -> 21 | # Fake handler for binding event listeners to the request (only 22 | # supports 'load' event). 23 | @_listener = listener 24 | 25 | 26 | class MockAPI extends ContentFlow.BaseAPI 27 | 28 | # A mock API that returns results than can be tested with 29 | 30 | @_autoInc = 0 31 | 32 | constructor: (baseURL='/', baseParams={}) -> 33 | super(baseURL='/', baseParams={}) 34 | 35 | # A list of snippet types available to the flow 36 | @_snippetTypes = { 37 | 'article-body': [ 38 | { 39 | 'id': 'basic', 40 | 'label': 'Basic' 41 | }, { 42 | 'id': 'advanced', 43 | 'label': 'Advanced' 44 | } 45 | ], 46 | 'article-related': [ 47 | { 48 | 'id': 'basic', 49 | 'label': 'Basic' 50 | }, { 51 | 'id': 'archive', 52 | 'label': 'Archive' 53 | } 54 | ] 55 | } 56 | 57 | # A list of snippets in the flow 58 | @_snippets = { 59 | 'article-body': [ 60 | { 61 | 'id': @constructor._getId(), 62 | 'type': @_snippetTypes['article-body'][0], 63 | 'scope': 'local', 64 | 'settings': {} 65 | }, { 66 | 'id': @constructor._getId(), 67 | 'type': @_snippetTypes['article-body'][1], 68 | 'scope': 'local', 69 | 'settings': {} 70 | } 71 | ], 72 | 'article-related': [ 73 | { 74 | 'id': @constructor._getId(), 75 | 'type': @_snippetTypes['article-related'][1], 76 | 'scope': 'local', 77 | 'settings': {} 78 | }, { 79 | 'id': @constructor._getId(), 80 | 'type': @_snippetTypes['article-related'][0], 81 | 'scope': 'local', 82 | 'settings': {} 83 | } 84 | ] 85 | } 86 | 87 | # A list of globals snippets available to the flow 88 | @_globalSnippets = { 89 | 'article-body': [ 90 | { 91 | 'id': @constructor._getId(), 92 | 'type': @_snippetTypes['article-body'][0], 93 | 'scope': 'global', 94 | 'settings': {}, 95 | 'global_id': @constructor._getId(), 96 | 'label': 'Client logos' 97 | } 98 | ], 99 | 'article-related': [] 100 | } 101 | 102 | # Class methods 103 | 104 | @_getId: () -> 105 | # Return a unique ID 106 | @_autoInc += 1 107 | return @_autoInc 108 | 109 | # Private methods 110 | 111 | _callEndpoint: (method, endpoint, params={}) -> 112 | # Fake the response of calling a real API 113 | switch endpoint 114 | 115 | when 'add-snippet' 116 | 117 | # Find the snippet type 118 | snippetType = null 119 | for snippetType in @_snippetTypes[params['flow']] 120 | if snippetType.id is params['snippet_type'] 121 | break 122 | 123 | # Add a new snippet to the state 124 | snippet = { 125 | 'id': @constructor._getId(), 126 | 'type': snippetType, 127 | 'scope': 'local', 128 | 'settings': {} 129 | } 130 | @_snippets[params['flow']].push(snippet) 131 | 132 | # Generate some HTML for the snippet and return it 133 | return @_mockResponse({ 134 | 'html': """ 135 |
136 |

This is a new snippet

137 |
141 |
142 |
143 | """ 144 | }) 145 | 146 | when 'add-global-snippet' 147 | 148 | # Find the global snippet 149 | globalSnippet = null 150 | for globalSnippet in @_globalSnippets[params['flow']] 151 | if globalSnippet.global_id is params['global_snippet'] 152 | break 153 | 154 | # Add a new snippet to the state 155 | snippet = { 156 | 'id': @constructor._getId(), 157 | 'type': globalSnippet.type, 158 | 'scope': globalSnippet.scope, 159 | 'settings': globalSnippet.settings, 160 | 'global_id': globalSnippet.id, 161 | 'label': globalSnippet.label 162 | } 163 | @_snippets[params['flow']].push(snippet) 164 | 165 | # Generate some HTML for the snippet and return it 166 | return @_mockResponse({ 167 | 'html': """ 168 |
169 |

This is a global snippet: #{ snippet.label }

170 |
171 | """ 172 | }) 173 | 174 | when 'delete-snippet' 175 | # Remove the snippet from the flow 176 | snippets = @_snippets[params['flow']] 177 | newSnippets = [] 178 | for snippet in snippets 179 | unless snippet.id is params['snippet'] 180 | newSnippets.push(snippet) 181 | @_snippets[params['flow']] = newSnippets 182 | 183 | return @_mockResponse() 184 | 185 | when 'global-snippets' 186 | return @_mockResponse({ 187 | 'snippets': @_globalSnippets[params['flow']] 188 | }) 189 | 190 | when 'order-snippets' 191 | # Build a look up table for snippets by Id 192 | snippets = {} 193 | for snippet in @_snippets[params['flow']] 194 | snippets[snippet.id] = snippet 195 | 196 | # Order the snippets 197 | newSnippets = [] 198 | for id in params['snippets'] 199 | newSnippets.push(snippets[id]) 200 | @_snippets[params['flow']] = newSnippets 201 | 202 | return @_mockResponse() 203 | 204 | when 'snippets' 205 | return @_mockResponse({'snippets': @_snippets[params['flow']]}) 206 | 207 | when 'change-snippet-scope' 208 | # Find the snippet 209 | snippet = null 210 | for otherSnippet in @_snippets[params['flow']] 211 | if otherSnippet.id is params['snippet'] 212 | snippet = otherSnippet 213 | break 214 | 215 | if params['scope'] is 'local' 216 | # Make the snippet local 217 | snippet.scope = 'local' 218 | delete snippet.global_id 219 | delete snippet.label 220 | return @_mockResponse() 221 | 222 | else 223 | # Make the snippet global 224 | 225 | # Validate a label has been provided 226 | unless params['label'] 227 | return @_mockError({'label': 'This field is required'}) 228 | 229 | # Add the snippet to the global snippets for the flow 230 | globalId = @constructor._getId() 231 | @_globalSnippets[params['flow']].push({ 232 | 'id': @constructor._getId(), 233 | 'type': snippet.type, 234 | 'scope': 'global', 235 | 'settings': snippet.settigns, 236 | 'global_id': globalId, 237 | 'label': params['label'] 238 | }) 239 | 240 | # Convert the snippet to a global snippet 241 | snippet.scope = 'global' 242 | snippet.global_id = globalId 243 | snippet.label = params['label'] 244 | 245 | return @_mockResponse() 246 | 247 | when 'update-snippet-settings' 248 | if method.toLowerCase() is 'get' 249 | 250 | # Build a dummy set of fields for the response 251 | fields = [ 252 | { 253 | 'type': 'boolean', 254 | 'name': 'boolean_example', 255 | 'label': 'Boolean example', 256 | 'required': false, 257 | 'value': true 258 | }, { 259 | 'type': 'select', 260 | 'name': 'select_example', 261 | 'label': 'Select example', 262 | 'required': true, 263 | 'value': 1, 264 | 'choices': [ 265 | [1, 'One'], 266 | [2, 'Two'], 267 | [3, 'Three'], 268 | ] 269 | }, { 270 | 'type': 'text', 271 | 'name': 'Text_example', 272 | 'label': 'Text example', 273 | 'required': true, 274 | 'value': 'foo' 275 | }, 276 | ] 277 | return @_mockResponse({'fields': fields}) 278 | 279 | else 280 | return @_mockResponse({ 281 | 'html': """ 282 |
283 |

This is a snippet with updated settings

284 |
285 | """ 286 | }) 287 | 288 | when 'snippet-types' 289 | return @_mockResponse({ 290 | 'snippet_types': @_snippetTypes[params['flow']] 291 | }) 292 | 293 | _mockError: (errors) -> 294 | # Shortcut for building a mock failed API request/response 295 | response = {'status': 'fail'} 296 | if errors 297 | response['errors'] = errors 298 | return new MockRequest(JSON.stringify(response)) 299 | 300 | _mockResponse: (payload) -> 301 | # Shortcut for building a mock successful API request/response 302 | response = {'status': 'success'} 303 | if payload 304 | response['payload'] = payload 305 | return new MockRequest(JSON.stringify(response)) -------------------------------------------------------------------------------- /externals/selection.json: -------------------------------------------------------------------------------- 1 | { 2 | "IcoMoonType": "selection", 3 | "icons": [ 4 | { 5 | "icon": { 6 | "paths": [ 7 | "M864 0c88.364 0 160 71.634 160 160 0 36.020-11.91 69.258-32 96l-64 64-224-224 64-64c26.742-20.090 59.978-32 96-32zM64 736l-64 288 288-64 592-592-224-224-592 592zM715.578 363.578l-448 448-55.156-55.156 448-448 55.156 55.156z" 8 | ], 9 | "attrs": [], 10 | "isMulticolor": false, 11 | "isMulticolor2": false, 12 | "tags": [ 13 | "pencil", 14 | "write", 15 | "edit" 16 | ], 17 | "defaultCode": 57357, 18 | "grid": 16 19 | }, 20 | "attrs": [], 21 | "properties": { 22 | "id": 5, 23 | "order": 15, 24 | "prevSize": 16, 25 | "code": 59653, 26 | "ligatures": "pencil, write", 27 | "name": "pencil" 28 | }, 29 | "setIdx": 0, 30 | "setId": 4, 31 | "iconIdx": 5 32 | }, 33 | { 34 | "icon": { 35 | "paths": [ 36 | "M864.626 473.162c-65.754-183.44-205.11-348.15-352.626-473.162-147.516 125.012-286.87 289.722-352.626 473.162-40.664 113.436-44.682 236.562 12.584 345.4 65.846 125.14 198.632 205.438 340.042 205.438s274.196-80.298 340.040-205.44c57.27-108.838 53.25-231.962 12.586-345.398zM738.764 758.956c-43.802 83.252-132.812 137.044-226.764 137.044-55.12 0-108.524-18.536-152.112-50.652 13.242 1.724 26.632 2.652 40.112 2.652 117.426 0 228.668-67.214 283.402-171.242 44.878-85.292 40.978-173.848 23.882-244.338 14.558 28.15 26.906 56.198 36.848 83.932 22.606 63.062 40.024 156.34-5.368 242.604z" 37 | ], 38 | "attrs": [], 39 | "isMulticolor": false, 40 | "isMulticolor2": false, 41 | "tags": [ 42 | "droplet", 43 | "color", 44 | "water" 45 | ], 46 | "defaultCode": 57381, 47 | "grid": 16 48 | }, 49 | "attrs": [], 50 | "properties": { 51 | "id": 11, 52 | "order": 23, 53 | "prevSize": 16, 54 | "code": 59659, 55 | "ligatures": "droplet, color2", 56 | "name": "droplet" 57 | }, 58 | "setIdx": 0, 59 | "setId": 4, 60 | "iconIdx": 11 61 | }, 62 | { 63 | "icon": { 64 | "paths": [ 65 | "M959.884 128c0.040 0.034 0.082 0.076 0.116 0.116v767.77c-0.034 0.040-0.076 0.082-0.116 0.116h-895.77c-0.040-0.034-0.082-0.076-0.114-0.116v-767.772c0.034-0.040 0.076-0.082 0.114-0.114h895.77zM960 64h-896c-35.2 0-64 28.8-64 64v768c0 35.2 28.8 64 64 64h896c35.2 0 64-28.8 64-64v-768c0-35.2-28.8-64-64-64v0z", 66 | "M832 288c0 53.020-42.98 96-96 96s-96-42.98-96-96 42.98-96 96-96 96 42.98 96 96z", 67 | "M896 832h-768v-128l224-384 256 320h64l224-192z" 68 | ], 69 | "attrs": [], 70 | "isMulticolor": false, 71 | "isMulticolor2": false, 72 | "tags": [ 73 | "image", 74 | "picture", 75 | "photo", 76 | "graphic" 77 | ], 78 | "defaultCode": 57388, 79 | "grid": 16 80 | }, 81 | "attrs": [], 82 | "properties": { 83 | "order": 1, 84 | "id": 13, 85 | "prevSize": 16, 86 | "code": 59661, 87 | "ligatures": "image, picture", 88 | "name": "image" 89 | }, 90 | "setIdx": 0, 91 | "setId": 4, 92 | "iconIdx": 13 93 | }, 94 | { 95 | "icon": { 96 | "paths": [ 97 | "M658.744 749.256l-210.744-210.746v-282.51h128v229.49l173.256 173.254zM512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM512 896c-212.078 0-384-171.922-384-384s171.922-384 384-384c212.078 0 384 171.922 384 384s-171.922 384-384 384z" 98 | ], 99 | "attrs": [], 100 | "isMulticolor": false, 101 | "isMulticolor2": false, 102 | "tags": [ 103 | "clock", 104 | "time", 105 | "schedule" 106 | ], 107 | "defaultCode": 57601, 108 | "grid": 16 109 | }, 110 | "attrs": [], 111 | "properties": { 112 | "id": 78, 113 | "order": 35, 114 | "prevSize": 16, 115 | "code": 59726, 116 | "ligatures": "clock, time2", 117 | "name": "clock" 118 | }, 119 | "setIdx": 0, 120 | "setId": 4, 121 | "iconIdx": 78 122 | }, 123 | { 124 | "icon": { 125 | "paths": [ 126 | "M512 64c-141.384 0-269.376 57.32-362.032 149.978l-149.968-149.978v384h384l-143.532-143.522c69.496-69.492 165.492-112.478 271.532-112.478 212.068 0 384 171.924 384 384 0 114.696-50.292 217.636-130.018 288l84.666 96c106.302-93.816 173.352-231.076 173.352-384 0-282.77-229.23-512-512-512z" 127 | ], 128 | "attrs": [], 129 | "isMulticolor": false, 130 | "isMulticolor2": false, 131 | "tags": [ 132 | "undo", 133 | "ccw", 134 | "arrow" 135 | ], 136 | "defaultCode": 57659, 137 | "grid": 16 138 | }, 139 | "attrs": [], 140 | "properties": { 141 | "id": 101, 142 | "order": 16, 143 | "prevSize": 16, 144 | "code": 59749, 145 | "ligatures": "undo, ccw", 146 | "name": "undo" 147 | }, 148 | "setIdx": 0, 149 | "setId": 4, 150 | "iconIdx": 101 151 | }, 152 | { 153 | "icon": { 154 | "paths": [ 155 | "M0 576c0 152.924 67.048 290.184 173.35 384l84.666-96c-79.726-70.364-130.016-173.304-130.016-288 0-212.076 171.93-384 384-384 106.042 0 202.038 42.986 271.53 112.478l-143.53 143.522h384v-384l-149.97 149.978c-92.654-92.658-220.644-149.978-362.030-149.978-282.77 0-512 229.23-512 512z" 156 | ], 157 | "attrs": [], 158 | "isMulticolor": false, 159 | "isMulticolor2": false, 160 | "tags": [ 161 | "redo", 162 | "cw", 163 | "arrow" 164 | ], 165 | "defaultCode": 57660, 166 | "grid": 16 167 | }, 168 | "attrs": [], 169 | "properties": { 170 | "id": 102, 171 | "order": 17, 172 | "prevSize": 16, 173 | "code": 59750, 174 | "ligatures": "redo, cw", 175 | "name": "redo" 176 | }, 177 | "setIdx": 0, 178 | "setId": 4, 179 | "iconIdx": 102 180 | }, 181 | { 182 | "icon": { 183 | "paths": [ 184 | "M896 448h16c26.4 0 48-21.6 48-48v-160c0-26.4-21.6-48-48-48h-16v-192h-128v192h-16c-26.4 0-48 21.6-48 48v160c0 26.4 21.6 48 48 48h16v576h128v-576zM768 256h128v128h-128v-128zM592 832c26.4 0 48-21.6 48-48v-160c0-26.4-21.6-48-48-48h-16v-576h-128v576h-16c-26.4 0-48 21.6-48 48v160c0 26.4 21.6 48 48 48h16v192h128v-192h16zM448 640h128v128h-128v-128zM272 448c26.4 0 48-21.6 48-48v-160c0-26.4-21.6-48-48-48h-16v-192h-128v192h-16c-26.4 0-48 21.6-48 48v160c0 26.4 21.6 48 48 48h16v576h128v-576h16zM128 256h128v128h-128v-128z" 185 | ], 186 | "attrs": [], 187 | "isMulticolor": false, 188 | "isMulticolor2": false, 189 | "tags": [ 190 | "equalizer", 191 | "sliders", 192 | "settings", 193 | "preferences", 194 | "dashboard", 195 | "control" 196 | ], 197 | "defaultCode": 57820, 198 | "grid": 16 199 | }, 200 | "attrs": [], 201 | "properties": { 202 | "id": 147, 203 | "order": 5, 204 | "prevSize": 16, 205 | "code": 59795, 206 | "ligatures": "equalizer2, sliders2", 207 | "name": "equalizer2" 208 | }, 209 | "setIdx": 0, 210 | "setId": 4, 211 | "iconIdx": 147 212 | }, 213 | { 214 | "icon": { 215 | "paths": [ 216 | "M933.79 610.25c-53.726-93.054-21.416-212.304 72.152-266.488l-100.626-174.292c-28.75 16.854-62.176 26.518-97.846 26.518-107.536 0-194.708-87.746-194.708-195.99h-201.258c0.266 33.41-8.074 67.282-25.958 98.252-53.724 93.056-173.156 124.702-266.862 70.758l-100.624 174.292c28.97 16.472 54.050 40.588 71.886 71.478 53.638 92.908 21.512 211.92-71.708 266.224l100.626 174.292c28.65-16.696 61.916-26.254 97.4-26.254 107.196 0 194.144 87.192 194.7 194.958h201.254c-0.086-33.074 8.272-66.57 25.966-97.218 53.636-92.906 172.776-124.594 266.414-71.012l100.626-174.29c-28.78-16.466-53.692-40.498-71.434-71.228zM512 719.332c-114.508 0-207.336-92.824-207.336-207.334 0-114.508 92.826-207.334 207.336-207.334 114.508 0 207.332 92.826 207.332 207.334-0.002 114.51-92.824 207.334-207.332 207.334z" 217 | ], 218 | "attrs": [], 219 | "isMulticolor": false, 220 | "isMulticolor2": false, 221 | "tags": [ 222 | "cog", 223 | "gear", 224 | "preferences", 225 | "settings", 226 | "generate", 227 | "control", 228 | "options" 229 | ], 230 | "defaultCode": 57823, 231 | "grid": 16 232 | }, 233 | "attrs": [], 234 | "properties": { 235 | "id": 148, 236 | "order": 34, 237 | "prevSize": 16, 238 | "code": 59796, 239 | "ligatures": "cog, gear", 240 | "name": "cog" 241 | }, 242 | "setIdx": 0, 243 | "setId": 4, 244 | "iconIdx": 148 245 | }, 246 | { 247 | "icon": { 248 | "paths": [ 249 | "M128 320v640c0 35.2 28.8 64 64 64h576c35.2 0 64-28.8 64-64v-640h-704zM320 896h-64v-448h64v448zM448 896h-64v-448h64v448zM576 896h-64v-448h64v448zM704 896h-64v-448h64v448z", 250 | "M848 128h-208v-80c0-26.4-21.6-48-48-48h-224c-26.4 0-48 21.6-48 48v80h-208c-26.4 0-48 21.6-48 48v80h832v-80c0-26.4-21.6-48-48-48zM576 128h-192v-63.198h192v63.198z" 251 | ], 252 | "attrs": [], 253 | "isMulticolor": false, 254 | "isMulticolor2": false, 255 | "tags": [ 256 | "bin", 257 | "trashcan", 258 | "remove", 259 | "delete", 260 | "recycle", 261 | "dispose" 262 | ], 263 | "defaultCode": 57938, 264 | "grid": 16 265 | }, 266 | "attrs": [], 267 | "properties": { 268 | "id": 172, 269 | "order": 14, 270 | "prevSize": 16, 271 | "code": 59820, 272 | "ligatures": "bin, trashcan", 273 | "name": "bin" 274 | }, 275 | "setIdx": 0, 276 | "setId": 4, 277 | "iconIdx": 172 278 | }, 279 | { 280 | "icon": { 281 | "paths": [ 282 | "M384 832h640v128h-640zM384 448h640v128h-640zM384 64h640v128h-640zM192 0v256h-64v-192h-64v-64zM128 526v50h128v64h-192v-146l128-60v-50h-128v-64h192v146zM256 704v320h-192v-64h128v-64h-128v-64h128v-64h-128v-64z" 283 | ], 284 | "attrs": [], 285 | "isMulticolor": false, 286 | "isMulticolor2": false, 287 | "tags": [ 288 | "list-numbered", 289 | "options" 290 | ], 291 | "defaultCode": 58012, 292 | "grid": 16 293 | }, 294 | "attrs": [], 295 | "properties": { 296 | "id": 185, 297 | "order": 2, 298 | "prevSize": 16, 299 | "code": 59833, 300 | "ligatures": "list-numbered, options", 301 | "name": "list-numbered" 302 | }, 303 | "setIdx": 0, 304 | "setId": 4, 305 | "iconIdx": 185 306 | }, 307 | { 308 | "icon": { 309 | "paths": [ 310 | "M0 0h256v256h-256zM384 64h640v128h-640zM0 384h256v256h-256zM384 448h640v128h-640zM0 768h256v256h-256zM384 832h640v128h-640z" 311 | ], 312 | "attrs": [], 313 | "isMulticolor": false, 314 | "isMulticolor2": false, 315 | "tags": [ 316 | "list", 317 | "todo", 318 | "bullet", 319 | "menu", 320 | "options" 321 | ], 322 | "defaultCode": 58009, 323 | "grid": 16 324 | }, 325 | "attrs": [], 326 | "properties": { 327 | "id": 186, 328 | "order": 3, 329 | "prevSize": 16, 330 | "code": 59834, 331 | "ligatures": "list, todo", 332 | "name": "list" 333 | }, 334 | "setIdx": 0, 335 | "setId": 4, 336 | "iconIdx": 186 337 | }, 338 | { 339 | "icon": { 340 | "paths": [ 341 | "M384 64h640v128h-640v-128zM384 448h640v128h-640v-128zM384 832h640v128h-640v-128zM0 128c0-70.692 57.308-128 128-128s128 57.308 128 128c0 70.692-57.308 128-128 128s-128-57.308-128-128zM0 512c0-70.692 57.308-128 128-128s128 57.308 128 128c0 70.692-57.308 128-128 128s-128-57.308-128-128zM0 896c0-70.692 57.308-128 128-128s128 57.308 128 128c0 70.692-57.308 128-128 128s-128-57.308-128-128z" 342 | ], 343 | "attrs": [], 344 | "isMulticolor": false, 345 | "isMulticolor2": false, 346 | "tags": [ 347 | "list", 348 | "todo", 349 | "bullet", 350 | "menu", 351 | "options" 352 | ], 353 | "defaultCode": 58010, 354 | "grid": 16 355 | }, 356 | "attrs": [], 357 | "properties": { 358 | "id": 187, 359 | "order": 32, 360 | "prevSize": 16, 361 | "code": 59835, 362 | "ligatures": "list2, todo2", 363 | "name": "list2" 364 | }, 365 | "setIdx": 0, 366 | "setId": 4, 367 | "iconIdx": 187 368 | }, 369 | { 370 | "icon": { 371 | "paths": [ 372 | "M64 192h896v192h-896zM64 448h896v192h-896zM64 704h896v192h-896z" 373 | ], 374 | "attrs": [], 375 | "isMulticolor": false, 376 | "isMulticolor2": false, 377 | "tags": [ 378 | "menu", 379 | "list", 380 | "options", 381 | "lines", 382 | "hamburger" 383 | ], 384 | "defaultCode": 58031, 385 | "grid": 16 386 | }, 387 | "attrs": [], 388 | "properties": { 389 | "order": 41, 390 | "id": 189, 391 | "prevSize": 16, 392 | "code": 59837, 393 | "ligatures": "menu, list3", 394 | "name": "menu" 395 | }, 396 | "setIdx": 0, 397 | "setId": 4, 398 | "iconIdx": 189 399 | }, 400 | { 401 | "icon": { 402 | "paths": [ 403 | "M512 0c-282.77 0-512 229.23-512 512s229.23 512 512 512 512-229.23 512-512-229.23-512-512-512zM512 960.002c-62.958 0-122.872-13.012-177.23-36.452l233.148-262.29c5.206-5.858 8.082-13.422 8.082-21.26v-96c0-17.674-14.326-32-32-32-112.99 0-232.204-117.462-233.374-118.626-6-6.002-14.14-9.374-22.626-9.374h-128c-17.672 0-32 14.328-32 32v192c0 12.122 6.848 23.202 17.69 28.622l110.31 55.156v187.886c-116.052-80.956-192-215.432-192-367.664 0-68.714 15.49-133.806 43.138-192h116.862c8.488 0 16.626-3.372 22.628-9.372l128-128c6-6.002 9.372-14.14 9.372-22.628v-77.412c40.562-12.074 83.518-18.588 128-18.588 70.406 0 137.004 16.26 196.282 45.2-4.144 3.502-8.176 7.164-12.046 11.036-36.266 36.264-56.236 84.478-56.236 135.764s19.97 99.5 56.236 135.764c36.434 36.432 85.218 56.264 135.634 56.26 3.166 0 6.342-0.080 9.518-0.236 13.814 51.802 38.752 186.656-8.404 372.334-0.444 1.744-0.696 3.488-0.842 5.224-81.324 83.080-194.7 134.656-320.142 134.656z" 404 | ], 405 | "attrs": [], 406 | "isMulticolor": false, 407 | "isMulticolor2": false, 408 | "tags": [ 409 | "earth", 410 | "globe", 411 | "language", 412 | "web", 413 | "internet", 414 | "sphere", 415 | "planet" 416 | ], 417 | "defaultCode": 58055, 418 | "grid": 16 419 | }, 420 | "attrs": [], 421 | "properties": { 422 | "order": 6, 423 | "id": 202, 424 | "prevSize": 16, 425 | "code": 59850, 426 | "ligatures": "earth, globe2", 427 | "name": "earth" 428 | }, 429 | "setIdx": 0, 430 | "setId": 4, 431 | "iconIdx": 202 432 | }, 433 | { 434 | "icon": { 435 | "paths": [ 436 | "M440.236 635.766c-13.31 0-26.616-5.076-36.77-15.23-95.134-95.136-95.134-249.934 0-345.070l192-192c46.088-46.086 107.36-71.466 172.534-71.466s126.448 25.38 172.536 71.464c95.132 95.136 95.132 249.934 0 345.070l-87.766 87.766c-20.308 20.308-53.23 20.308-73.54 0-20.306-20.306-20.306-53.232 0-73.54l87.766-87.766c54.584-54.586 54.584-143.404 0-197.99-26.442-26.442-61.6-41.004-98.996-41.004s-72.552 14.562-98.996 41.006l-192 191.998c-54.586 54.586-54.586 143.406 0 197.992 20.308 20.306 20.306 53.232 0 73.54-10.15 10.152-23.462 15.23-36.768 15.23z", 437 | "M256 1012c-65.176 0-126.45-25.38-172.534-71.464-95.134-95.136-95.134-249.934 0-345.070l87.764-87.764c20.308-20.306 53.234-20.306 73.54 0 20.308 20.306 20.308 53.232 0 73.54l-87.764 87.764c-54.586 54.586-54.586 143.406 0 197.992 26.44 26.44 61.598 41.002 98.994 41.002s72.552-14.562 98.998-41.006l192-191.998c54.584-54.586 54.584-143.406 0-197.992-20.308-20.308-20.306-53.232 0-73.54 20.306-20.306 53.232-20.306 73.54 0.002 95.132 95.134 95.132 249.932 0.002 345.068l-192.002 192c-46.090 46.088-107.364 71.466-172.538 71.466z" 438 | ], 439 | "attrs": [], 440 | "isMulticolor": false, 441 | "isMulticolor2": false, 442 | "tags": [ 443 | "link", 444 | "chain", 445 | "url", 446 | "uri", 447 | "anchor" 448 | ], 449 | "defaultCode": 58061, 450 | "grid": 16 451 | }, 452 | "attrs": [], 453 | "properties": { 454 | "order": 13, 455 | "id": 203, 456 | "prevSize": 16, 457 | "code": 59851, 458 | "ligatures": "link, chain", 459 | "name": "link" 460 | }, 461 | "setIdx": 0, 462 | "setId": 4, 463 | "iconIdx": 203 464 | }, 465 | { 466 | "icon": { 467 | "paths": [ 468 | "M992 384h-352v-352c0-17.672-14.328-32-32-32h-192c-17.672 0-32 14.328-32 32v352h-352c-17.672 0-32 14.328-32 32v192c0 17.672 14.328 32 32 32h352v352c0 17.672 14.328 32 32 32h192c17.672 0 32-14.328 32-32v-352h352c17.672 0 32-14.328 32-32v-192c0-17.672-14.328-32-32-32z" 469 | ], 470 | "attrs": [], 471 | "isMulticolor": false, 472 | "isMulticolor2": false, 473 | "tags": [ 474 | "plus", 475 | "add", 476 | "sum" 477 | ], 478 | "defaultCode": 58230, 479 | "grid": 16 480 | }, 481 | "attrs": [], 482 | "properties": { 483 | "id": 266, 484 | "order": 4, 485 | "prevSize": 16, 486 | "code": 59914, 487 | "ligatures": "plus, add", 488 | "name": "plus" 489 | }, 490 | "setIdx": 0, 491 | "setId": 4, 492 | "iconIdx": 266 493 | }, 494 | { 495 | "icon": { 496 | "paths": [ 497 | "M1014.662 822.66c-0.004-0.004-0.008-0.008-0.012-0.010l-310.644-310.65 310.644-310.65c0.004-0.004 0.008-0.006 0.012-0.010 3.344-3.346 5.762-7.254 7.312-11.416 4.246-11.376 1.824-24.682-7.324-33.83l-146.746-146.746c-9.148-9.146-22.45-11.566-33.828-7.32-4.16 1.55-8.070 3.968-11.418 7.31 0 0.004-0.004 0.006-0.008 0.010l-310.648 310.652-310.648-310.65c-0.004-0.004-0.006-0.006-0.010-0.010-3.346-3.342-7.254-5.76-11.414-7.31-11.38-4.248-24.682-1.826-33.83 7.32l-146.748 146.748c-9.148 9.148-11.568 22.452-7.322 33.828 1.552 4.16 3.97 8.072 7.312 11.416 0.004 0.002 0.006 0.006 0.010 0.010l310.65 310.648-310.65 310.652c-0.002 0.004-0.006 0.006-0.008 0.010-3.342 3.346-5.76 7.254-7.314 11.414-4.248 11.376-1.826 24.682 7.322 33.83l146.748 146.746c9.15 9.148 22.452 11.568 33.83 7.322 4.16-1.552 8.070-3.97 11.416-7.312 0.002-0.004 0.006-0.006 0.010-0.010l310.648-310.65 310.648 310.65c0.004 0.002 0.008 0.006 0.012 0.008 3.348 3.344 7.254 5.762 11.414 7.314 11.378 4.246 24.684 1.826 33.828-7.322l146.746-146.748c9.148-9.148 11.57-22.454 7.324-33.83-1.552-4.16-3.97-8.068-7.314-11.414z" 498 | ], 499 | "attrs": [], 500 | "isMulticolor": false, 501 | "isMulticolor2": false, 502 | "tags": [ 503 | "cross", 504 | "cancel", 505 | "close", 506 | "quit", 507 | "remove" 508 | ], 509 | "defaultCode": 58219, 510 | "grid": 16 511 | }, 512 | "attrs": [], 513 | "properties": { 514 | "id": 271, 515 | "order": 21, 516 | "prevSize": 16, 517 | "code": 59919, 518 | "ligatures": "cross, cancel", 519 | "name": "cross" 520 | }, 521 | "setIdx": 0, 522 | "setId": 4, 523 | "iconIdx": 271 524 | }, 525 | { 526 | "icon": { 527 | "paths": [ 528 | "M864 128l-480 480-224-224-160 160 384 384 640-640z" 529 | ], 530 | "attrs": [], 531 | "isMulticolor": false, 532 | "isMulticolor2": false, 533 | "tags": [ 534 | "checkmark", 535 | "tick", 536 | "correct", 537 | "accept", 538 | "ok" 539 | ], 540 | "defaultCode": 58224, 541 | "grid": 16 542 | }, 543 | "attrs": [], 544 | "properties": { 545 | "id": 272, 546 | "order": 20, 547 | "prevSize": 16, 548 | "code": 59920, 549 | "ligatures": "checkmark, tick", 550 | "name": "checkmark" 551 | }, 552 | "setIdx": 0, 553 | "setId": 4, 554 | "iconIdx": 272 555 | }, 556 | { 557 | "icon": { 558 | "paths": [ 559 | "M621.254 877.254l320-320c24.994-24.992 24.994-65.516 0-90.51l-320-320c-24.994-24.992-65.516-24.992-90.51 0-24.994 24.994-24.994 65.516 0 90.51l210.746 210.746h-613.49c-35.346 0-64 28.654-64 64s28.654 64 64 64h613.49l-210.746 210.746c-12.496 12.496-18.744 28.876-18.744 45.254s6.248 32.758 18.744 45.254c24.994 24.994 65.516 24.994 90.51 0z" 560 | ], 561 | "attrs": [], 562 | "isMulticolor": false, 563 | "isMulticolor2": false, 564 | "tags": [ 565 | "arrow-right", 566 | "right", 567 | "next" 568 | ], 569 | "defaultCode": 58307, 570 | "grid": 16 571 | }, 572 | "attrs": [], 573 | "properties": { 574 | "order": 39, 575 | "id": 316, 576 | "prevSize": 16, 577 | "code": 59964, 578 | "ligatures": "arrow-right2, right4", 579 | "name": "arrow-right2" 580 | }, 581 | "setIdx": 0, 582 | "setId": 4, 583 | "iconIdx": 316 584 | }, 585 | { 586 | "icon": { 587 | "paths": [ 588 | "M704 512v384h64v-384h160l-192-192-192 192z", 589 | "M64 192h96v64h-96v-64z", 590 | "M192 192h96v64h-96v-64z", 591 | "M320 192h64v96h-64v-96z", 592 | "M64 416h64v96h-64v-96z", 593 | "M160 448h96v64h-96v-64z", 594 | "M288 448h96v64h-96v-64z", 595 | "M64 288h64v96h-64v-96z", 596 | "M320 320h64v96h-64v-96z", 597 | "M320 704v192h-192v-192h192zM384 640h-320v320h320v-320z" 598 | ], 599 | "attrs": [], 600 | "isMulticolor": false, 601 | "isMulticolor2": false, 602 | "tags": [ 603 | "move-up", 604 | "sort", 605 | "arrange" 606 | ], 607 | "defaultCode": 60997, 608 | "grid": 16 609 | }, 610 | "attrs": [], 611 | "properties": { 612 | "id": 326, 613 | "order": 40, 614 | "prevSize": 16, 615 | "code": 59974, 616 | "ligatures": "move-up, sort", 617 | "name": "move-up" 618 | }, 619 | "setIdx": 0, 620 | "setId": 4, 621 | "iconIdx": 326 622 | }, 623 | { 624 | "icon": { 625 | "paths": [ 626 | "M832 256l192-192-64-64-192 192h-448v-192h-128v192h-192v128h192v512h512v192h128v-192h192v-128h-192v-448zM320 320h320l-320 320v-320zM384 704l320-320v320h-320z" 627 | ], 628 | "attrs": [], 629 | "isMulticolor": false, 630 | "isMulticolor2": false, 631 | "tags": [ 632 | "crop", 633 | "resize", 634 | "cut" 635 | ], 636 | "defaultCode": 58428, 637 | "grid": 16 638 | }, 639 | "attrs": [], 640 | "properties": { 641 | "id": 343, 642 | "order": 22, 643 | "prevSize": 16, 644 | "code": 59991, 645 | "ligatures": "crop, resize", 646 | "name": "crop" 647 | }, 648 | "setIdx": 0, 649 | "setId": 4, 650 | "iconIdx": 343 651 | }, 652 | { 653 | "icon": { 654 | "paths": [ 655 | "M707.88 484.652c37.498-44.542 60.12-102.008 60.12-164.652 0-141.16-114.842-256-256-256h-320v896h384c141.158 0 256-114.842 256-256 0-92.956-49.798-174.496-124.12-219.348zM384 192h101.5c55.968 0 101.5 57.42 101.5 128s-45.532 128-101.5 128h-101.5v-256zM543 832h-159v-256h159c58.45 0 106 57.42 106 128s-47.55 128-106 128z" 656 | ], 657 | "attrs": [], 658 | "isMulticolor": false, 659 | "isMulticolor2": false, 660 | "tags": [ 661 | "bold", 662 | "wysiwyg" 663 | ], 664 | "defaultCode": 58452, 665 | "grid": 16 666 | }, 667 | "attrs": [], 668 | "properties": { 669 | "id": 354, 670 | "order": 9, 671 | "prevSize": 16, 672 | "code": 60002, 673 | "ligatures": "bold, wysiwyg4", 674 | "name": "bold" 675 | }, 676 | "setIdx": 0, 677 | "setId": 4, 678 | "iconIdx": 354 679 | }, 680 | { 681 | "icon": { 682 | "paths": [ 683 | "M896 64v64h-128l-320 768h128v64h-448v-64h128l320-768h-128v-64z" 684 | ], 685 | "attrs": [], 686 | "isMulticolor": false, 687 | "isMulticolor2": false, 688 | "tags": [ 689 | "italic", 690 | "wysiwyg" 691 | ], 692 | "defaultCode": 58454, 693 | "grid": 16 694 | }, 695 | "attrs": [], 696 | "properties": { 697 | "id": 356, 698 | "order": 10, 699 | "prevSize": 16, 700 | "code": 60004, 701 | "ligatures": "italic, wysiwyg6", 702 | "name": "italic" 703 | }, 704 | "setIdx": 0, 705 | "setId": 4, 706 | "iconIdx": 356 707 | }, 708 | { 709 | "icon": { 710 | "paths": [ 711 | "M0 512h128v64h-128zM192 512h192v64h-192zM448 512h128v64h-128zM640 512h192v64h-192zM896 512h128v64h-128zM880 0l16 448h-768l16-448h32l16 384h640l16-384zM144 1024l-16-384h768l-16 384h-32l-16-320h-640l-16 320z" 712 | ], 713 | "attrs": [], 714 | "isMulticolor": false, 715 | "isMulticolor2": false, 716 | "tags": [ 717 | "page-break", 718 | "wysiwyg" 719 | ], 720 | "defaultCode": 58460, 721 | "grid": 16 722 | }, 723 | "attrs": [], 724 | "properties": { 725 | "id": 360, 726 | "order": 38, 727 | "prevSize": 16, 728 | "code": 60008, 729 | "ligatures": "page-break, wysiwyg10", 730 | "name": "page-break" 731 | }, 732 | "setIdx": 0, 733 | "setId": 4, 734 | "iconIdx": 360 735 | }, 736 | { 737 | "icon": { 738 | "paths": [ 739 | "M256 384v-384h768v384h-64v-320h-640v320zM1024 576v448h-768v-448h64v384h640v-384zM512 448h128v64h-128zM320 448h128v64h-128zM704 448h128v64h-128zM896 448h128v64h-128zM0 288l192 192-192 192z" 740 | ], 741 | "attrs": [], 742 | "isMulticolor": false, 743 | "isMulticolor2": false, 744 | "tags": [ 745 | "pagebreak", 746 | "wysiwyg" 747 | ], 748 | "defaultCode": 58467, 749 | "grid": 16 750 | }, 751 | "attrs": [], 752 | "properties": { 753 | "id": 366, 754 | "order": 19, 755 | "prevSize": 16, 756 | "code": 60014, 757 | "ligatures": "pagebreak, wysiwyg16", 758 | "name": "pagebreak" 759 | }, 760 | "setIdx": 0, 761 | "setId": 4, 762 | "iconIdx": 366 763 | }, 764 | { 765 | "icon": { 766 | "paths": [ 767 | "M0 64v896h1024v-896h-1024zM384 640v-192h256v192h-256zM640 704v192h-256v-192h256zM640 192v192h-256v-192h256zM320 192v192h-256v-192h256zM64 448h256v192h-256v-192zM704 448h256v192h-256v-192zM704 384v-192h256v192h-256zM64 704h256v192h-256v-192zM704 896v-192h256v192h-256z" 768 | ], 769 | "attrs": [], 770 | "isMulticolor": false, 771 | "isMulticolor2": false, 772 | "tags": [ 773 | "table", 774 | "wysiwyg" 775 | ], 776 | "defaultCode": 58470, 777 | "grid": 16 778 | }, 779 | "attrs": [], 780 | "properties": { 781 | "id": 369, 782 | "order": 11, 783 | "prevSize": 16, 784 | "code": 60017, 785 | "ligatures": "table2, wysiwyg19", 786 | "name": "table2" 787 | }, 788 | "setIdx": 0, 789 | "setId": 4, 790 | "iconIdx": 369 791 | }, 792 | { 793 | "icon": { 794 | "paths": [ 795 | "M0 64h1024v128h-1024zM0 256h640v128h-640zM0 640h640v128h-640zM0 448h1024v128h-1024zM0 832h1024v128h-1024z" 796 | ], 797 | "attrs": [], 798 | "isMulticolor": false, 799 | "isMulticolor2": false, 800 | "tags": [ 801 | "paragraph-left", 802 | "wysiwyg", 803 | "align-left", 804 | "left" 805 | ], 806 | "defaultCode": 58479, 807 | "grid": 16 808 | }, 809 | "attrs": [], 810 | "properties": { 811 | "id": 375, 812 | "order": 4, 813 | "prevSize": 16, 814 | "code": 60023, 815 | "ligatures": "paragraph-left, wysiwyg25", 816 | "name": "paragraph-left" 817 | }, 818 | "setIdx": 0, 819 | "setId": 4, 820 | "iconIdx": 375 821 | }, 822 | { 823 | "icon": { 824 | "paths": [ 825 | "M0 64h1024v128h-1024zM192 256h640v128h-640zM192 640h640v128h-640zM0 448h1024v128h-1024zM0 832h1024v128h-1024z" 826 | ], 827 | "attrs": [], 828 | "isMulticolor": false, 829 | "isMulticolor2": false, 830 | "tags": [ 831 | "paragraph-center", 832 | "wysiwyg", 833 | "align-center", 834 | "center" 835 | ], 836 | "defaultCode": 58480, 837 | "grid": 16 838 | }, 839 | "attrs": [], 840 | "properties": { 841 | "id": 376, 842 | "order": 5, 843 | "prevSize": 16, 844 | "code": 60024, 845 | "ligatures": "paragraph-center, wysiwyg26", 846 | "name": "paragraph-center" 847 | }, 848 | "setIdx": 0, 849 | "setId": 4, 850 | "iconIdx": 376 851 | }, 852 | { 853 | "icon": { 854 | "paths": [ 855 | "M0 64h1024v128h-1024zM384 256h640v128h-640zM384 640h640v128h-640zM0 448h1024v128h-1024zM0 832h1024v128h-1024z" 856 | ], 857 | "attrs": [], 858 | "isMulticolor": false, 859 | "isMulticolor2": false, 860 | "tags": [ 861 | "paragraph-right", 862 | "wysiwyg", 863 | "align-right", 864 | "right" 865 | ], 866 | "defaultCode": 58481, 867 | "grid": 16 868 | }, 869 | "attrs": [], 870 | "properties": { 871 | "id": 377, 872 | "order": 6, 873 | "prevSize": 16, 874 | "code": 60025, 875 | "ligatures": "paragraph-right, wysiwyg27", 876 | "name": "paragraph-right" 877 | }, 878 | "setIdx": 0, 879 | "setId": 4, 880 | "iconIdx": 377 881 | }, 882 | { 883 | "icon": { 884 | "paths": [ 885 | "M0 64h1024v128h-1024zM384 256h640v128h-640zM384 448h640v128h-640zM384 640h640v128h-640zM0 832h1024v128h-1024zM0 704v-384l256 192z" 886 | ], 887 | "attrs": [], 888 | "isMulticolor": false, 889 | "isMulticolor2": false, 890 | "tags": [ 891 | "indent-increase", 892 | "wysiwyg" 893 | ], 894 | "defaultCode": 58483, 895 | "grid": 16 896 | }, 897 | "attrs": [], 898 | "properties": { 899 | "id": 379, 900 | "order": 7, 901 | "prevSize": 16, 902 | "code": 60027, 903 | "ligatures": "indent-increase, wysiwyg29", 904 | "name": "indent-increase" 905 | }, 906 | "setIdx": 0, 907 | "setId": 4, 908 | "iconIdx": 379 909 | }, 910 | { 911 | "icon": { 912 | "paths": [ 913 | "M0 64h1024v128h-1024zM384 256h640v128h-640zM384 448h640v128h-640zM384 640h640v128h-640zM0 832h1024v128h-1024zM256 320v384l-256-192z" 914 | ], 915 | "attrs": [], 916 | "isMulticolor": false, 917 | "isMulticolor2": false, 918 | "tags": [ 919 | "indent-decrease", 920 | "wysiwyg" 921 | ], 922 | "defaultCode": 58484, 923 | "grid": 16 924 | }, 925 | "attrs": [], 926 | "properties": { 927 | "id": 380, 928 | "order": 8, 929 | "prevSize": 16, 930 | "code": 60028, 931 | "ligatures": "indent-decrease, wysiwyg30", 932 | "name": "indent-decrease" 933 | }, 934 | "setIdx": 0, 935 | "setId": 4, 936 | "iconIdx": 380 937 | }, 938 | { 939 | "icon": { 940 | "paths": [ 941 | "M256 640c0 0 58.824-192 384-192v192l384-256-384-256v192c-256 0-384 159.672-384 320zM704 768h-576v-384h125.876c10.094-11.918 20.912-23.334 32.488-34.18 43.964-41.19 96.562-72.652 156.114-93.82h-442.478v640h832v-268.624l-128 85.334v55.29z" 942 | ], 943 | "attrs": [], 944 | "isMulticolor": false, 945 | "isMulticolor2": false, 946 | "tags": [ 947 | "share", 948 | "out", 949 | "external", 950 | "outside" 951 | ], 952 | "defaultCode": 58491, 953 | "grid": 16 954 | }, 955 | "attrs": [], 956 | "properties": { 957 | "id": 381, 958 | "order": 36, 959 | "prevSize": 16, 960 | "code": 60029, 961 | "ligatures": "share, out", 962 | "name": "share" 963 | }, 964 | "setIdx": 0, 965 | "setId": 4, 966 | "iconIdx": 381 967 | }, 968 | { 969 | "icon": { 970 | "paths": [ 971 | "M832 736l96 96 320-320-320-320-96 96 224 224z", 972 | "M448 288l-96-96-320 320 320 320 96-96-224-224z", 973 | "M701.298 150.519l69.468 18.944-191.987 704.026-69.468-18.944 191.987-704.026z" 974 | ], 975 | "width": 1280, 976 | "attrs": [], 977 | "isMulticolor": false, 978 | "isMulticolor2": false, 979 | "tags": [ 980 | "embed", 981 | "code", 982 | "html", 983 | "xml" 984 | ], 985 | "defaultCode": 58496, 986 | "grid": 16 987 | }, 988 | "attrs": [], 989 | "properties": { 990 | "id": 384, 991 | "order": 18, 992 | "prevSize": 16, 993 | "code": 60032, 994 | "ligatures": "embed2, code2", 995 | "name": "embed2" 996 | }, 997 | "setIdx": 0, 998 | "setId": 4, 999 | "iconIdx": 384 1000 | }, 1001 | { 1002 | "icon": { 1003 | "paths": [ 1004 | "M832 128h-640c-105.6 0-192 86.4-192 192v384c0 105.6 86.4 192 192 192h640c105.6 0 192-86.4 192-192v-384c0-105.6-86.4-192-192-192zM960 704c0 33.978-13.374 66.062-37.654 90.346-24.284 24.28-56.366 37.654-90.346 37.654h-640c-33.978 0-66.062-13.374-90.344-37.654-24.282-24.284-37.656-56.368-37.656-90.346v-384c0-33.978 13.374-66.062 37.656-90.344s56.366-37.656 90.344-37.656h640c33.978 0 66.062 13.374 90.346 37.656 24.282 24.282 37.654 56.366 37.654 90.344v384zM384 768l320-256-320-256z" 1005 | ], 1006 | "attrs": [], 1007 | "isMulticolor": false, 1008 | "isMulticolor2": false, 1009 | "tags": [ 1010 | "youtube", 1011 | "brand", 1012 | "social" 1013 | ], 1014 | "defaultCode": 58532, 1015 | "grid": 16 1016 | }, 1017 | "attrs": [], 1018 | "properties": { 1019 | "id": 408, 1020 | "order": 12, 1021 | "prevSize": 16, 1022 | "code": 60056, 1023 | "ligatures": "youtube2, brand15", 1024 | "name": "youtube2" 1025 | }, 1026 | "setIdx": 0, 1027 | "setId": 4, 1028 | "iconIdx": 408 1029 | } 1030 | ], 1031 | "height": 1024, 1032 | "metadata": { 1033 | "name": "icomoon" 1034 | }, 1035 | "preferences": { 1036 | "showGlyphs": true, 1037 | "showQuickUse": true, 1038 | "showQuickUse2": true, 1039 | "showSVGs": true, 1040 | "fontPref": { 1041 | "prefix": "icon-", 1042 | "metadata": { 1043 | "fontFamily": "icomoon" 1044 | }, 1045 | "metrics": { 1046 | "emSize": 1024, 1047 | "baseline": 6.25, 1048 | "whitespace": 50 1049 | }, 1050 | "embed": false 1051 | }, 1052 | "imagePref": { 1053 | "prefix": "icon-", 1054 | "png": true, 1055 | "useClassSelector": true, 1056 | "color": 0, 1057 | "bgColor": 16777215, 1058 | "classSelector": ".icon", 1059 | "height": 16, 1060 | "overrideSize": "true" 1061 | }, 1062 | "historySize": 50, 1063 | "showCodes": true, 1064 | "gridSize": 16 1065 | } 1066 | } --------------------------------------------------------------------------------