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