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