PluginProbe ʕ •ᴥ•ʔ
SiteOrigin CSS / 1.3.2
SiteOrigin CSS v1.3.2
1.2.1 1.2.10 1.2.11 1.2.12 1.2.13 1.2.14 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 1.3.0 1.3.1 1.3.2 1.4.0 1.4.1 1.4.2 1.4.3 1.5.0 1.5.1 1.5.10 1.5.11 1.5.2 1.5.3 1.5.4 1.5.5 1.5.6 1.5.7 1.5.8 1.5.9 1.6.0 1.6.1 1.6.2 1.6.3 1.6.4 1.6.5 1.6.6 trunk 1.0 1.0.1 1.0.2 1.0.3 1.0.4 1.0.5 1.0.6 1.0.7 1.0.8 1.1 1.1.1 1.1.2 1.1.3 1.1.4 1.1.5 1.2.0
so-css / js / editor.js
so-css / js Last commit date
URI.js 9 years ago URI.min.js 6 years ago css.js 6 years ago css.min.js 6 years ago csslint.js 9 years ago csslint.min.js 6 years ago editor.js 5 years ago editor.min.js 5 years ago inspector.js 5 years ago inspector.min.js 5 years ago jquery.sizes.js 11 years ago jquery.sizes.min.js 6 years ago specificity.js 11 years ago specificity.min.js 6 years ago
editor.js
1853 lines
1 /* globals jQuery, _, socssOptions, Backbone, CodeMirror, console, cssjs, wp */
2
3 ( function ( $, _, socssOptions ) {
4
5 var socss = {
6 model: {},
7 collection: {},
8 view: {},
9 fn: {}
10 };
11
12 window.socss = socss;
13
14 socss.model.CustomCssModel = Backbone.Model.extend( {
15 defaults: {
16 postId: null,
17 postTitle: null,
18 css: null,
19 },
20
21 urlRoot: socssOptions.postCssUrlRoot,
22
23 url: function () {
24 return this.urlRoot + '&postId=' + this.get( 'postId' );
25 }
26 } );
27
28 socss.model.CustomCssCollection = Backbone.Collection.extend( {
29 model: socss.model.CustomCssModel,
30
31 modelId: function( attrs ) {
32 return attrs.postId;
33 },
34 } );
35
36 socss.model.CSSEditorModel = Backbone.Model.extend( {
37 defaults: {
38 customCssPosts: null,
39 }
40 } );
41
42 /**
43 * The toolbar view
44 */
45 socss.view.toolbar = Backbone.View.extend( {
46
47 button: _.template( '<li><a href="#<%= action %>" class="toolbar-button socss-button"><%= text %></a></li>' ),
48
49 events: {
50 'click .socss-button': 'triggerEvent',
51 },
52
53 triggerEvent: function ( event ) {
54 event.preventDefault();
55 var $target = $( event.currentTarget );
56 $target.trigger( 'blur' );
57 var value = $target.attr( 'href' ).replace( '#', '' );
58 this.$el.trigger( 'click_' + value );
59 },
60
61 addButton: function ( text, action ) {
62 var button = $( this.button( { text: text, action: action } ) )
63 .appendTo( this.$( '.toolbar-function-buttons .toolbar-buttons' ) );
64
65 return button;
66 },
67 } );
68
69 /**
70 * The editor view, which handles codemirror stuff
71 *
72 * model: socss.model.CSSEditorModel
73 *
74 */
75 socss.view.editor = Backbone.View.extend( {
76
77 codeMirror: null,
78 snippets: null,
79 toolbar: null,
80 visualProperties: null,
81
82 inspector: null,
83
84 cssSelectors: [],
85
86 initValue: null,
87
88 events: {
89 'click_expand .custom-css-toolbar': 'toggleExpand',
90 'click_visual .custom-css-toolbar': 'showVisualEditor',
91 'submit': 'onSubmit',
92 },
93
94 initialize: function ( options ) {
95
96 this.listenTo( this.model, 'change:selectedPost', this.getSelectedPostCss );
97
98 this.getSelectedPostCss().then( function () {
99
100 if ( options.openVisualEditor ) {
101 this.showVisualEditor();
102 }
103 }.bind( this ) );
104
105 },
106
107 getSelectedPostCss: function () {
108 var selectedPost = this.model.get( 'selectedPost' );
109 var promise;
110 if ( selectedPost && ! selectedPost.has( 'css' ) ) {
111 promise = selectedPost.fetch();
112 } else {
113 promise = new $.Deferred().resolve();
114 }
115
116 return promise.then( this.render.bind( this ) );
117 },
118
119 render: function () {
120
121 var selectedPost = this.model.get( 'selectedPost' );
122
123 if ( selectedPost && !selectedPost.has( 'css' ) ) {
124 return this;
125 }
126
127 if ( !this.codeMirror ) {
128 this.setupEditor();
129 }
130
131 if ( ! this.toolbar ) {
132 this.toolbar = new socss.view.toolbar( {
133 el: this.$( '.custom-css-toolbar' ),
134 model: this.model,
135 } );
136 this.toolbar.render();
137 }
138
139 if ( !this.visualProperties ) {
140 this.visualProperties = new socss.view.properties( {
141 editor: this,
142 el: $( '#so-custom-css-properties' )
143 } );
144 this.visualProperties.render();
145 }
146
147 if ( !this.preview ) {
148 this.preview = new socss.view.preview( {
149 editor: this,
150 model: this.model,
151 el: this.$( '.custom-css-preview' ),
152 initURL: socssOptions.homeURL,
153 } );
154 this.preview.render();
155 }
156
157 if ( selectedPost ) {
158 this.codeMirror.setValue( selectedPost.get( 'css' ) );
159 this.codeMirror.clearHistory();
160 }
161
162 return this;
163 },
164
165 /**
166 * Do the initial setup of the CodeMirror editor
167 */
168 setupEditor: function () {
169 // Setup the Codemirror instance
170 var $textArea = this.$( 'textarea.css-editor' );
171 this.initValue = $textArea.val();
172 // Pad with empty lines so the editor takes up all the white space. To try make sure user gets copy/paste
173 // options in context menu.
174 var newlineMatches = this.initValue.match( /\n/gm );
175 var lineCount = newlineMatches ? newlineMatches.length + 1 : 1;
176 var paddedValue = this.initValue;
177 $textArea.val( paddedValue );
178
179 var codeMirrorSettings = {
180 tabSize: 2,
181 lineNumbers: true,
182 mode: 'css',
183 theme: 'neat',
184 inputStyle: 'contenteditable', //necessary to allow context menu (right click) copy/paste etc.
185 gutters: [
186 "CodeMirror-lint-markers"
187 ],
188 lint: true,
189 search: true,
190 dialog: true,
191 annotateScrollbar: true,
192 extraKeys: {
193 'Ctrl-F': 'findPersistent',
194 'Alt-G': 'jumpToLine',
195 },
196 }
197
198 if ( typeof wp.codeEditor != "undefined" ) {
199 codeMirrorSettings = _.extend(
200 wp.codeEditor.defaultSettings.codemirror,
201 codeMirrorSettings
202 );
203 this.codeMirror = wp.codeEditor.initialize( $textArea.get( 0 ), codeMirrorSettings ).codemirror;
204 } else {
205 this.registerCodeMirrorAutocomplete();
206 this.codeMirror = CodeMirror.fromTextArea( $textArea.get( 0 ), codeMirrorSettings );
207 this.setupCodeMirrorExtensions();
208 }
209
210 this.codeMirror.on( 'change', function ( cm, change ) {
211 var selectedPost = this.model.get( 'selectedPost' );
212 if ( selectedPost && selectedPost.get( 'css' ) !== cm.getValue().trim() ) {
213 selectedPost.set( 'css', cm.getValue().trim() );
214 }
215 }.bind( this ) );
216
217 // Make sure the user doesn't leave without saving
218 $( window ).on( 'beforeunload', function () {
219 var editorValue = this.codeMirror.getValue().trim();
220 if ( editorValue !== this.initValue ) {
221 return socssOptions.loc.leave;
222 }
223 }.bind( this ) );
224
225
226 // Set the container to visible overflow once the editor is setup
227 this.$el.find( '.custom-css-container' ).css( 'overflow', 'visible' );
228 this.scaleEditor();
229
230 // Scale the editor whenever the window is resized
231 $( window ).on( 'resize', function () {
232 this.scaleEditor();
233 }.bind( this ) );
234 },
235
236 onSubmit: function () {
237 this.initValue = this.codeMirror.getValue().trim();
238 },
239
240 /**
241 * Register the autocomplete helper. Based on css-hint.js in the codemirror addon folder.
242 */
243 registerCodeMirrorAutocomplete: function () {
244 var pseudoClasses = {
245 link: 1, visited: 1, active: 1, hover: 1, focus: 1,
246 "first-letter": 1, "first-line": 1, "first-child": 1,
247 before: 1, after: 1, lang: 1
248 };
249
250 CodeMirror.registerHelper( "hint", "css", function ( cm ) {
251 var cur = cm.getCursor(), token = cm.getTokenAt( cur );
252 var inner = CodeMirror.innerMode( cm.getMode(), token.state );
253 if ( inner.mode.name !== "css" ) {
254 return;
255 }
256
257 if ( token.type === "keyword" && "!important".indexOf( token.string ) === 0 ) {
258 return {
259 list: [ "!important" ], from: CodeMirror.Pos( cur.line, token.start ),
260 to: CodeMirror.Pos( cur.line, token.end )
261 };
262 }
263
264 var start = token.start, end = cur.ch, word = token.string.slice( 0, end - start );
265 if ( /[^\w$_-]/.test( word ) ) {
266 word = "";
267 start = end = cur.ch;
268 }
269
270 var spec = CodeMirror.resolveMode( "text/css" );
271
272 var result = [];
273
274 function add( keywords ) {
275 for ( var name in keywords ) {
276 if ( !word || name.lastIndexOf( word, 0 ) === 0 ) {
277 result.push( name );
278 }
279 }
280 }
281
282 var st = inner.state.state;
283
284 if ( st === 'top' ) {
285 // We're going to autocomplete the selector using our own set of rules
286 var line = cm.getLine( cur.line ).trim();
287
288 var selectors = this.cssSelectors;
289 for ( var i = 0; i < selectors.length; i++ ) {
290 if ( selectors[ i ].selector.indexOf( line ) !== -1 ) {
291 result.push( selectors[ i ].selector );
292 }
293 }
294
295 if ( result.length ) {
296 return {
297 list: result,
298 from: CodeMirror.Pos( cur.line, 0 ),
299 to: CodeMirror.Pos( cur.line, end )
300 };
301 }
302 }
303 else {
304
305 if ( st === "pseudo" || token.type === "variable-3" ) {
306 add( pseudoClasses );
307 }
308 else if ( st === "block" || st === "maybeprop" ) {
309 add( spec.propertyKeywords );
310 }
311 else if ( st === "prop" || st === "parens" || st === "at" || st === "params" ) {
312 add( spec.valueKeywords );
313 add( spec.colorKeywords );
314 }
315 else if ( st === "media" || st === "media_parens" ) {
316 add( spec.mediaTypes );
317 add( spec.mediaFeatures );
318 }
319
320 if ( result.length ) {
321 return {
322 list: result,
323 from: CodeMirror.Pos( cur.line, start ),
324 to: CodeMirror.Pos( cur.line, end )
325 };
326 }
327
328 }
329
330 }.bind( this ) );
331 },
332
333 setupCodeMirrorExtensions: function () {
334
335 this.codeMirror.on( 'cursorActivity', function ( cm ) {
336 var cur = cm.getCursor(), token = cm.getTokenAt( cur );
337 var inner = CodeMirror.innerMode( cm.getMode(), token.state );
338
339 // If we have a qualifier selected, then highlight that in the preview
340 if ( token.type === 'qualifier' || token.type === 'tag' || token.type === 'builtin' ) {
341 var line = cm.getLine( cur.line );
342 var selector = line.substring( 0, token.end );
343
344 this.preview.highlight( selector );
345 }
346 else {
347 this.preview.clearHighlight();
348 }
349 }.bind( this ) );
350
351 if ( typeof CodeMirror.showHint == 'function' ) {
352 // This sets up automatic autocompletion at all times
353 this.codeMirror.on( 'keyup', function ( cm, e ) {
354 if (
355 ( e.keyCode >= 65 && e.keyCode <= 90 ) ||
356 ( e.keyCode === 189 && !e.shiftKey ) ||
357 ( e.keyCode === 190 && !e.shiftKey ) ||
358 ( e.keyCode === 51 && e.shiftKey ) ||
359 ( e.keyCode === 189 && e.shiftKey )
360 ) {
361 cm.showHint( {
362 completeSingle: false
363 } );
364 }
365 } );
366 }
367 },
368
369 /**
370 * Scale the size of the editor depending on whether it's expanded or not
371 */
372 scaleEditor: function () {
373 var windowHeight = $( window ).outerHeight();
374 var areaHeight;
375 if ( this.$el.hasClass( 'expanded' ) ) {
376 // If we're in the expanded view, then resize the editor
377 this.$el.find( '.CodeMirror-scroll' ).css( 'max-height', '' );
378 areaHeight = windowHeight - this.$( '.custom-css-toolbar' ).outerHeight();
379 this.codeMirror.setSize( '100%', areaHeight );
380 }
381 else {
382 // Attempt to calculate approximate space available for editor when not expanded.
383 var $form = $( '#so-custom-css-form' );
384 var otherEltsHeight = $( '#wpadminbar' ).outerHeight( true ) +
385 $( '#siteorigin-custom-css' ).find( '> h2' ).outerHeight( true ) +
386 $form.find( '> .custom-css-toolbar' ).outerHeight( true ) +
387 $form.find( '> p.description' ).outerHeight( true ) +
388 parseFloat( $( '#wpbody-content' ).css( 'padding-bottom' ) );
389
390 areaHeight = windowHeight - otherEltsHeight;
391 // The container has a min-height of 300px so we need to ensure the areaHeight is at least that large.
392 if ( areaHeight < 300 ) {
393 areaHeight = 300;
394 }
395
396 this.$el.find( '.CodeMirror-scroll' ).css( 'min-height', areaHeight + 'px' );
397 this.codeMirror.setSize( '100%', 'auto' );
398 }
399 this.$el.find( '.CodeMirror-code' ).css( 'min-height', areaHeight + 'px' );
400 },
401
402 /**
403 * Check if the editor is in expanded mode
404 * @returns bool
405 */
406 isExpanded: function () {
407 return this.$el.hasClass( 'expanded' );
408 },
409
410 /**
411 * Toggle if this is expanded or not
412 */
413 toggleExpand: function () {
414 this.$el.toggleClass( 'expanded' );
415 this.scaleEditor();
416 },
417
418 /**
419 * Set the expanded state of the editor
420 * @param expanded
421 */
422 setExpand: function ( expanded ) {
423 if ( expanded ) {
424 this.$el.addClass( 'expanded' );
425 }
426 else {
427 this.$el.removeClass( 'expanded' );
428 }
429 this.scaleEditor();
430 },
431
432 /**
433 * Show the visual editor view.
434 */
435 showVisualEditor: function () {
436 this.visualProperties.loadCSS( this.codeMirror.getValue().trim() );
437 this.visualProperties.show();
438 },
439
440 /**
441 * Set the snippets available to this editor
442 */
443 setSnippets: function ( snippets ) {
444 if ( !_.isEmpty( snippets ) ) {
445
446 this.snippets = new socss.view.snippets( {
447 snippets: snippets
448 } );
449 this.snippets.editor = this;
450
451 this.snippets.render();
452 this.toolbar.addButton( 'Snippets', 'snippets' );
453 this.toolbar.on( 'click_snippets', function () {
454 this.snippets.show();
455 }.bind( this ) );
456 }
457 },
458
459 /**
460 * Add some CSS to the editor.
461 * @param css
462 */
463 addCode: function ( css ) {
464 var editor = this.codeMirror;
465
466 var before_css = '';
467 if ( editor.doc.lineCount() === 1 && editor.doc.getLine( editor.doc.lastLine() ).length === 0 ) {
468 before_css = "";
469 }
470 else if ( editor.doc.getLine( editor.doc.lastLine() ).length === 0 ) {
471 before_css = "\n";
472 }
473 else {
474 before_css = "\n\n";
475 }
476
477 // Now insert the code in the editor
478 editor.doc.setCursor(
479 editor.doc.lastLine(),
480 editor.doc.getLine( editor.doc.lastLine() ).length
481 );
482 editor.doc.replaceSelection( before_css + css );
483 },
484
485 addEmptySelector: function ( selector ) {
486 this.addCode( selector + " {\n \n}" );
487 },
488
489 /**
490 * Sets the inspector view that's being used by the editor
491 */
492 setInspector: function ( inspector ) {
493 this.inspector = inspector;
494 this.cssSelectors = inspector.pageSelectors;
495
496 // A selector is clicked in the inspector
497 inspector.on( 'click_selector', function ( selector ) {
498 if ( this.visualProperties.isVisible() ) {
499 this.visualProperties.addSelector( selector );
500 }
501 else {
502 this.addEmptySelector( selector );
503 }
504 }.bind( this ) );
505
506 // A property is clicked in the inspector
507 inspector.on( 'click_property', function ( property ) {
508 if ( !this.visualProperties.isVisible() ) {
509 this.codeMirror.replaceSelection( property + ";\n " );
510 }
511 }.bind( this ) );
512
513 inspector.on( 'set_active_element', function ( el, selectors ) {
514 if ( this.visualProperties.isVisible() && selectors.length ) {
515 this.visualProperties.addSelector( selectors[ 0 ].selector );
516 }
517 }.bind( this ) );
518 }
519
520 } );
521
522 /**
523 * The preview.
524 */
525 socss.view.preview = Backbone.View.extend( {
526
527 template: _.template( $( '#template-preview-window' ).html() ),
528 editor: null,
529 originalUri: null,
530 currentUri: null,
531
532 events: {
533 'mouseleave #preview-iframe': 'clearHighlight',
534 'keydown #preview-navigator input[type="text"]': 'reloadPreview',
535 },
536
537 initialize: function ( attr ) {
538 this.editor = attr.editor;
539
540 this.listenTo( this.model, 'change:selectedPost', this.render.bind( this ) );
541
542 this.originalUri = new URI( attr.initURL );
543 this.currentUri = new URI( attr.initURL );
544
545 this.editor.codeMirror.on( 'change', function ( cm, c ) {
546 this.updatePreviewCss();
547 }.bind( this ) );
548 },
549
550 render: function () {
551
552 var selectedPost = this.model.get( 'selectedPost' );
553
554 if ( selectedPost && !selectedPost.has( 'postUrl' ) ) {
555 selectedPost.fetch().then( this.render.bind( this ) );
556 return this;
557 }
558
559 this.$el.html( this.template() );
560
561 if ( selectedPost ) {
562 this.currentUri = new URI( selectedPost.get( 'postUrl' ) );
563 }
564
565 this.currentUri.removeQuery( 'so_css_preview', 1 );
566 this.$( '#preview-navigator input' ).val( this.currentUri.toString() );
567 this.currentUri.addQuery( 'so_css_preview', 1 );
568
569 this.$( '#preview-iframe' )
570 .attr( 'src', this.currentUri.toString() )
571 // 'load' event doesn't bubble so can't be used in the events hash
572 .on( 'load', this.initPreview.bind( this ) );
573 },
574
575 initPreview: function () {
576 var $$ = this.$( '#preview-iframe' );
577
578 // Update the current URI with the iframe URI
579 this.currentUri = new URI( $$.contents().get( 0 ).location.href );
580 this.currentUri.removeQuery( 'so_css_preview' );
581 this.$( '#preview-navigator input' ).val( this.currentUri.toString() );
582 this.currentUri.addQuery( 'so_css_preview', 1 );
583
584 var wcCheck = $$.contents().find( '.single-product' ).length;
585 $$.contents().find( 'a' ).each( function () {
586 var link = $( this );
587 var href = link.attr( 'href' );
588 if ( href === undefined || ( wcCheck && link.parents( '.wc-tabs' ).length ) ) {
589 return true;
590 }
591
592 var firstSeperator = ( href.indexOf( '?' ) === -1 ? '?' : '&' );
593 link.attr( 'href', href + firstSeperator + 'so_css_preview=1' );
594 } );
595
596 this.updatePreviewCss();
597 },
598
599 reloadPreview: function ( e ) {
600 var $$ = this.$( '#preview-navigator input[type="text"]' );
601
602 if ( e.keyCode === 13 ) {
603 e.preventDefault();
604
605 var newUri = new URI( $$.val() );
606
607 // Validate the URI
608 if (
609 this.originalUri.host() !== newUri.host() ||
610 this.originalUri.protocol() !== newUri.protocol()
611 ) {
612 $$.trigger( 'blur' );
613 alert( $$.data( 'invalid-uri' ) );
614 $$.trigger( 'focus' );
615 }
616 else {
617 newUri.addQuery( 'so_css_preview', 1 );
618 this.$( '#preview-iframe' ).attr( 'src', newUri.toString() );
619 }
620 }
621 },
622
623 /**
624 * Update the preview CSS from the CodeMirror value in the editor
625 */
626 updatePreviewCss: function () {
627 var preview = this.$( '#preview-iframe' );
628 if ( preview.length === 0 ) {
629 return;
630 }
631
632 var head = preview.contents().find( 'head' );
633 if ( head.find( 'style.siteorigin-custom-css' ).length === 0 ) {
634 head.append( '<style class="siteorigin-custom-css" type="text/css"></style>' );
635 }
636 var style = head.find( 'style.siteorigin-custom-css' );
637
638 // Update the CSS after a short delay
639 var css = this.editor.codeMirror.getValue().trim();
640 style.html( css );
641 },
642
643 /**
644 * Highlight all elements with a given selector
645 */
646 highlight: function ( selector ) {
647 try {
648 this.editor.inspector.hl.highlight( selector );
649 }
650 catch ( err ) {
651 console.log( 'No inspector to highlight with' );
652 }
653 },
654
655 /**
656 * Clear the currently highlighted elements in preview
657 */
658 clearHighlight: function () {
659 try {
660 this.editor.inspector.hl.clear();
661 }
662 catch ( err ) {
663 console.log( 'No inspector to highlight with' );
664 }
665 }
666
667 } );
668
669 /**
670 * The dialog for the snippets browser
671 */
672 socss.view.snippets = Backbone.View.extend( {
673 template: _.template( $( '#template-snippet-browser' ).html() ),
674 snippet: _.template( '<li class="snippet"><%- name %></li>' ),
675 className: 'css-editor-snippet-browser',
676 snippets: null,
677 editor: null,
678
679 events: {
680 'click .close': 'hide',
681 'click .buttons .insert-snippet': 'insertSnippet',
682 'click .snippet': 'clickSnippet',
683 },
684
685 currentSnippet: null,
686
687 initialize: function ( args ) {
688 this.snippets = args.snippets;
689 },
690
691 render: function () {
692 this.$el.html( this.template() );
693 for ( var i = 0; i < this.snippets.length; i++ ) {
694 $( this.snippet( { name: this.snippets[ i ].Name } ) )
695 .data( {
696 'description': this.snippets[ i ].Description,
697 'css': this.snippets[ i ].css
698 } )
699 .appendTo( this.$( 'ul.snippets' ) );
700 }
701
702 // Click on the first one
703 this.$( '.snippets li.snippet' ).eq( 0 ).trigger( 'click' );
704
705 this.attach();
706 return this;
707 },
708
709 clickSnippet: function ( event ) {
710 event.preventDefault();
711 var $$ = $( event.currentTarget );
712
713 this.$( '.snippets li.snippet' ).removeClass( 'active' );
714 $( this ).addClass( 'active' );
715 this.viewSnippet( {
716 name: $$.html(),
717 description: $$.data( 'description' ),
718 css: $$.data( 'css' )
719 } );
720 },
721
722 viewSnippet: function ( args ) {
723 var w = this.$( '.main .snippet-view' );
724
725 w.find( '.snippet-title' ).html( args.name );
726 w.find( '.snippet-description' ).html( args.description );
727 w.find( '.snippet-code' ).html( args.css );
728
729 this.currentSnippet = args;
730 },
731
732 insertSnippet: function () {
733 var editor = this.editor.codeMirror;
734 var css = this.currentSnippet.css;
735
736 var before_css = '';
737 if ( editor.doc.lineCount() === 1 && editor.doc.getLine( editor.doc.lastLine() ).length === 0 ) {
738 before_css = "";
739 }
740 else if ( editor.doc.getLine( editor.doc.lastLine() ).length === 0 ) {
741 before_css = "\n";
742 }
743 else {
744 before_css = "\n\n";
745 }
746
747 // Now insert the code in the editor
748 editor.doc.setCursor(
749 editor.doc.lastLine(),
750 editor.doc.getLine( editor.doc.lastLine() ).length
751 );
752 editor.doc.replaceSelection( before_css + css );
753
754 this.hide();
755 },
756
757 attach: function () {
758 this.$el.appendTo( 'body' );
759 },
760
761 show: function () {
762 this.$el.show();
763 },
764
765 hide: function () {
766 this.$el.hide();
767 }
768 } );
769
770
771 /**
772 * The visual properties editor
773 */
774 socss.view.properties = Backbone.View.extend( {
775
776 tabTemplate: _.template( '<li data-section="<%- id %>"><span class="fa fa-<%- icon %>"></span> <%- title %></li>' ),
777 sectionTemplate: _.template( '<div class="section" data-section="<%- id %>"><table class="fields-table"><tbody></tbody></table></div>' ),
778 controllerTemplate: _.template( '<tr><th scope="row"><%- title %></th><td></td></tr>' ),
779
780 /**
781 * The controllers for each of the properties
782 */
783 propertyControllers: [],
784
785 /**
786 * The editor view
787 */
788 editor: null,
789
790 /**
791 * The current, raw CSS
792 */
793 css: '',
794
795 /**
796 * Parsed CSS
797 */
798 parsed: {},
799
800 /**
801 * The current active selector
802 */
803 activeSelector: '',
804
805 /**
806 * Was the editor expanded before we went into the property editor
807 */
808 editorExpandedBefore: false,
809
810 events: {
811 'click .close': 'hide',
812 'click .section-tabs li': 'onTabClick',
813 'change .toolbar select': 'onToolbarSelectChange',
814 },
815
816 /**
817 * Initialize the properties editor with a new model
818 */
819 initialize: function ( options ) {
820 this.parser = window.css;
821 this.editor = options.editor;
822 },
823
824 /**
825 * Render the property editor
826 */
827 render: function () {
828 // Clean up for potential re-renders
829 this.$( '.section-tabs' ).empty();
830 this.$( '.sections' ).empty();
831 this.$( '.toolbar select' ).off();
832 this.propertyControllers = [];
833
834 var controllers = socssOptions.propertyControllers;
835
836 for ( var id in controllers ) {
837 // Create the tabs
838 var $t = $( this.tabTemplate( {
839 id: id,
840 icon: controllers[ id ].icon,
841 title: controllers[ id ].title
842 } ) ).appendTo( this.$( '.section-tabs' ) );
843
844 // Create the section wrapper
845 var $s = $( this.sectionTemplate( {
846 id: id
847 } ) ).appendTo( this.$( '.sections' ) );
848
849 // Now lets add the controllers
850 if ( !_.isEmpty( controllers[ id ].controllers ) ) {
851
852 for ( var i = 0; i < controllers[ id ].controllers.length; i++ ) {
853
854 var $c = $( this.controllerTemplate( {
855 title: controllers[ id ].controllers[ i ].title
856 } ) ).appendTo( $s.find( 'tbody' ) );
857
858 var controllerAtts = controllers[ id ].controllers[ i ];
859 var controller;
860
861 if ( typeof socss.view.properties.controllers[ controllerAtts.type ] === 'undefined' ) {
862 // Setup a default controller
863 controller = new socss.view.propertyController( {
864 el: $c.find( 'td' ),
865 propertiesView: this,
866 args: ( typeof controllerAtts.args === 'undefined' ? {} : controllerAtts.args )
867 } );
868 }
869 else {
870 // Setup a specific controller
871 controller = new socss.view.properties.controllers[ controllerAtts.type ]( {
872 el: $c.find( 'td' ),
873 propertiesView: this,
874 args: ( typeof controllerAtts.args === 'undefined' ? {} : controllerAtts.args )
875 } );
876 }
877
878 this.propertyControllers.push( controller );
879
880 // Setup and render the controller
881 controller.render();
882 }
883 }
884 }
885
886 // Switch to the first tab.
887 this.$( '.section-tabs li' ).eq( 0 ).trigger( 'click' );
888 },
889
890 onTabClick: function ( event ) {
891 var $$ = $( event.currentTarget );
892 var show = this.$( '.sections .section[data-section="' + $$.data( 'section' ) + '"]' );
893
894 this.$( '.sections .section' ).not( show ).hide().removeClass( 'active' );
895 show.show().addClass( 'active' );
896
897 this.$( '.section-tabs li' ).not( $$ ).removeClass( 'active' );
898 $$.addClass( 'active' );
899 },
900
901 onToolbarSelectChange: function ( event ) {
902 this.setActiveSelector( $( event.currentTarget ).find( ':selected' ).data( 'selector' ) );
903 },
904
905 /**
906 * Sets the rule value for the active selector
907 * @param rule
908 * @param value
909 */
910 setRuleValue: function ( rule, value ) {
911 if (
912 typeof this.activeSelector === 'undefined' ||
913 typeof this.activeSelector.declarations === 'undefined'
914 ) {
915 return;
916 }
917
918 var declarations = this.activeSelector.declarations;
919 var newRule = true;
920 var valueChanged = false;
921 for ( var i = 0; i < declarations.length; i++ ) {
922 if ( declarations[ i ].property === rule ) {
923 newRule = false;
924 var declaration = declarations[ i ];
925 if ( declaration.value !== value ) {
926 declaration.value = value;
927 valueChanged = true;
928 }
929
930 // Remove empty declarations
931 if ( _.isEmpty( declaration.value ) ) {
932 declarations.splice( declarations.indexOf( declaration ) );
933 }
934 break;
935 }
936 }
937
938 if ( newRule && !_.isEmpty( value ) ) {
939 declarations.push( {
940 property: rule,
941 value: value,
942 type: 'declaration',
943 } );
944 valueChanged = true;
945 }
946
947 if ( valueChanged ) {
948 this.updateMainEditor( false );
949 }
950 },
951
952 /**
953 * Adds the @import rule value if it doesn't already exist.
954 *
955 * @param newRule
956 *
957 */
958 addImport: function ( newRule ) {
959
960 // get @import rules
961 // check if any have the same value
962 // if not, then add the new @ rule
963
964 var importRules = _.filter( this.parsed.stylesheet.rules, function ( rule ) {
965 return rule.type === 'import';
966 } );
967 var exists = _.any( importRules, function ( rule ) {
968 return rule.import === newRule.import;
969 } );
970
971 if ( !exists ) {
972 // Add it to the top!
973 // @import statements must precede other rule types.
974 this.parsed.stylesheet.rules.unshift( newRule );
975 this.updateMainEditor( false );
976 }
977
978 },
979
980 /**
981 * Find @import which completely or partially contains the specified value.
982 *
983 * @param value
984 */
985 findImport: function ( value ) {
986 return _.find( this.parsed.stylesheet.rules, function ( rule ) {
987 return rule.type === 'import' && rule.import.indexOf( value ) > -1;
988 } );
989 },
990
991 /**
992 * Find @import which completely or partially contains the identifier value and update it's import property.
993 *
994 * @param identifier
995 * @param value
996 */
997 updateImport: function ( identifier, value ) {
998 var importRule = this.findImport( identifier );
999 if ( importRule.import !== value.import ) {
1000 importRule.import = value.import;
1001 this.updateMainEditor( false );
1002 }
1003 },
1004
1005 /**
1006 * Find @import which completely or partially contains the identifier value and remove it.
1007 *
1008 * @param identifier
1009 */
1010 removeImport: function ( identifier ) {
1011 var importIndex = _.findIndex( this.parsed.stylesheet.rules, function ( rule ) {
1012 return rule.type === 'import' && rule.import.indexOf( identifier ) > -1;
1013 } );
1014 if ( importIndex > -1 ) {
1015 this.parsed.stylesheet.rules.splice( importIndex, 1 );
1016 }
1017 },
1018
1019 /**
1020 * Get the rule value for the active selector
1021 * @param rule
1022 */
1023 getRuleValue: function ( rule ) {
1024 if ( typeof this.activeSelector === 'undefined' || typeof this.activeSelector.declarations === 'undefined' ) {
1025 return '';
1026 }
1027
1028 var declarations = this.activeSelector.declarations;
1029 for ( var i = 0; i < declarations.length; i++ ) {
1030 if ( declarations[ i ].property === rule ) {
1031 return declarations[ i ].value;
1032 }
1033 }
1034 return '';
1035 },
1036
1037 /**
1038 * Update the main editor with the value of the parsed CSS
1039 */
1040 updateMainEditor: function ( compress ) {
1041 //TODO: add back compress option to remove/merge duplicated CSS selectors.
1042 this.editor.codeMirror.setValue( this.parser.stringify( this.parsed ) );
1043 },
1044
1045 /**
1046 * Show the properties editor
1047 */
1048 show: function () {
1049 this.editorExpandedBefore = this.editor.isExpanded();
1050 this.editor.setExpand( true );
1051
1052 this.$el.show().animate( { 'left': 0 }, 'fast' );
1053 },
1054
1055 /**
1056 * Hide the properties editor
1057 */
1058 hide: function () {
1059 this.editor.setExpand( this.editorExpandedBefore );
1060 this.$el.animate( { 'left': -338 }, 'fast', function () {
1061 $( this ).hide();
1062 } );
1063
1064 // Update the main editor with compressed CSS when we close the properties editor
1065 this.updateMainEditor( true );
1066 },
1067
1068 /**
1069 * @returns boolean
1070 */
1071 isVisible: function () {
1072 return this.$el.is( ':visible' );
1073 },
1074
1075 /**
1076 * Loads a single CSS selector and associated properties into the model
1077 * @param css
1078 */
1079 loadCSS: function ( css, activeSelector ) {
1080 this.css = css;
1081
1082 // Load the CSS
1083 this.parsed = this.parser.parse( css, {
1084 silent: true
1085 } );
1086 var rules = this.parsed.stylesheet.rules;
1087
1088 // Add the dropdown menu items
1089 var dropdown = this.$( '.toolbar select' ).empty();
1090 for ( var i = 0; i < rules.length; i++ ) {
1091 var rule = rules[ i ];
1092
1093 // Exclude @import statements
1094 if ( !_.contains( [ 'rule', 'media' ], rule.type ) ) {
1095 continue;
1096 }
1097
1098 if ( rule.type === 'media' ) {
1099
1100 for ( var j = 0; j < rule.rules.length; j++ ) {
1101 var mediaRule = '@media ' + rule.media;
1102 var subRule = rule.rules[ j ];
1103 if ( subRule.type != 'rule' ) {
1104 continue;
1105 }
1106 dropdown.append(
1107 $( '<option>' )
1108 .html( mediaRule + ': ' + subRule.selectors.join( ',' ) )
1109 .attr( 'val', mediaRule + ': ' + subRule.selectors.join( ',' ) )
1110 .data( 'selector', subRule )
1111 );
1112 }
1113
1114 }
1115 else {
1116 dropdown.append(
1117 $( '<option>' )
1118 .html( rule.selectors.join( ',' ) )
1119 .attr( 'val', rule.selectors.join( ',' ) )
1120 .data( 'selector', rule )
1121 );
1122 }
1123 }
1124
1125 if ( typeof activeSelector === 'undefined' ) {
1126 activeSelector = dropdown.find( 'option' ).eq( 0 ).attr( 'val' );
1127 }
1128 if ( !_.isEmpty( activeSelector ) ) {
1129 dropdown.val( activeSelector ).trigger( 'change' );
1130 }
1131 },
1132
1133 /**
1134 * Set the selector that we're currently dealing with
1135 * @param selector
1136 */
1137 setActiveSelector: function ( selector ) {
1138 this.activeSelector = selector;
1139 for ( var i = 0; i < this.propertyControllers.length; i++ ) {
1140 this.propertyControllers[ i ].refreshFromRule();
1141 }
1142 },
1143
1144 /**
1145 * Add or select a selector.
1146 *
1147 * @param selector
1148 */
1149 addSelector: function ( selector ) {
1150 // Check if this selector already exists
1151 var dropdown = this.$( '.toolbar select' );
1152 dropdown.val( selector );
1153
1154 if ( dropdown.val() === selector ) {
1155 // Trigger a change event to load the existing selector
1156 dropdown.trigger( 'change' );
1157 }
1158 else {
1159 // The selector doesn't exist, so add it to the CSS, then reload
1160 this.editor.addEmptySelector( selector );
1161 this.loadCSS( this.editor.codeMirror.getValue().trim(), selector );
1162 }
1163
1164 dropdown.addClass( 'highlighted' );
1165 setTimeout( function () {
1166 dropdown.removeClass( 'highlighted' );
1167 }, 2000 );
1168 }
1169
1170 } );
1171
1172 // The basic property controller
1173 socss.view.propertyController = Backbone.View.extend( {
1174
1175 template: _.template( '<input type="text" value="" class="socss-property-controller-input"/>' ),
1176 activeRule: null,
1177 args: null,
1178 propertiesView: null,
1179
1180 events: {
1181 'change .socss-property-controller-input': 'onChange',
1182 'keyup input.socss-property-controller-input': 'onChange',
1183 },
1184
1185 initialize: function ( args ) {
1186
1187 this.args = args.args;
1188 this.propertiesView = args.propertiesView;
1189
1190 // If sub-views items define their own events hash with the same keys as above they will override those on
1191 // the above events hash.
1192 this.events = _.extend( socss.view.propertyController.prototype.events, this.events );
1193 this.delegateEvents( this.events );
1194
1195 // By default, update the active rule whenever things change
1196 this.on( 'set_value', this.updateRule, this );
1197 this.on( 'change', this.updateRule, this );
1198 },
1199
1200 /**
1201 * Render the property field controller
1202 */
1203 render: function () {
1204 this.$el.append( $( this.template( {} ) ) );
1205 this.field = this.$( 'input.socss-property-controller-input' );
1206 },
1207
1208 onChange: function () {
1209 this.trigger( 'change', this.field.val() );
1210 },
1211
1212 /**
1213 * Update the value of an active rule
1214 */
1215 updateRule: function () {
1216 this.propertiesView.setRuleValue(
1217 this.args.property,
1218 this.getValue()
1219 );
1220 },
1221
1222 /**
1223 * This is called when the selector changes
1224 */
1225 refreshFromRule: function () {
1226 var value = this.propertiesView.getRuleValue( this.args.property );
1227 this.setValue( value, { silent: true } );
1228 },
1229
1230 /**
1231 * Get the current value
1232 * @return string
1233 */
1234 getValue: function () {
1235 return this.field.val();
1236 },
1237
1238 /**
1239 * Set the current value
1240 * @param socss.view.properties val
1241 */
1242 setValue: function ( val, options ) {
1243 options = _.extend( { silent: false }, options );
1244
1245 this.field.val( val );
1246
1247 if ( !options.silent ) {
1248 this.trigger( 'set_value', val );
1249 }
1250 },
1251
1252 /**
1253 * Reset the current value
1254 */
1255 reset: function ( options ) {
1256 options = _.extend( { silent: false }, options );
1257
1258 this.setValue( '', options );
1259 }
1260
1261 } );
1262
1263 // All the value controllers
1264 socss.view.properties.controllers = {};
1265
1266 // The color controller
1267 socss.view.properties.controllers.color = socss.view.propertyController.extend( {
1268
1269 render: function () {
1270 socss.view.propertyController.prototype.render.apply( this, arguments );
1271 // Set this up as a color picker
1272 this.field.minicolors( {} );
1273
1274 },
1275
1276 onChange: function () {
1277 this.trigger( 'change', this.field.minicolors( 'value' ) );
1278 },
1279
1280 getValue: function () {
1281 return this.field.minicolors( 'value' ).trim();
1282 },
1283
1284 setValue: function ( val, options ) {
1285 options = _.extend( { silent: false }, options );
1286
1287 this.field.minicolors( 'value', val );
1288
1289 if ( !options.silent ) {
1290 this.trigger( 'set_value', val );
1291 }
1292 }
1293
1294 } );
1295
1296 // The dropdown select box controller
1297 socss.view.properties.controllers.select = socss.view.propertyController.extend( {
1298 template: _.template( '<select class="socss-property-controller-input"></select>' ),
1299
1300 events: {
1301 'click .select-tab': 'onSelect',
1302 },
1303
1304 render: function () {
1305 this.$el.append( $( this.template( {} ) ) );
1306 this.field = this.$( 'select' );
1307
1308 // Add the unchanged option
1309 this.field.append( $( '<option value=""></option>' ).html( '' ) );
1310
1311 // Add all the options to the dropdown
1312 for ( var k in this.args.options ) {
1313 this.field.append( $( '<option></option>' ).attr( 'value', k ).html( this.args.options[ k ] ) );
1314 }
1315
1316 if ( typeof this.args.option_icons !== 'undefined' ) {
1317 this.setupVisualSelect();
1318 }
1319 },
1320
1321 setupVisualSelect: function () {
1322 this.field.hide();
1323
1324 var $tc = $( '<div class="select-tabs"></div>' ).appendTo( this.$el );
1325
1326 // Add the none value
1327 $( '<div class="select-tab" data-value=""><span class="fa fa-circle-o"></span></div>' ).appendTo( $tc );
1328
1329 // Now add one for each of the option icons
1330 for ( var k in this.args.option_icons ) {
1331 $( '<div class="select-tab"></div>' )
1332 .appendTo( $tc )
1333 .append(
1334 $( '<span class="fa"></span>' )
1335 .addClass( 'fa-' + this.args.option_icons[ k ] )
1336 )
1337 .attr( 'data-value', k )
1338 ;
1339 }
1340
1341 $tc.find( '.select-tab' ).css( 'width', 100 / ( $tc.find( '>div' ).length ) + "%" );
1342 },
1343
1344 onSelect: function ( event ) {
1345 this.$( '.select-tab' ).removeClass( 'active' );
1346 var $t = $( event.currentTarget );
1347 $t.addClass( 'active' );
1348 this.field.val( $t.data( 'value' ) ).trigger( 'change' );
1349 },
1350
1351 /**
1352 * Set the current value
1353 * @param socss.view.properties val
1354 */
1355 setValue: function ( val, options ) {
1356 options = _.extend( { silent: false }, options );
1357
1358 this.field.val( val );
1359
1360 this.$( '.select-tabs .select-tab' ).removeClass( 'active' ).filter( '[data-value="' + val + '"]' ).addClass( 'active' );
1361
1362 if ( !options.silent ) {
1363 this.trigger( 'set_value', val );
1364 }
1365 }
1366
1367 } );
1368
1369 // A field that lets a user upload an image
1370 socss.view.properties.controllers.image = socss.view.propertyController.extend( {
1371 template: _.template( '<input type="text" value="" /> <span class="select socss-button"><span class="fa fa-upload"></span></span>' ),
1372
1373 events: {
1374 'click .select': 'openMedia',
1375 },
1376
1377 render: function () {
1378 this.media = wp.media( {
1379 // Set the title of the modal.
1380 title: socssOptions.loc.select_image,
1381
1382 // Tell the modal to show only images.
1383 library: {
1384 type: 'image'
1385 },
1386
1387 // Customize the submit button.
1388 button: {
1389 // Set the text of the button.
1390 text: socssOptions.loc.select,
1391 // Tell the button not to close the modal, since we're
1392 // going to refresh the page when the image is selected.
1393 close: false
1394 }
1395 } );
1396
1397 this.$el.append( $( this.template( {
1398 select: socssOptions.loc.select
1399 } ) ) );
1400
1401 this.field = this.$el.find( 'input' );
1402
1403 this.media.on( 'select', function () {
1404 // Grab the selected attachment.
1405 var attachment = this.media.state().get( 'selection' ).first().attributes;
1406 var val = this.args.value.replace( '{{url}}', attachment.url );
1407
1408 // Change the field value and trigger a change event
1409 this.field.val( val ).trigger( 'change' );
1410 this.trigger( 'set_value', val );
1411
1412 // Close the image selector
1413 this.media.close();
1414
1415 }.bind( this ) );
1416 },
1417
1418 openMedia: function () {
1419 this.media.open();
1420 },
1421
1422 } );
1423
1424 // A simple measurement field
1425 socss.view.properties.controllers.measurement = socss.view.propertyController.extend( {
1426
1427 wrapperClass: 'socss-field-measurement',
1428
1429 events: {
1430 'click .toggle-dropdown': 'toggleUnitDropdown',
1431 'click .dropdown li': 'onSelectUnit',
1432 'keydown .socss-field-input': 'onInputKeyPress',
1433 'keyup .socss-field-input': 'onInputKeyUp',
1434 },
1435
1436 render: function () {
1437 socss.view.propertyController.prototype.render.apply( this, arguments );
1438
1439 this.setupMeasurementField();
1440 },
1441
1442 setValue: function ( val, options ) {
1443 options = _.extend( { silent: false }, options );
1444 this.field.val( val ).trigger( 'measurement_refresh' );
1445 if ( !options.silent ) {
1446 this.trigger( 'set_value', val );
1447 }
1448 },
1449
1450 units: [
1451 'px',
1452 '%',
1453 'em',
1454 'cm',
1455 'mm',
1456 'in',
1457 'pt',
1458 'pc',
1459 'ex',
1460 'ch',
1461 'rem',
1462 'vw',
1463 'vh',
1464 'vmin',
1465 'vmax'
1466 ],
1467
1468 parseUnits: function ( value ) {
1469 var escapeRegExp = function ( str ) {
1470 return str.replace( /[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&" );
1471 };
1472
1473 var regexUnits = this.units.map( escapeRegExp );
1474 var regex = new RegExp( '([0-9\\.\\-]+)(' + regexUnits.join( '|' ) + ')?', 'i' );
1475 var result = regex.exec( value );
1476
1477 if ( result === null ) {
1478 return {
1479 value: '',
1480 unit: ''
1481 };
1482 }
1483 else {
1484 return {
1485 value: result[ 1 ],
1486 unit: result[ 2 ] === undefined ? '' : result[ 2 ]
1487 };
1488 }
1489 },
1490
1491 setupMeasurementField: function () {
1492 var defaultUnit = 'px';
1493
1494 this.field.hide();
1495 this.$el.addClass( this.wrapperClass ).data( 'unit', defaultUnit );
1496
1497 // Create the fake input field
1498 var $fi = $( '<input type="text" class="socss-field-input"/>' ).appendTo( this.$el );
1499 $( '<span class="toggle-dropdown dashicons dashicons-arrow-down"></span>' ).appendTo( this.$el );
1500 var $dd = $( '<ul class="dropdown"></ul>' ).appendTo( this.$el );
1501 var $u = $( '<span class="units"></span>' ).html( defaultUnit ).appendTo( this.$el );
1502
1503 for ( var i = 0; i < this.units.length; i++ ) {
1504 var $o = $( '<li></li>' ).html( this.units[ i ] ).data( 'unit', this.units[ i ] );
1505 if ( this.units[ i ] === defaultUnit ) {
1506 $o.addClass( 'active' );
1507 }
1508 $dd.append( $o );
1509 }
1510
1511 this.field.on( 'measurement_refresh', function () {
1512 var value = this.parseUnits( this.field.val() );
1513 $fi.val( value.value );
1514
1515 var unit = value.unit === '' ? defaultUnit : value.unit;
1516 this.$el.data( 'unit', unit );
1517 $u.html( unit );
1518
1519 var $pl = $( '<span class="socss-hidden-placeholder"></span>' )
1520 .css( {
1521 'font-size': '14px'
1522 } )
1523 .html( value.value )
1524 .appendTo( 'body' );
1525 var width = $pl.width();
1526 width = Math.min( width, 63 );
1527 $pl.remove();
1528
1529 $u.css( 'left', width + 12 );
1530 }.bind( this ) );
1531
1532 // Now add the increment/decrement buttons
1533 var $diw = $( '<div class="socss-diw"></div>' ).appendTo( this.$el );
1534 var $dec = $( '<div class="dec-button socss-button"><span class="fa fa-minus"></span></div>' ).appendTo( $diw );
1535 var $inc = $( '<div class="inc-button socss-button"><span class="fa fa-plus"></span></div>' ).appendTo( $diw );
1536
1537 this.setupStepButton( $dec );
1538 this.setupStepButton( $inc );
1539
1540 },
1541
1542 updateValue: function () {
1543 var $fi = this.$( '.socss-field-input' );
1544 var value = this.parseUnits( $fi.val() );
1545
1546 if ( value.unit !== '' && value.unit !== this.$el.data( 'unit' ) ) {
1547 $fi.val( value.value );
1548 this.setUnit( value.unit );
1549 }
1550
1551 if ( value.value === '' ) {
1552 this.field.val( '' );
1553 }
1554 else {
1555 this.field.val( value.value + this.$el.data( 'unit' ) );
1556 }
1557 this.field.trigger( 'change' );
1558 },
1559
1560 setUnit: function ( unit ) {
1561 this.$( '.units' ).html( unit );
1562 this.$el.data( 'unit', unit );
1563 this.$( '.socss-field-input' ).trigger( 'keydown' );
1564 },
1565
1566 toggleUnitDropdown: function () {
1567 this.$( '.dropdown' ).toggle();
1568 },
1569
1570 onSelectUnit: function ( event ) {
1571 this.toggleUnitDropdown();
1572 this.setUnit( $( event.currentTarget ).data( 'unit' ) );
1573 this.updateValue();
1574 },
1575
1576 onInputKeyUp: function( event ) {
1577 this.onInputKeyPress( event );
1578 this.updateValue();
1579 },
1580
1581 onInputKeyPress: function ( event ) {
1582 var $fi = this.$( '.socss-field-input' );
1583
1584 var char = '';
1585 if ( event.type === 'keydown' ) {
1586 if ( event.keyCode >= 48 && event.keyCode <= 57 ) {
1587 char = String.fromCharCode( event.keyCode );
1588 }
1589 else if ( event.keyCode === 189 ) {
1590 char = '-';
1591 }
1592 else if ( event.keyCode === 190 ) {
1593 char = '.';
1594 }
1595 }
1596
1597 var $pl = $( '<span class="socss-hidden-placeholder"></span>' )
1598 .css( {
1599 'font-size': '14px'
1600 } )
1601 .html( $fi.val() + char )
1602 .appendTo( 'body' );
1603 var width = $pl.width();
1604 width = Math.min( width, 63 );
1605 $pl.remove();
1606
1607 this.$( '.units' ).css( 'left', width + 12 );
1608 },
1609
1610 stepValue: function ( direction ) {
1611 var value = Number.parseInt( this.parseUnits( this.field.val() ).value );
1612
1613 if ( Number.isNaN( value ) ) {
1614 value = 0;
1615 }
1616
1617 var newVal = value + direction;
1618
1619 this.$( '.socss-field-input' ).val( newVal );
1620 this.updateValue();
1621 this.field.trigger( 'measurement_refresh' );
1622 },
1623
1624 setupStepButton: function ( $button ) {
1625 var direction = $button.is( '.dec-button' ) ? -1 : 1;
1626 var intervalId;
1627 var timeoutId;
1628 $button.on( 'mousedown', function() {
1629 this.stepValue( direction );
1630 timeoutId = setTimeout( function () {
1631 intervalId = setInterval( function () {
1632 this.stepValue( direction );
1633 }.bind( this ), 50 );
1634 }.bind( this ), 500 );
1635 }.bind( this ) ).on( 'mouseup mouseout', function () {
1636 if ( timeoutId ) {
1637 clearTimeout( timeoutId );
1638 timeoutId = null;
1639 }
1640 if ( intervalId ) {
1641 clearInterval( intervalId );
1642 intervalId = null;
1643 }
1644 } );
1645 },
1646 } );
1647
1648 // A simple measurement field
1649 socss.view.properties.controllers.number = socss.view.propertyController.extend( {
1650
1651 initialize: function ( args ) {
1652 socss.view.propertyController.prototype.initialize.apply( this, arguments );
1653
1654 this.args = _.extend( {
1655 change: null,
1656 default: 0,
1657 increment: 1,
1658 decrement: -1,
1659 max: null,
1660 min: null
1661 }, args.args );
1662 },
1663
1664 render: function () {
1665 socss.view.propertyController.prototype.render.apply( this, arguments );
1666
1667 this.setupNumberField();
1668 },
1669
1670 setupNumberField: function () {
1671
1672 this.$el.addClass( 'socss-field-number' );
1673
1674 // Now add the increment/decrement buttons
1675 var $diw = $( '<div class="socss-diw"></div>' ).appendTo( this.$el );
1676 var $dec = $( '<div class="dec-button socss-button"><span class="fa fa-minus"></span></div>' ).appendTo( $diw );
1677 var $inc = $( '<div class="inc-button socss-button"><span class="fa fa-plus"></span></div>' ).appendTo( $diw );
1678
1679 this.setupStepButton( $dec );
1680 this.setupStepButton( $inc );
1681
1682 return this;
1683 },
1684
1685 stepValue: function ( direction ) {
1686 var value = Number.parseFloat( this.field.val() );
1687
1688 if ( Number.isNaN( value ) ) {
1689 value = this.args.default;
1690 }
1691
1692 var newVal = value + direction;
1693
1694 newVal = Math.round( newVal * 100 ) / 100;
1695
1696 if ( this.args.max !== null ) {
1697 newVal = Math.min( this.args.max, newVal );
1698 }
1699
1700 if ( this.args.min !== null ) {
1701 newVal = Math.max( this.args.min, newVal );
1702 }
1703
1704 this.field.val( newVal );
1705 this.field.trigger( 'change' );
1706 },
1707
1708 setupStepButton: function ( $button ) {
1709 var direction = $button.is( '.dec-button' ) ? this.args.decrement : this.args.increment;
1710 var intervalId;
1711 var timeoutId;
1712 $button.on( 'mousedown', function() {
1713 this.stepValue( direction );
1714 timeoutId = setTimeout( function () {
1715 intervalId = setInterval( function () {
1716 this.stepValue( direction );
1717 }.bind( this ), 50 );
1718 }.bind( this ), 500 );
1719 }.bind( this ) ).on( 'mouseup mouseout', function () {
1720 if ( timeoutId ) {
1721 clearTimeout( timeoutId );
1722 timeoutId = null;
1723 }
1724 if ( intervalId ) {
1725 clearInterval( intervalId );
1726 intervalId = null;
1727 }
1728 } );
1729 },
1730
1731 } );
1732
1733
1734 socss.view.properties.controllers.sides = socss.view.propertyController.extend( {
1735
1736 template: _.template( $( '#template-sides-field' ).html().trim() ),
1737
1738 controllers: [],
1739
1740 events: {
1741 'click .select-tab': 'onTabClick',
1742 },
1743
1744 render: function () {
1745
1746 socss.view.propertyController.prototype.render.apply( this, arguments );
1747
1748 if ( !this.args.hasAll ) {
1749 this.$( '.select-tab' ).eq( 0 ).remove();
1750 this.$( '.select-tab' ).css( 'width', '25%' );
1751 }
1752
1753 if ( ! this.args.isRadius ) {
1754 this.$( '.select-tabs[data-type="radius"]' ).remove();
1755 } else {
1756 this.$( '.select-tabs[data-type="box"]' ).remove();
1757 }
1758
1759 this.$( '.select-tab' ).each( function ( index, element ) {
1760 var dir = $( element ).data( 'direction' );
1761
1762 var container = $( '<li class="side">' )
1763 .appendTo( this.$( '.sides' ) )
1764 .hide();
1765
1766 for ( var i = 0; i < this.args.controllers.length; i++ ) {
1767
1768 var controllerArgs = this.args.controllers[ i ];
1769
1770 if ( typeof socss.view.properties.controllers[ controllerArgs.type ] ) {
1771
1772 // Create the measurement view
1773 var property = '';
1774 if ( dir === 'all' ) {
1775 property = controllerArgs.args.propertyAll;
1776 }
1777 else {
1778 property = controllerArgs.args.property.replace( '{dir}', dir );
1779 }
1780
1781 var theseControllerArgs = _.extend( {}, controllerArgs.args, { property: property } );
1782
1783 var controller = new socss.view.properties.controllers[ controllerArgs.type ]( {
1784 el: $( '<div>' ).appendTo( container ),
1785 propertiesView: this.propertiesView,
1786 args: theseControllerArgs
1787 } );
1788
1789 // Setup and render the measurement controller and register it with the properties view
1790 controller.render();
1791 this.propertiesView.propertyControllers.push( controller );
1792 }
1793 }
1794
1795 }.bind( this ) );
1796
1797 // Select the first tab by default
1798 this.$( '.select-tab' ).eq( 0 ).click();
1799 },
1800
1801 onTabClick: function ( event ) {
1802 var $tabs = this.$( '.select-tab' );
1803 $tabs.removeClass( 'active' );
1804
1805 var $tab = $( event.currentTarget );
1806 $tab.addClass( 'active' );
1807
1808 var $sides = this.$( '.sides .side' )
1809 $sides.hide();
1810
1811 $sides.eq( $tabs.index( $tab ) ).show();
1812 },
1813 } );
1814
1815 // This is a placeholder for the full font_select in SiteOrigin Premium
1816 socss.view.properties.controllers.font_select = socss.view.propertyController.extend( {
1817 template: _.template( $('#template-webfont-teaser').html().trim() )
1818 });
1819
1820 } )( jQuery, _, socssOptions );
1821
1822 // Setup the main editor
1823 jQuery( function ( $ ) {
1824 var socss = window.socss;
1825
1826 var editorModel = new socss.model.CSSEditorModel( {
1827 customCssPosts: socssOptions.customCssPosts,
1828 } );
1829
1830 // Setup the editor
1831 var editor = new socss.view.editor( {
1832 el: $( '#so-custom-css-form' ).get( 0 ),
1833 model: editorModel,
1834 openVisualEditor: socssOptions.openVisualEditor,
1835 } );
1836 // editor.render();
1837 editor.setSnippets( socssOptions.snippets );
1838
1839 // This is for hiding the getting started video
1840 $( '#so-custom-css-getting-started a.hide' ).on( 'click', function( e ) {
1841 e.preventDefault();
1842 $( '#so-custom-css-getting-started' ).slideUp();
1843 $.get( $( this ).attr( 'href' ) );
1844 } );
1845
1846 window.socss.mainEditor = editor;
1847 $( socss ).trigger( 'initialized' );
1848
1849 $( '.button-primary[name="siteorigin_custom_css_save"]' ).on( 'click', function() {
1850 $( '#so-custom-css-form' ).trigger( 'submit' );
1851 } );
1852 } );
1853