A new way of doing css/sass & New Canvas Theme Editor
what this does: * Changes the way we generate css so we are able to generate custom css for people that use the theme editor. * Sets everything up so we can push all of our static assets (js, fonts, css, images, etc) to s3 pre-deploy and serve them from cloudfront. Yay! faster canvas for everyone! * as part of that, this enables the rails asset pipeline just so we can use it to put md5s in our urls. we don't use it for any of the coffeescript/sass/sprockets transformer stuff. * adds a new "Theme editor" functionality (only for people that have have the use-new-styles feature flag turned on) where an admin for an account can pick their own colors/images for all the users at their account/school. * when the user is done saving things in theme editor, it will, in a delayed job, generate all the css with against the variables that user specified and push it to s3 so it will be available to anyone else that requests it. (the delayed job will shell out to a node.js executable called `brandable_css`). * ability to pick an existing shared theme and to reset to blank theme. closes: CNVS-19685 * gets rid of jammit. test plan: (this is exaustive, so not every person has to do every step but we should make sure at least someone does each of these things. maybe as part of the review add a comment if you have done one of these bulletpoints) * before you check this out, compile all css and copy the public/stylsheets_compiled directory somewhere. after you check out this code and regenerate all the css. make sure there are no significant changes to the css output. (we updated the versions of node-sass and autoprefixer that we use so we want to make sure they don't change things in a way we weren't expecting) * make sure the way we load css for handlebars templates still works. eg: if there is a handlebars template at app/views/jst/some/template.handlebars if there is also a scss file at app/stylesheets/jst/some/template.scss then that stylesheet should get loaded when that template is rendered * check out the code and run migrations. browse around canvas, make sure css and js files load correctly as before. * cody, jacob, or someone on queso: look at the db migrations and make sure everything looks good and that I am handling sharding correctly. * verify that both rake canvas:compile_assets and guard, works as well as `node_modules/.bin/brandable_css` (note: if you have "node_modules/.bin" in your PATH (which you should), it will also work with just `brandable_css`) * verify that passing the --watch option to `.bin/node_modules/brandable_css` works and picks up changes to sass files, images, fonts, or any other resource that goes into a css file. and that it only recompiles the css files that actually depend on that file. * go to https://github.com/ryankshaw/brandable_css and check out the code there. that is what is actually doing the sass compiling * create a config/canvas_cdn.yml file and add aws access creds and an s3 bucket and cdn hostname (for testing, you can use the credentials for instructure_uploads_engineering from https://gollum.instructure.com/OtherServiceTestAccounts ). for a test cdn hostname you can use https://diu0rq5m1weh1.cloudfront.net. that is a cloudfront bucket I set up on my personal account that points to instructure_uploads_engineering * run rake canvas:compile_assets again, this time, at the end, you should see it run the assets:precompile task that puts md5s in filenames and, gzipps them, and copys them to public/assets. then you should see it run canvas:cdn:upload_to_s3 (look at log/development.log for progress), which pushes everything to s3. closes: CNVS-17333 CNVS-17430 CNVS-17337 * try out the theme editor: turn on new styles, go to accounts/x (where x is the @domain root acount you are testing from) and click the "theme editor" button on the right side of the page. that should take you to a page that has the ability to pick colors/images on the left side and preview your changes in an iframe on the right closes: CNVS-19360 CNVS-20551 * test the "preview", "save", "reset", and "choose existing" functionality closes: CNVS-17339 CNVS-17338 CNVS-19685 * make sure that the themeeditor works both if you have config/canvas_cdn.yml set up and enabled as well as if you don't. if it is enabled, you should see it push the css for just that new brand config to s3 when you hit preview, and the css should be accessible from the cdn you configured. Change-Id: Ie0a812d04f5eeb40e7df7e71941ff63ea51a4d22 Reviewed-on: https://gerrit.instructure.com/53873 Tested-by: Jenkins QA-Review: Jeremy Putnam <jeremyp@instructure.com> Reviewed-by: Jacob Fugal <jacob@instructure.com> Product-Review: Ryan Shaw <ryan@instructure.com>
|
@ -14,8 +14,9 @@
|
|||
/.yardoc/
|
||||
/app/coffeescripts/ember/*/main.coffee
|
||||
/app/views/info/styleguide.html.erb
|
||||
app/stylesheets/_brandable_variables_defaults_autogenerated.scss
|
||||
app/stylesheets/brandable_css_brands
|
||||
/config/*.yml
|
||||
/config/brand_variables.scss
|
||||
/config/build.js
|
||||
/config/environments/*-local.rb
|
||||
/config/locales/generated/
|
||||
|
@ -31,14 +32,13 @@ Gemfile.lock3
|
|||
Gemfile.lock4
|
||||
/log/
|
||||
/node_modules
|
||||
/public/assets/
|
||||
/public/dist/
|
||||
/public/doc/api/
|
||||
/public/javascripts/compiled/
|
||||
/public/javascripts/jst/
|
||||
/public/javascripts/jsx/
|
||||
/public/javascripts/translations/
|
||||
/public/optimized/
|
||||
/public/stylesheets_compiled/
|
||||
/spec/javascripts/compiled/
|
||||
/spec/javascripts/requirejs_config.js
|
||||
/spec/javascripts/runner.html
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
group :development do
|
||||
gem 'guard', '1.8.0'
|
||||
gem 'guard-gulp'
|
||||
gem 'listen', '~>1.3' # pinned to fix guard error
|
||||
gem 'rb-inotify', '~>0.9.0', require: false
|
||||
gem 'rb-fsevent', require: false
|
||||
|
|
|
@ -49,9 +49,6 @@ gem 'i18nema19', '0.0.8', platform: :ruby_19
|
|||
gem 'i18nliner', '0.0.11'
|
||||
gem 'icalendar', '1.5.4', require: false
|
||||
gem 'ims-lti', '2.0.0.beta.26', require: false
|
||||
gem 'jammit', github: 'documentcloud/jammit', ref: '98b50a67029c2860717485a72a2ff0ae8ec37840'
|
||||
gem 'cssmin', '1.0.3', require: false
|
||||
gem 'jsmin', '1.0.1', require: false
|
||||
gem 'json', '1.8.2'
|
||||
gem 'oj', '2.5.5'
|
||||
gem 'jwt', '1.2.1', require: false
|
||||
|
|
|
@ -2,6 +2,7 @@ $LOAD_PATH << File.dirname(__FILE__)
|
|||
|
||||
ignore! Listen::DirectoryRecord::DEFAULT_IGNORED_DIRECTORIES - ['vendor'] + [%r{vendor/(?!plugins)}]
|
||||
|
||||
guard :brandable_css
|
||||
guard 'coffeescript', :input => 'app/coffeescripts', :output => 'public/javascripts/compiled'
|
||||
guard 'coffeescript', :input => 'spec/coffeescripts', :output => 'spec/javascripts/compiled'
|
||||
guard 'coffeescript', :input => 'spec_canvas/coffeescripts', :output => 'spec_canvas/javascripts'
|
||||
|
@ -11,5 +12,8 @@ guard :ember_templates
|
|||
guard :ember_bundles
|
||||
guard :styleguide
|
||||
guard :js_extensions
|
||||
guard :compass
|
||||
guard :jsx
|
||||
|
||||
guard 'gulp' do
|
||||
watch(%r{^gulpfile.babel.js$})
|
||||
end
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
require [
|
||||
'react'
|
||||
'jsx/theme_editor/ThemeEditor'
|
||||
], (React, ThemeEditor) ->
|
||||
|
||||
React.render(React.createElement(ThemeEditor, {
|
||||
brandConfig: window.ENV.brandConfig,
|
||||
variableSchema: window.ENV.variableSchema
|
||||
sharedBrandConfigs: window.ENV.sharedBrandConfigs
|
||||
}), document.body)
|
|
@ -0,0 +1,46 @@
|
|||
define ->
|
||||
loadedStylesheets = {}
|
||||
|
||||
brandableCss =
|
||||
getCssVariant: ->
|
||||
variant = if window.ENV.k12
|
||||
'k12'
|
||||
else if window.ENV.use_new_styles
|
||||
'new_styles'
|
||||
else
|
||||
'legacy'
|
||||
|
||||
contrast = if window.ENV.use_high_contrast
|
||||
'_high_contrast'
|
||||
else
|
||||
'_normal_contrast'
|
||||
|
||||
variant + contrast
|
||||
|
||||
|
||||
urlFor: (bundleName) ->
|
||||
brandPart = if window.ENV.active_brand_config
|
||||
'/' + window.ENV.active_brand_config
|
||||
else
|
||||
''
|
||||
return [
|
||||
window.ENV.ASSET_HOST || '',
|
||||
'dist'
|
||||
'brandable_css' + brandPart,
|
||||
brandableCss.getCssVariant(),
|
||||
bundleName + '.css'
|
||||
].join('/')
|
||||
|
||||
# bundleName needs to include the 'combinedChecksum'
|
||||
# eg: 'jst/foo-65ed0284f8f911179a6d5655ebbb8498'
|
||||
# 'jst/foo' will not work.
|
||||
loadStylesheet: (bundleName) ->
|
||||
return if bundleName of loadedStylesheets
|
||||
linkElement = document.createElement("link")
|
||||
linkElement.rel = "stylesheet"
|
||||
linkElement.href = brandableCss.urlFor(bundleName)
|
||||
|
||||
# give the person trying to track down a bug a hint on how
|
||||
# this link tag got on the page
|
||||
linkElement.setAttribute('data-loaded-by-brandableCss', true)
|
||||
document.head.appendChild(linkElement)
|
|
@ -1,39 +0,0 @@
|
|||
define ->
|
||||
|
||||
doc = window.document
|
||||
templatesWithStyles = {}
|
||||
|
||||
registerTemplateCss = (templateId, css) ->
|
||||
templatesWithStyles[templateId] = css
|
||||
render()
|
||||
|
||||
registerTemplateCss.clear = ->
|
||||
templatesWithStyles = {}
|
||||
render()
|
||||
|
||||
render = ->
|
||||
strings = []
|
||||
for templateId, css of templatesWithStyles
|
||||
strings.push "/* From: #{templateId} */"
|
||||
strings.push css
|
||||
combined = strings.join '\n'
|
||||
styleNode = cleanStyleNode()
|
||||
if 'cssText' of styleNode
|
||||
styleNode.cssText = combined
|
||||
else
|
||||
styleNode.appendChild doc.createTextNode(combined)
|
||||
|
||||
_styleNode = null
|
||||
cleanStyleNode = ->
|
||||
if _styleNode
|
||||
_styleNode.removeChild child while child = _styleNode.firstChild
|
||||
return _styleNode
|
||||
|
||||
if doc.createStyleSheet
|
||||
_styleNode = doc.createStyleSheet()
|
||||
else
|
||||
head = doc.head || doc.getElementsByTagName('head')[0]
|
||||
_styleNode = doc.createElement('style')
|
||||
head.appendChild(_styleNode)
|
||||
|
||||
return registerTemplateCss
|
|
@ -454,21 +454,6 @@ class AccountsController < ApplicationController
|
|||
params[:account].delete :services
|
||||
end
|
||||
if @account.grants_right?(@current_user, :manage_site_settings)
|
||||
|
||||
# handle branding stuff
|
||||
if @account.root_account? && params[:account][:settings]
|
||||
(Account::BRANDING_SETTINGS - [:msapplication_tile_color]).each do |setting|
|
||||
if params[:account][:settings]["#{setting}_remove"] == "1"
|
||||
params[:account][:settings][setting] = nil
|
||||
elsif params[:account][:settings][setting].present?
|
||||
attachment = Attachment.create(uploaded_data: params[:account][:settings][setting], context: @account)
|
||||
params[:account][:settings][setting] = attachment.authenticated_s3_url(:expires => 15.years)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
|
||||
# If the setting is present (update is called from 2 different settings forms, one for notifications)
|
||||
if params[:account][:settings] && params[:account][:settings][:outgoing_email_default_name_option].present?
|
||||
# If set to default, remove the custom name so it doesn't get saved
|
||||
|
@ -491,7 +476,7 @@ class AccountsController < ApplicationController
|
|||
end
|
||||
else
|
||||
# must have :manage_site_settings to update these
|
||||
([ :admins_can_change_passwords,
|
||||
[ :admins_can_change_passwords,
|
||||
:admins_can_view_notifications,
|
||||
:enable_alerts,
|
||||
:enable_eportfolios,
|
||||
|
@ -499,7 +484,7 @@ class AccountsController < ApplicationController
|
|||
:show_scheduler,
|
||||
:global_includes,
|
||||
:gmail_domain
|
||||
] + Account::BRANDING_SETTINGS).each do |key|
|
||||
].each do |key|
|
||||
params[:account][:settings].try(:delete, key)
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1658,16 +1658,35 @@ class ApplicationController < ActionController::Base
|
|||
super
|
||||
end
|
||||
|
||||
def jammit_css_bundles; @jammit_css_bundles ||= []; end
|
||||
helper_method :jammit_css_bundles
|
||||
def active_brand_config
|
||||
@active_brand_config ||= begin
|
||||
if !use_new_styles? || (@current_user && @current_user.prefers_high_contrast?)
|
||||
nil
|
||||
elsif session.key?(:brand_config_md5)
|
||||
BrandConfig.find(session[:brand_config_md5]) if session[:brand_config_md5]
|
||||
else
|
||||
@domain_root_account.brand_config
|
||||
end
|
||||
end
|
||||
end
|
||||
helper_method :active_brand_config
|
||||
|
||||
def jammit_css(*args)
|
||||
def css_bundles
|
||||
@css_bundles ||= []
|
||||
end
|
||||
helper_method :css_bundles
|
||||
|
||||
def css_bundle(*args)
|
||||
opts = (args.last.is_a?(Hash) ? args.pop : {})
|
||||
Array(args).flatten.each do |bundle|
|
||||
jammit_css_bundles << [bundle, opts[:plugin]] unless jammit_css_bundles.include? [bundle, opts[:plugin]]
|
||||
css_bundles << [bundle, opts[:plugin]] unless css_bundles.include? [bundle, opts[:plugin]]
|
||||
end
|
||||
nil
|
||||
end
|
||||
helper_method :css_bundle
|
||||
|
||||
alias_method :jammit_css, :css_bundle
|
||||
deprecate :jammit_css
|
||||
helper_method :jammit_css
|
||||
|
||||
def js_bundles; @js_bundles ||= []; end
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
class BrandConfigsController < ApplicationController
|
||||
before_filter :require_user
|
||||
before_filter :require_manage_site_settings, except: [:destroy]
|
||||
|
||||
def new
|
||||
brand_config = active_brand_config || BrandConfig.new
|
||||
@page_title = t('Canvas Theme Editor')
|
||||
css_bundle :common, :theme_editor
|
||||
js_bundle :theme_editor
|
||||
js_env brandConfig: brand_config.variables,
|
||||
variableSchema: BrandableCSS::BRANDABLE_VARIABLES,
|
||||
sharedBrandConfigs: BrandConfig.select('md5, name').where(share: true).as_json(include_root: false)
|
||||
render text: '', layout: 'layouts/bare'
|
||||
end
|
||||
|
||||
def create
|
||||
session[:brand_config_md5] = begin
|
||||
if params[:brand_config] == ''
|
||||
false
|
||||
elsif params[:brand_config][:md5]
|
||||
BrandConfig.find(params[:brand_config][:md5]).md5
|
||||
elsif (variables = params[:brand_config][:variables])
|
||||
create_brand_config(variables).md5
|
||||
end
|
||||
end
|
||||
redirect_to brand_configs_new_path
|
||||
end
|
||||
|
||||
def save_to_account
|
||||
@domain_root_account.update_attributes!(brand_config_md5: session[:brand_config_md5].presence)
|
||||
BrandConfig.clean_up_unused
|
||||
session.delete(:brand_config_md5)
|
||||
redirect_to :back, notice: t('Success! All users on this domain will now see this branding.')
|
||||
end
|
||||
|
||||
def destroy
|
||||
session.delete(:brand_config_md5)
|
||||
BrandConfig.clean_up_unused
|
||||
redirect_to :back, notice: t('Theme editor changes have been cancelled.')
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def require_manage_site_settings
|
||||
return false unless authorized_action(@domain_root_account, @current_user, :manage_site_settings) && use_new_styles?
|
||||
end
|
||||
|
||||
def create_brand_config(variables)
|
||||
variables_to_save = variables.each_with_object({}) do |(key, value), memo|
|
||||
next unless value.present? && (config = BrandableCSS.variables_map[key])
|
||||
value = upload_image(value) if config['type'] == 'image' && value.is_a?(ActionDispatch::Http::UploadedFile)
|
||||
memo[key] = value
|
||||
end
|
||||
brand_config = BrandConfig.create!(variables: variables_to_save)
|
||||
# TODO, show user progress of this
|
||||
brand_config.send_later_if_production(:save_and_sync_to_s3!)
|
||||
brand_config
|
||||
end
|
||||
|
||||
|
||||
def upload_image(image)
|
||||
attachment = Attachment.create(uploaded_data: image, context: @domain_root_account)
|
||||
expires_in = 15.years
|
||||
attachment.authenticated_s3_url({
|
||||
# this is how long the s3 verifier token will work
|
||||
expires: expires_in,
|
||||
# these are the http cache headers that will be set on the response
|
||||
response_expires: expires_in,
|
||||
response_cache_control: "Cache-Control:max-age=#{expires_in}, public"
|
||||
})
|
||||
end
|
||||
|
||||
end
|
|
@ -222,7 +222,6 @@ module ApplicationHelper
|
|||
def use_optimized_js?
|
||||
if ENV['USE_OPTIMIZED_JS'] == 'true'
|
||||
# allows overriding by adding ?debug_assets=1 or ?debug_js=1 to the url
|
||||
# (debug_assets is also used by jammit => you'll get unpackaged css AND js)
|
||||
!(params[:debug_assets] || params[:debug_js])
|
||||
else
|
||||
# allows overriding by adding ?optimized_js=1 to the url
|
||||
|
@ -247,7 +246,7 @@ module ApplicationHelper
|
|||
# Returns a <script> tag for each registered js_bundle
|
||||
def include_js_bundles
|
||||
paths = js_bundles.inject([]) do |ary, (bundle, plugin)|
|
||||
base_url = js_base_url
|
||||
base_url = "#{js_base_url}"
|
||||
base_url += "/plugins/#{plugin}" if plugin
|
||||
ary.concat(Canvas::RequireJs.extensions_for(bundle, 'plugins/')) unless use_optimized_js?
|
||||
ary << "#{base_url}/compiled/bundles/#{bundle}.js"
|
||||
|
@ -256,32 +255,50 @@ module ApplicationHelper
|
|||
end
|
||||
|
||||
def include_css_bundles
|
||||
unless jammit_css_bundles.empty?
|
||||
bundles = jammit_css_bundles.map do |(bundle,plugin)|
|
||||
bundle = variant_name_for(bundle)
|
||||
plugin ? "plugins_#{plugin}_#{bundle}" : bundle
|
||||
unless css_bundles.empty?
|
||||
bundles = css_bundles.map do |(bundle,plugin)|
|
||||
css_url_for(bundle, plugin)
|
||||
end
|
||||
bundles << {:media => 'all'}
|
||||
include_stylesheets(*bundles)
|
||||
stylesheet_link_tag(*bundles)
|
||||
end
|
||||
end
|
||||
|
||||
def variant_name_for(bundle_name)
|
||||
def css_variant
|
||||
if k12?
|
||||
variant = '_k12'
|
||||
variant = 'k12'
|
||||
elsif use_new_styles?
|
||||
variant = '_new_styles'
|
||||
variant = 'new_styles'
|
||||
else
|
||||
variant = '_legacy'
|
||||
variant = 'legacy'
|
||||
end
|
||||
|
||||
use_high_contrast = @current_user && @current_user.prefers_high_contrast?
|
||||
variant += use_high_contrast ? '_high_contrast' : '_normal_contrast'
|
||||
"#{bundle_name}#{variant}"
|
||||
variant + (use_high_contrast ? '_high_contrast' : '_normal_contrast')
|
||||
end
|
||||
|
||||
def css_url_for(bundle_name, plugin=false)
|
||||
bundle_path = "#{plugin ? "plugins/#{plugin}" : 'bundles'}/#{bundle_name}"
|
||||
content_md5 = BrandableCSS.fingerprint_for(bundle_path, css_variant)
|
||||
File.join('/dist', 'brandable_css', active_brand_config.try(:md5).to_s,
|
||||
css_variant, "#{bundle_path}-#{content_md5}.css")
|
||||
end
|
||||
|
||||
def brand_variable(variable_name)
|
||||
BrandableCSS.brand_variable_value(variable_name, active_brand_config)
|
||||
end
|
||||
|
||||
def favicon
|
||||
possibly_customized_favicon = brand_variable('ic-brand-favicon')
|
||||
default_favicon = BrandableCSS.brand_variable_value('ic-brand-favicon')
|
||||
if possibly_customized_favicon == default_favicon
|
||||
return "favicon-green.ico" if Rails.env.development?
|
||||
return "favicon-yellow.ico" if Rails.env.test?
|
||||
end
|
||||
possibly_customized_favicon
|
||||
end
|
||||
|
||||
def include_common_stylesheets
|
||||
include_stylesheets variant_name_for(:vendor), variant_name_for(:common), media: "all"
|
||||
stylesheet_link_tag css_url_for(:vendor), css_url_for(:common), media: "all"
|
||||
end
|
||||
|
||||
def sortable_tabs
|
||||
|
|
|
@ -0,0 +1,34 @@
|
|||
/** @jsx React.DOM */
|
||||
|
||||
define([
|
||||
'react',
|
||||
'i18n!theme_editor',
|
||||
'str/htmlEscape'
|
||||
], (React, I18n, htmlEscape) => {
|
||||
return React.createClass({
|
||||
|
||||
displayName: 'sharedBrandConfigPicker',
|
||||
|
||||
selectBrandConfig(md5) {
|
||||
$(`
|
||||
<form hidden method="POST" action="/brand_configs" method="POST">
|
||||
<input name="authenticity_token" type="hidden" value="${htmlEscape($.cookie('_csrf_token'))}" />
|
||||
<input name="brand_config[md5]" value="${htmlEscape(md5)}" />
|
||||
</form>
|
||||
`).appendTo('body').submit()
|
||||
},
|
||||
|
||||
render() {
|
||||
return (
|
||||
<select onChange={event => this.selectBrandConfig(event.target.value)}>
|
||||
<option value="" disabled>{I18n.t('Start from a template')}</option>
|
||||
{this.props.sharedBrandConfigs.map(brandConfig =>
|
||||
<option key={brandConfig.md5} value={brandConfig.md5}>
|
||||
{brandConfig.name}
|
||||
</option>
|
||||
)}
|
||||
</select>
|
||||
)
|
||||
}
|
||||
})
|
||||
});
|
|
@ -0,0 +1,188 @@
|
|||
/** @jsx React.DOM */
|
||||
|
||||
define([
|
||||
'i18n!theme_editor',
|
||||
'react',
|
||||
'str/htmlEscape',
|
||||
'compiled/fn/preventDefault',
|
||||
'./ThemeEditorAccordion',
|
||||
'./SharedBrandConfigPicker'
|
||||
], (I18n, React, htmlEscape, preventDefault, ThemeEditorAccordion, SharedBrandConfigPicker) => {
|
||||
|
||||
var ReactCSSTransitionGroup = React.addons.CSSTransitionGroup;
|
||||
|
||||
function findVarDef (variableSchema, variableName) {
|
||||
for (var i = 0; i < variableSchema.length; i++) {
|
||||
for (var j = 0; j < variableSchema[i].variables.length; j++) {
|
||||
var varDef = variableSchema[i].variables[j]
|
||||
if (varDef.variable_name === variableName){
|
||||
return varDef
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function submitHtmlForm(action, method) {
|
||||
$(`
|
||||
<form hidden action="${htmlEscape(action)}" method="POST">
|
||||
<input name="_method" type="hidden" value="${htmlEscape(method)}" />
|
||||
<input name="authenticity_token" type="hidden" value="${htmlEscape($.cookie('_csrf_token'))}" />
|
||||
<input name="brand_config" value="" />
|
||||
</form>
|
||||
`).appendTo('body').submit()
|
||||
}
|
||||
|
||||
return React.createClass({
|
||||
|
||||
displayName: 'ThemeEditor',
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
somethingChanged: false,
|
||||
changedValues: {}
|
||||
}
|
||||
},
|
||||
|
||||
somethingChanged(variableName, newValue) {
|
||||
this.state.changedValues[variableName] = newValue
|
||||
this.setState({
|
||||
somethingChanged: true,
|
||||
changedValues: this.state.changedValues
|
||||
})
|
||||
},
|
||||
|
||||
getDefault(variableName) {
|
||||
var val = this.state.changedValues[variableName]
|
||||
if (val) return val
|
||||
if (val !== '') {
|
||||
val = this.props.brandConfig[variableName]
|
||||
if (val) return val
|
||||
}
|
||||
val = findVarDef(this.props.variableSchema, variableName).default
|
||||
if (val && val[0] === '$') return this.getDefault(val.slice(1))
|
||||
return val
|
||||
},
|
||||
|
||||
resetToCanvasDefaults() {
|
||||
submitHtmlForm('/brand_configs', 'POST')
|
||||
},
|
||||
|
||||
cancelChanges() {
|
||||
var msg = I18n.t('This will just cancel the unsaved changes you ' +
|
||||
'have made in the Theme Editor. It will not affect ' +
|
||||
'anyone else and you will now see canvas as everyone ' +
|
||||
'else at your accout does.')
|
||||
|
||||
if(confirm(msg)) submitHtmlForm('/brand_configs', 'DELETE')
|
||||
},
|
||||
|
||||
saveToAccount() {
|
||||
var msg = I18n.t('This will apply the changes that you have made in ' +
|
||||
'the Theme Editor so everyone else at your accout will ' +
|
||||
'see Canvas as you are seeing it now. ' +
|
||||
'Are you sure you want to do this?')
|
||||
|
||||
if (confirm(msg)) submitHtmlForm('/brand_configs/save_to_account', 'POST')
|
||||
},
|
||||
|
||||
render() {
|
||||
return (
|
||||
<form encType="multipart/form-data" acceptCharset="UTF-8" action="/brand_configs" method="POST" className="Theme__container">
|
||||
<input name="utf8" type="hidden" value="✓" />
|
||||
<input name="authenticity_token" type="hidden" value={$.cookie('_csrf_token')} />
|
||||
<div className="Theme__editor">
|
||||
<div className="Theme__editor-header">
|
||||
<h1 className="Theme__editor-header_title">Theme Editor</h1>
|
||||
<div className="Theme__editor-header_actions">
|
||||
<div className="al-dropdown__container">
|
||||
<a className="al-trigger Button" role="button" href="#">
|
||||
<i className="icon-more"></i>
|
||||
<span className="screenreader-only">{I18n.t('Settings')}</span>
|
||||
</a>
|
||||
<ul
|
||||
className="al-options"
|
||||
role="menu"
|
||||
tabIndex="0"
|
||||
aria-hidden="true"
|
||||
aria-expanded="false">
|
||||
<li role="presentation" tabIndex="-1" role="menuitem">
|
||||
<a
|
||||
href="#"
|
||||
className="icon-reset"
|
||||
onClick={preventDefault(this.resetToCanvasDefaults)}
|
||||
>
|
||||
{I18n.t('Reset all to Canvas defaults')}
|
||||
</a>
|
||||
</li>
|
||||
<li role="presentation" tabIndex="-1" role="menuitem">
|
||||
<a
|
||||
href="#"
|
||||
className="icon-end"
|
||||
onClick={preventDefault(this.cancelChanges)}
|
||||
>
|
||||
{I18n.t('Cancel Changes')}
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<button
|
||||
type="button"
|
||||
className="Theme__editor-header_button Button Button--success"
|
||||
disabled={this.state.somethingChanged}
|
||||
title={this.state.somethingChanged ?
|
||||
I18n.t('"Preview Your Changes" before applying to everyone') :
|
||||
null
|
||||
}
|
||||
onClick={this.saveToAccount}
|
||||
>
|
||||
{I18n.t('Save')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{this.props.sharedBrandConfigs.length ?
|
||||
<SharedBrandConfigPicker sharedBrandConfigs={this.props.sharedBrandConfigs} />
|
||||
:
|
||||
null
|
||||
}
|
||||
<div id="Theme__editor-tabs">
|
||||
<div id="te-editor">
|
||||
<div className="Theme__editor-tabs_panel">
|
||||
<ThemeEditorAccordion
|
||||
variableSchema={this.props.variableSchema}
|
||||
brandConfig={this.props.brandConfig}
|
||||
getDefault={this.getDefault}
|
||||
changedValues={this.state.changedValues}
|
||||
somethingChanged={this.somethingChanged}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="Theme__preview">
|
||||
<div
|
||||
hidden={!this.state.somethingChanged}
|
||||
className="Theme__preview-overlay"
|
||||
>
|
||||
<div
|
||||
style={{
|
||||
width: 700,
|
||||
textAlign: 'center',
|
||||
height: '100vh',
|
||||
margin: '0 auto'
|
||||
}}
|
||||
>
|
||||
<button type="submit" className="Button Button--primary" style={{margin: '40% auto'}}>
|
||||
<i className="icon-refresh" />
|
||||
{I18n.t('Preview Your Changes')}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<iframe src="/" style={{border: 'none', width: '100%', height: '100vh'}} />
|
||||
</div>
|
||||
</form>
|
||||
|
||||
)
|
||||
}
|
||||
})
|
||||
});
|
|
@ -0,0 +1,63 @@
|
|||
/** @jsx React.DOM */
|
||||
|
||||
define([
|
||||
'react',
|
||||
'./ThemeEditorColorRow',
|
||||
'./ThemeEditorImageRow',
|
||||
'jquery',
|
||||
'jqueryui/accordion'
|
||||
], (React, ThemeEditorColorRow, ThemeEditorImageRow, $) => {
|
||||
return React.createClass({
|
||||
|
||||
displayName: 'ThemeEditorAccordion',
|
||||
|
||||
propTypes: {
|
||||
variableSchema: React.PropTypes.array.isRequired,
|
||||
brandConfig: React.PropTypes.object.isRequired,
|
||||
changedValues: React.PropTypes.object.isRequired,
|
||||
somethingChanged: React.PropTypes.func.isRequired,
|
||||
getDefault: React.PropTypes.func.isRequired
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
$(this.getDOMNode()).accordion({
|
||||
header: "h3",
|
||||
heightStyle: "content"
|
||||
})
|
||||
},
|
||||
|
||||
renderRow(varDef) {
|
||||
var props = {
|
||||
currentValue: this.props.brandConfig[varDef.variable_name],
|
||||
chosenValue: this.props.changedValues[varDef.variable_name],
|
||||
onChange: this.props.somethingChanged.bind(null, varDef.variable_name),
|
||||
placeholder: this.props.getDefault(varDef.variable_name),
|
||||
varDef: varDef
|
||||
}
|
||||
return varDef.type === 'color' ? ThemeEditorColorRow(props) : ThemeEditorImageRow(props)
|
||||
},
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="accordion ui-accordion--mini Theme__editor-accordion">
|
||||
{this.props.variableSchema.map(variableGroup =>
|
||||
[
|
||||
<h3>
|
||||
<a href="#">
|
||||
<div className="te-Flex">
|
||||
<span className="te-Flex__block">{variableGroup.group_name}</span>
|
||||
<i className="Theme__editor-accordion-icon icon-mini-arrow-right" />
|
||||
</div>
|
||||
</a>
|
||||
</h3>
|
||||
,
|
||||
<div>
|
||||
{variableGroup.variables.map(this.renderRow)}
|
||||
</div>
|
||||
]
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
})
|
||||
});
|
|
@ -0,0 +1,54 @@
|
|||
/** @jsx React.DOM */
|
||||
|
||||
define(['react'], (React) => {
|
||||
return React.createClass({
|
||||
|
||||
displayName: 'ThemeEditorColorRow',
|
||||
|
||||
propTypes: {
|
||||
varDef: React.PropTypes.object.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
currentValue: React.PropTypes.string,
|
||||
placeholder: React.PropTypes.string.isRequired
|
||||
},
|
||||
|
||||
render() {
|
||||
return (
|
||||
<section className="Theme__editor-accordion_element Theme__editor-color">
|
||||
<div className="Theme__editor-form--color">
|
||||
<label
|
||||
htmlFor={'brand_config[variables]['+ this.props.varDef.variable_name +']'}
|
||||
className="Theme__editor-color_title"
|
||||
>
|
||||
{this.props.varDef.human_name}
|
||||
</label>
|
||||
<div className="Theme__editor-color-block">
|
||||
<input
|
||||
type="text"
|
||||
className="Theme__editor-color-block_input-text Theme__editor-color-block_input"
|
||||
placeholder={this.props.placeholder}
|
||||
name={'brand_config[variables]['+ this.props.varDef.variable_name +']'}
|
||||
value={this.props.chosenValue != null ? this.props.chosenValue : this.props.currentValue}
|
||||
onChange={event => this.props.onChange(event.target.value) }
|
||||
/>
|
||||
<label className="Theme__editor-color-label Theme__editor-color-block_label-sample" style={{backgroundColor: this.props.placeholder}}
|
||||
/* this <label> and <input type=color> are here so if you click the 'sample',
|
||||
it will pop up a color picker on browsers that support it */
|
||||
>
|
||||
<input
|
||||
className="Theme__editor-color-block_input-sample Theme__editor-color-block_input"
|
||||
type="color"
|
||||
ref="colorpicker"
|
||||
value={this.props.placeholder}
|
||||
role="presentation-only"
|
||||
onChange={event => this.props.onChange(event.target.value) }
|
||||
/>
|
||||
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
})
|
||||
});
|
|
@ -0,0 +1,89 @@
|
|||
/** @jsx React.DOM */
|
||||
|
||||
define([
|
||||
'react',
|
||||
'i18n!theme_editor'
|
||||
], (React, I18n) => {
|
||||
return React.createClass({
|
||||
|
||||
displayName: 'ThemeEditorImageRow',
|
||||
|
||||
propTypes: {
|
||||
varDef: React.PropTypes.object.isRequired,
|
||||
onChange: React.PropTypes.func.isRequired,
|
||||
currentValue: React.PropTypes.string,
|
||||
placeholder: React.PropTypes.string
|
||||
},
|
||||
|
||||
setValue(inputElement) {
|
||||
var chosenValue = inputElement
|
||||
|
||||
if (!chosenValue) {
|
||||
// if they hit the "remove" button, we want to also clear out the value of the <input type=file>
|
||||
// but we don't want to mess with its value otherwise
|
||||
this.refs.fileInput.getDOMNode().value = ''
|
||||
} else {
|
||||
chosenValue = window.URL.createObjectURL(inputElement.files[0])
|
||||
}
|
||||
this.props.onChange(chosenValue)
|
||||
},
|
||||
|
||||
render() {
|
||||
return (
|
||||
<section className="Theme__editor-accordion_element Theme__editor-upload">
|
||||
<div className="te-Flex">
|
||||
<div className="Theme__editor-form--upload">
|
||||
<label
|
||||
htmlFor={'brand_config[variables]['+ this.props.varDef.variable_name +']'}
|
||||
className="Theme__editor-upload_title"
|
||||
>
|
||||
{this.props.varDef.human_name}
|
||||
<span className="Theme__editor-upload_restrictions">
|
||||
{this.props.varDef.helper_text}
|
||||
</span>
|
||||
</label>
|
||||
|
||||
<div className="Theme__editor_preview-img-container">
|
||||
<img
|
||||
src={this.props.chosenValue || this.props.placeholder}
|
||||
className="Theme__editor-placeholder--main"
|
||||
/>
|
||||
</div>
|
||||
<input
|
||||
type="hidden"
|
||||
name={this.props.chosenValue == null ? '' : 'brand_config[variables]['+ this.props.varDef.variable_name +']'}
|
||||
value={this.props.currentValue}
|
||||
/>
|
||||
<div className="Theme__editor-image_upload">
|
||||
<label className="Theme__editor-image_upload-label">
|
||||
<span className="Theme__editor-button_upload Button Button--primary">Upload Image</span>
|
||||
<input
|
||||
type="file"
|
||||
className="Theme__editor-input_upload"
|
||||
name={this.props.chosenValue ? 'brand_config[variables]['+ this.props.varDef.variable_name +']' : ''}
|
||||
accept={this.props.varDef.accept}
|
||||
onChange={event => this.setValue(event.target)}
|
||||
ref="fileInput"
|
||||
/>
|
||||
<div className="Theme__editor-input_resets">
|
||||
{this.props.chosenValue || (this.props.currentValue && this.props.chosenValue !== '') ? (
|
||||
<button
|
||||
type="button"
|
||||
className="Button Button--secondary"
|
||||
onClick={() => this.setValue(this.props.chosenValue ? null : '')}
|
||||
>
|
||||
{this.props.chosenValue && this.props.currentValue ? I18n.t('Undo') : I18n.t('Clear')}
|
||||
</button>
|
||||
) : (
|
||||
null
|
||||
)}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
}
|
||||
})
|
||||
});
|
|
@ -24,7 +24,7 @@ class Account < ActiveRecord::Base
|
|||
:turnitin_host, :turnitin_comments, :turnitin_pledge,
|
||||
:default_time_zone, :parent_account, :settings, :default_storage_quota,
|
||||
:default_storage_quota_mb, :storage_quota, :ip_filters, :default_locale,
|
||||
:default_user_storage_quota_mb, :default_group_storage_quota_mb, :integration_id
|
||||
:default_user_storage_quota_mb, :default_group_storage_quota_mb, :integration_id, :brand_config_md5
|
||||
|
||||
EXPORTABLE_ATTRIBUTES = [:id, :name, :created_at, :updated_at, :workflow_state, :deleted_at,
|
||||
:default_time_zone, :external_status, :storage_quota,
|
||||
|
@ -118,6 +118,7 @@ class Account < ActiveRecord::Base
|
|||
has_many :user_account_associations
|
||||
has_many :report_snapshots
|
||||
has_many :external_integration_keys, :as => :context, :dependent => :destroy
|
||||
belongs_to :brand_config, foreign_key: "brand_config_md5"
|
||||
|
||||
before_validation :verify_unique_sis_source_id
|
||||
before_save :ensure_defaults
|
||||
|
@ -223,12 +224,6 @@ class Account < ActiveRecord::Base
|
|||
add_setting :include_students_in_global_survey, boolean: true, root_only: true, default: false
|
||||
add_setting :trusted_referers, root_only: true
|
||||
|
||||
BRANDING_SETTINGS = [:header_image, :favicon, :apple_touch_icon,
|
||||
:msapplication_tile_color, :msapplication_tile_square,
|
||||
:msapplication_tile_wide].freeze
|
||||
|
||||
BRANDING_SETTINGS.each { |setting| add_setting(setting, root_only: true) }
|
||||
|
||||
def settings=(hash)
|
||||
@invalidate_settings_cache = true
|
||||
if hash.is_a?(Hash)
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
class BrandConfig < ActiveRecord::Base
|
||||
include BrandableCSS
|
||||
|
||||
self.primary_key = 'md5'
|
||||
serialize :variables, Hash
|
||||
|
||||
attr_accessible :variables
|
||||
|
||||
validates :variables, presence: true
|
||||
validates :md5, length: {is: 32}
|
||||
|
||||
before_validation :generate_md5
|
||||
before_update do
|
||||
raise 'BrandConfigs are a key-value mapping of config variables and an md5 digest '\
|
||||
'of those variables, so they are immutable. You do not update them, you just '\
|
||||
'save a new one and it will generate the new md5 for you'
|
||||
end
|
||||
|
||||
has_many :accounts, foreign_key: 'brand_config_md5'
|
||||
|
||||
def generate_md5
|
||||
self.id = Digest::MD5.hexdigest(self.variables.to_s)
|
||||
end
|
||||
|
||||
def get_value(variable_name)
|
||||
self.variables[variable_name]
|
||||
end
|
||||
|
||||
def to_scss
|
||||
"// This file is autogenerated by brand_config.rb as a result of running `rake brand_configs:write`\n" +
|
||||
variables.map do |name, value|
|
||||
next unless (config = BrandableCSS.variables_map[name])
|
||||
value = %{url("#{value}")} if config['type'] == 'image'
|
||||
"$#{name}: #{value};"
|
||||
end.compact.join("\n")
|
||||
end
|
||||
|
||||
def filename
|
||||
File.join(CONFIG['paths']['branded_scss_folder'], md5, '_brand_variables.scss')
|
||||
end
|
||||
|
||||
def public_folder
|
||||
"dist/brandable_css/#{md5}"
|
||||
end
|
||||
|
||||
def save_file!
|
||||
logger.info "saving BrandConfig: #{filename}"
|
||||
FileUtils.mkdir_p(File.dirname(filename))
|
||||
File.write(filename, to_scss)
|
||||
end
|
||||
|
||||
def compile_css!
|
||||
BrandableCSS.compile_brand!(md5)
|
||||
end
|
||||
|
||||
def sync_to_s3!
|
||||
Canvas::CDN.push_to_s3!(public_folder) if Canvas::CDN.enabled?
|
||||
end
|
||||
|
||||
def save_and_sync_to_s3!
|
||||
save_file!
|
||||
compile_css!
|
||||
sync_to_s3!
|
||||
end
|
||||
|
||||
def self.clean_up_unused
|
||||
BrandConfig.
|
||||
where('NOT EXISTS (SELECT 1 FROM accounts WHERE brand_config_md5=brand_configs.md5)').
|
||||
where('NOT share').
|
||||
destroy_all
|
||||
end
|
||||
|
||||
end
|
|
@ -20,13 +20,22 @@
|
|||
//================
|
||||
// Custom Branding
|
||||
//================
|
||||
// Pulls in variables in 'config/brand_variables.scss', if it exists.
|
||||
// See docs in config/brand_variables.scss.example.
|
||||
// In the future, this will be how we'll pull in the variables that
|
||||
// a school sets in their "Theme Editor" interface.
|
||||
// Only variables here with a "!default" at the end are able to be overrideable by the school.
|
||||
// see: http://sass-lang.com/documentation/file.SASS_REFERENCE.html#variable_defaults_
|
||||
@import "brand_variables";
|
||||
|
||||
// First, pull in the defaults for the brandable varables, which are auto-generated
|
||||
// from app/stylesheets/brandable_variables.json (so if you are looking to set the
|
||||
// default for any of the $ic-brand-* variables, go there)
|
||||
@import "brandable_variables_defaults_autogenerated";
|
||||
|
||||
// Now, pull in the account's custom BrandConfig variables, if there is one.
|
||||
// run `rake brand_configs:write && ./node_modules/.bin/brandable_css`
|
||||
// Which adds that folder to the top of the sass 'includePaths', so it will
|
||||
// get picked up here.
|
||||
// But if not (for the css used when there is no custom BrandConfig),
|
||||
// this just pulls in 'app/stylesheets/_brand_variables.scss'
|
||||
// which is a blank file and does nothing.
|
||||
@if $use_new_styles and not $use_high_contrast {
|
||||
@import "brand_variables";
|
||||
}
|
||||
|
||||
//=======================
|
||||
// Canvas LMS Color Sheet
|
||||
|
@ -36,19 +45,11 @@
|
|||
// When you need to use a color, please create a functional variable name
|
||||
// and use the color variable name of your choosing. See examples below.
|
||||
|
||||
//=================================
|
||||
// Canvas Theme Color Variables
|
||||
//=================================
|
||||
// These are the new colors that we would like to start rebranding
|
||||
// all the canvas pages in. All of the app variables that denote a
|
||||
// color should use or be based off of one of these variables
|
||||
$ic-brand-primary: #0096db !default; // light blue
|
||||
@if $use_high_contrast { $ic-brand-primary: #0073ac; }
|
||||
|
||||
$ic-brand-secondary: #5b6c79 !default; // dark blue
|
||||
@if $use_new_styles { $ic-brand-secondary: #384a58; }
|
||||
@if $use_high_contrast { $ic-brand-primary: #0073ac; }
|
||||
@if $use_high_contrast { $ic-brand-secondary: #343c44; }
|
||||
|
||||
|
||||
$ic-color-success: #008a14; // green
|
||||
@if $use_high_contrast { $ic-color-success: #007a12; }
|
||||
|
||||
|
@ -205,7 +206,7 @@ $ic-dim-helper-text: darken($ic-color-neutral, 33);
|
|||
$ic-dim-helper-text: darken($ic-color-neutral, 50);
|
||||
}
|
||||
|
||||
// We don't use any glyph icons like Bootstrap. This is the overwrite.
|
||||
// We don't use any of the Bootstrap glyph icons, But bootstrap's scss expects this variable to be defined.
|
||||
$iconSpritePath: "canvas_does_not_use_boostraps_default_sprit_based_icons_so_this_is_meaningless" !default;
|
||||
$iconWhiteSpritePath: $iconSpritePath !default;
|
||||
|
||||
|
|
|
@ -0,0 +1,83 @@
|
|||
[{
|
||||
"group_name": "Global Branding",
|
||||
"variables": [{
|
||||
"human_name": "Primary Color",
|
||||
"variable_name": "ic-brand-primary",
|
||||
"type": "color",
|
||||
"default": "#0096db"
|
||||
},{
|
||||
"human_name": "Secondary Color",
|
||||
"variable_name": "ic-brand-secondary",
|
||||
"type": "color",
|
||||
"default": "#5b6c79"
|
||||
},{
|
||||
"human_name": "Primary Button Color",
|
||||
"variable_name": "ic-brand-button--primary-bgd",
|
||||
"type": "color",
|
||||
"default": "$ic-brand-primary"
|
||||
},{
|
||||
"human_name": "Primary Button Text Color",
|
||||
"variable_name": "ic-brand-button--primary-text",
|
||||
"type": "color",
|
||||
"default": "#ffffff"
|
||||
},{
|
||||
"human_name": "Secondary Button",
|
||||
"variable_name": "ic-brand-button--secondary-bgd",
|
||||
"type": "color",
|
||||
"default": "$ic-brand-secondary"
|
||||
},{
|
||||
"human_name": "Secondary Button Text Color",
|
||||
"variable_name": "ic-brand-button--secondary-text",
|
||||
"type": "color",
|
||||
"default": "#ffffff"
|
||||
},{
|
||||
"human_name": "Link Color",
|
||||
"variable_name": "ic-link-color",
|
||||
"type": "color",
|
||||
"default": "$ic-brand-primary"
|
||||
}
|
||||
]
|
||||
},{
|
||||
"group_name": "Logos & Watermarks",
|
||||
"variables": [{
|
||||
"human_name": "Main Logo",
|
||||
"helper_text": "400x400, svg, png, jpg, gif",
|
||||
"variable_name": "ic-brand-header-image",
|
||||
"type": "image",
|
||||
"accept": "image/png,image/gif,image/jpeg,image/svg",
|
||||
"default": "/images/k-12-logo-placeholder.png"
|
||||
},{
|
||||
"human_name": "Favicon",
|
||||
"helper_text": "use a single 16x16, 32x32, 48x48 ico file",
|
||||
"variable_name": "ic-brand-favicon",
|
||||
"type": "image",
|
||||
"accept": "image/vnd.microsoft.icon,image/x-icon,image/png,image/gif",
|
||||
"default": "/images/favicon.ico"
|
||||
},{
|
||||
"human_name": "Mobile Homescreen Icon",
|
||||
"helper_text": "The shortcut icon for iOS/Android devices. 180x180 svg, png, jpg, gif",
|
||||
"variable_name": "ic-brand-apple-touch-icon",
|
||||
"type": "image",
|
||||
"accept": "image/png,image/svg",
|
||||
"default": "/images/apple-touch-icon.png"
|
||||
},{
|
||||
"human_name": "Windows Tile: Square",
|
||||
"helper_text": "558x558 svg, png, jpg, gif (1.8x the standard tile size, so it can be scaled up or down as needed)",
|
||||
"variable_name": "ic-brand-msapplication-tile-square",
|
||||
"type": "image",
|
||||
"accept": "image/png,image/gif,image/jpeg",
|
||||
"default": "/images/windows-tile.png"
|
||||
},{
|
||||
"human_name": "Windows Tile: Wide",
|
||||
"helper_text": "558x270 svg, png, jpg, gif",
|
||||
"variable_name": "ic-brand-msapplication-tile-wide",
|
||||
"type": "image",
|
||||
"accept": "image/png,image/gif,image/jpeg",
|
||||
"default": "/images/windows-tile-wide.png"
|
||||
},{
|
||||
"human_name": "Windows Tile Color",
|
||||
"variable_name": "ic-brand-msapplication-tile-color",
|
||||
"type": "color",
|
||||
"default": "$ic-brand-primary"
|
||||
}]
|
||||
}]
|
|
@ -0,0 +1,357 @@
|
|||
@import "base/environment";
|
||||
// TODO
|
||||
// - make theme editor panel content (if exceeding browser height) scroll independently from preview area
|
||||
//// Vertical content alignment on elements in Theme editor
|
||||
///////
|
||||
.te-Flex {
|
||||
display: flex;
|
||||
.te-Flex__block {flex: 1;}
|
||||
.te-Flex__end {
|
||||
align-self: flex-end;
|
||||
}
|
||||
&.te-Flex--v-middle {
|
||||
align-items: center;
|
||||
}
|
||||
}
|
||||
//// Make sure we're using border-box
|
||||
///////
|
||||
* {
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
//// Override for Canvas full-width (may be taken out with responsive redesign)
|
||||
///////
|
||||
body, html {height: 100vh;margin: 0; padding: 0;}
|
||||
#main {
|
||||
max-width: 100% !important;
|
||||
margin-bottom: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
.grid-row--fix {
|
||||
overflow: hidden;
|
||||
margin: 0;
|
||||
}
|
||||
//// Variables for theme editor
|
||||
///////
|
||||
$theme-editor-bgd: $ic-color-dark;
|
||||
$theme-editor-content-bgd: lighten($ic-color-dark, 10);
|
||||
$theme-editor-border-color: darken($ic-color-neutral, 30);
|
||||
//// Layout for Theme editor
|
||||
///////
|
||||
.Theme__container {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
}
|
||||
.Theme__editor {
|
||||
height: 100vh;
|
||||
width: 300px;
|
||||
box-shadow: 0 0 8px $ic-color-dark;
|
||||
background: $theme-editor-bgd;
|
||||
}
|
||||
.Theme__preview {
|
||||
flex: 1;
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
position: relative;
|
||||
.grid-row {margin: 0;}
|
||||
}
|
||||
.Theme__preview-overlay {
|
||||
background: rgba(255, 255, 255, 0.7);
|
||||
height: 100vh;
|
||||
width: 100%;
|
||||
z-index: 8000;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
//// Theme Editor content
|
||||
///////
|
||||
|
||||
.Theme__editor-header {
|
||||
background: $ic-color-light;
|
||||
padding: 10px 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.Theme__editor-header_title {
|
||||
padding: 0 0 0 $ic-sp;
|
||||
margin: 0;
|
||||
line-height: normal;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
flex: 1;
|
||||
}
|
||||
.Theme__editor-header_actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.Theme__editor-Button--trigger {
|
||||
|
||||
}
|
||||
.Theme__editor-header_button {
|
||||
margin: 0 $ic-sp;
|
||||
}
|
||||
|
||||
}
|
||||
//// Tabs for Theme Editor
|
||||
///////
|
||||
|
||||
@keyframes fadeIn { from { opacity:0; } to { opacity:1; } }
|
||||
.Theme__container {
|
||||
opacity: 0.01;
|
||||
animation: fadeIn ease-in 1;
|
||||
animation-fill-mode:forwards;
|
||||
animation-duration: 0.2s;
|
||||
}
|
||||
//// TE Panel Header
|
||||
///////
|
||||
// TODO: get tabs working properly
|
||||
.Theme__editor-tabs_list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
.Theme__editor-tabs_panel {
|
||||
color: $ic-color-light;
|
||||
}
|
||||
.Theme__editor-tabs_list {
|
||||
background: $ic-color-neutral;
|
||||
}
|
||||
.Theme__editor-tabs_list-item {
|
||||
font-size: 16px;
|
||||
font-weight: 300;
|
||||
padding: $ic-sp;
|
||||
flex: 1;
|
||||
text-align: center;
|
||||
&:hover {
|
||||
background: darken($ic-color-neutral, 10);
|
||||
text-decoration: none;
|
||||
@if $use_high_contrast {text-decoration: underline;}
|
||||
}
|
||||
}
|
||||
.Theme__editor-tabs_list-item_link {
|
||||
display: inline-block;
|
||||
color: $ic-color-dark;
|
||||
text-decoration: none;
|
||||
@if $use_high_contrast {text-decoration: underline;}
|
||||
|
||||
}
|
||||
|
||||
//// Accordion styles for Theme Editor
|
||||
///////
|
||||
// There is a lot of overriding here to support our jquery
|
||||
// accordion ui, but the theme for theme editor
|
||||
@mixin animate-icon {
|
||||
transform: rotate(90deg);
|
||||
}
|
||||
@mixin animate-hover {
|
||||
transition: padding 0.2s;
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
}
|
||||
.Theme__editor-accordion.ui-accordion {
|
||||
.ui-accordion-header {
|
||||
border: none;
|
||||
background: $theme-editor-bgd;
|
||||
font-weight: 300;
|
||||
border-radius: 0;
|
||||
&:not(:first-child) {
|
||||
border-top: 1px solid $theme-editor-border-color;
|
||||
}
|
||||
|
||||
// Default accordion state
|
||||
&.ui-state-default {
|
||||
background: $theme-editor-bgd;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
border-bottom: 1px solid $theme-editor-border-color;
|
||||
border-radius: 0;
|
||||
padding: 8px $ic-sp;
|
||||
margin-top: 0;
|
||||
.Theme__editor-accordion-icon {
|
||||
transform: rotate(-90deg);
|
||||
}
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
// Link, Link hover & Focus accordion state
|
||||
a {
|
||||
color: $ic-color-light;
|
||||
transition: color 0.2s;
|
||||
border-radius: 0;
|
||||
transition: padding 0.2s;
|
||||
padding: $ic-sp/2 0;
|
||||
font-weight: 500;
|
||||
font-size: 14px;
|
||||
}
|
||||
&.ui-state-hover {
|
||||
background: $ic-brand-primary;
|
||||
.Theme__editor-accordion-icon {
|
||||
|
||||
@include animate-icon;
|
||||
}
|
||||
}
|
||||
&.ui-state-focus {
|
||||
background: $ic-brand-primary;
|
||||
border: none;
|
||||
outline: none;
|
||||
a {
|
||||
box-shadow: inset 0 0 0 2px $ic-brand-primary;
|
||||
}
|
||||
.Theme__editor-accordion-icon {
|
||||
@include animate-icon;
|
||||
}
|
||||
}
|
||||
// Active & Focus accordion state
|
||||
&.ui-state-active {
|
||||
border: none;
|
||||
background: $theme-editor-bgd;
|
||||
border-color: $theme-editor-content-bgd;
|
||||
&.ui-state-focus {
|
||||
a {
|
||||
box-shadow: inset 0 0 0 2px $theme-editor-content-bgd;
|
||||
}
|
||||
}
|
||||
.Theme__editor-accordion-icon {
|
||||
@include animate-icon;
|
||||
}
|
||||
}
|
||||
// Takes out un-needed jquery icon
|
||||
> span {display: none;}
|
||||
}
|
||||
}
|
||||
.ui-accordion-content {
|
||||
background: $theme-editor-content-bgd;
|
||||
padding: 0 $ic-sp;
|
||||
color: $ic-color-light;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
&.ui-accordion-content-active {
|
||||
border-radius: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
//// Panel Content Styles
|
||||
//////
|
||||
.Theme__editor-accordion_element {
|
||||
padding: $ic-sp 0;
|
||||
margin: 0;
|
||||
}
|
||||
.Theme__editor-form--color {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
.Theme__editor-color-label {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
////
|
||||
// Styles for color block
|
||||
////
|
||||
.Theme__editor-color_title {
|
||||
flex: 1 80px;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-size: 13px;
|
||||
}
|
||||
.Theme__editor-color-block {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
.Theme__editor-color-block_input {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
$te-input-height: 30px;
|
||||
.Theme__editor-color-block_input-text {
|
||||
width: 75px;
|
||||
height: $te-input-height;
|
||||
margin: 0 $ic-sp 0 0;
|
||||
padding: 0 $ic-sp/2;
|
||||
color: $ic-color-dark;
|
||||
border: 1px solid $ic-color-neutral;
|
||||
border-radius: 3px;
|
||||
$te-placeholder-color: $ic-color-dark;
|
||||
&::-webkit-input-placeholder {color: $te-placeholder-color;}
|
||||
&::-moz-placeholder {color: $te-placeholder-color;}
|
||||
&:-ms-input-placeholder {color: $te-placeholder-color;}
|
||||
|
||||
}
|
||||
.Theme__editor-color-block_label-sample {
|
||||
border: 1px solid $ic-color-light;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.Theme__editor-color-block_input-sample {
|
||||
visibility: hidden;
|
||||
display: block;
|
||||
width: $te-input-height;
|
||||
height: $te-input-height;
|
||||
box-shadow: none;
|
||||
}
|
||||
}
|
||||
////
|
||||
// Styles for image upload block
|
||||
////
|
||||
.Theme__editor-upload {
|
||||
.Theme__editor-form--upload {
|
||||
width: 100%; // needed for IE to size image previews based on width of parent
|
||||
}
|
||||
.Theme__editor-upload_title {
|
||||
|
||||
}
|
||||
.Theme__editor-image_upload {
|
||||
flex: 100%;
|
||||
}
|
||||
.Theme__editor-upload_restrictions {
|
||||
color: $ic-color-neutral;
|
||||
font-style: italic;
|
||||
display: block;
|
||||
font-size: 12px;
|
||||
font-weight: 100;
|
||||
padding-top: $ic-sp/2;
|
||||
}
|
||||
.Theme__editor_preview-img-container {
|
||||
padding: $ic-sp;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid rgba($ic-color-neutral, 0.2);
|
||||
background: rgba($ic-color-light, 0.1);
|
||||
img {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
.Theme__editor-image_upload {
|
||||
margin: $ic-sp 0;
|
||||
position: relative;
|
||||
.Theme__editor-image_upload-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
.Theme__editor-input_upload {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
visibility: hidden;
|
||||
z-index: 1;
|
||||
}
|
||||
.Theme__editor-input_resets {
|
||||
z-index: 2; // make sure these go over the hidden input box
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
}
|
||||
}
|
||||
|
||||
.Theme__editor-placeholder {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -101,7 +101,7 @@ Simply add the **.ui-accordion--mini** class to the parent **.accordion** elemen
|
|||
background: lighten($ic-color-neutral, 5%);
|
||||
box-shadow: none;
|
||||
border-color: $ic-border-light;
|
||||
border-bottom: 1px solid $ic-border-light !important;
|
||||
border-bottom: 1px solid $ic-border-light;
|
||||
&.ui-state-focus {
|
||||
a { @include button-focus-light; }
|
||||
}
|
||||
|
@ -110,11 +110,11 @@ Simply add the **.ui-accordion--mini** class to the parent **.accordion** elemen
|
|||
border-bottom-left-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
border-color: darken($ic-brand-primary, 5%);
|
||||
border-bottom: 1px solid darken($ic-brand-primary, 5%) !important;
|
||||
border-bottom: 1px solid darken($ic-brand-primary, 5%);
|
||||
background: $ic-brand-primary;
|
||||
&.ui-state-focus {
|
||||
border-color: darken($ic-brand-primary, 10%);
|
||||
border-bottom: 1px solid darken($ic-brand-primary, 10%) !important;
|
||||
border-bottom: 1px solid darken($ic-brand-primary, 10%);
|
||||
a { @include button-focus-dark; }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -112,3 +112,15 @@
|
|||
.ic-flash-error {
|
||||
background-color: #b94a48;
|
||||
}
|
||||
|
||||
.ic-flash--Theme-Editor {
|
||||
background-color: $ic-color-dark;
|
||||
}
|
||||
|
||||
// Buttons in Flash actions
|
||||
a.Button--flash {
|
||||
text-decoration: none;
|
||||
@if $use_high_contrast {
|
||||
text-decoration: underline;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -223,13 +223,9 @@ input[type="button"] {
|
|||
&.btn-block, &.Button--block { width: 100%; }
|
||||
}
|
||||
|
||||
// Set the backgrounds
|
||||
// -------------------------
|
||||
// The colors for these come from app/stylesheets/brandable_variables.json
|
||||
// Or the values the account sets in the Theme Editor
|
||||
|
||||
$ic-brand-button--primary-bgd: $ic-brand-primary !default;
|
||||
$ic-brand-button--primary-text: $ic-color-light !default;
|
||||
$ic-brand-button--secondary-bgd: $ic-brand-secondary !default;
|
||||
$ic-brand-button--secondary-text: $ic-color-light !default;
|
||||
|
||||
// Primary appears as blue
|
||||
.btn-primary, // <-- deprecated- do not use
|
||||
|
@ -340,3 +336,7 @@ $ic-brand-button--secondary-text: $ic-color-light !default;
|
|||
}
|
||||
&.ui-state-disabled { @extend .Button.disabled; }
|
||||
}
|
||||
|
||||
.Button--theme-editor-apply {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
|
|
@ -244,134 +244,6 @@ TEXT
|
|||
<% end %>
|
||||
|
||||
<% unless @account.site_admin? %>
|
||||
<% if @account == @domain_root_account && (use_new_styles? || k12?) %>
|
||||
<%= f.fields_for :settings do |settings| %>
|
||||
<fieldset>
|
||||
<legend>Branding</legend>
|
||||
<table class="formtable Settings__Branding" style="100%">
|
||||
<tr>
|
||||
<td>
|
||||
<%= settings.label :header_image, t('Logo') %>
|
||||
</td>
|
||||
<td>
|
||||
<div class="branding_section">
|
||||
<%= settings.file_field :header_image, accept: 'image/png,image/gif,image/jpeg,image/svg' %>
|
||||
<p style="font-size: 0.9em;">
|
||||
<%= t("The logo image in the top right Canvas header. 200x200 svg or png") %>
|
||||
</p>
|
||||
<% if @account.settings[:header_image] %>
|
||||
<p><%= t("Current:") %></p>
|
||||
<p><img src="<%= @account.settings[:header_image] %>" width="200" height="200" /></p>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if @account.settings[:header_image] %>
|
||||
<%= settings.check_box :header_image_remove, "class" => "branding_section_toggler" %>
|
||||
<%= settings.label :header_image_remove, :en => "Revert to default" %>
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<%= settings.label :favicon, t('Favicon') %>
|
||||
</td>
|
||||
<td>
|
||||
<div class="branding_section">
|
||||
<%= settings.file_field :favicon, accept: 'image/vnd.microsoft.icon,image/x-icon,image/png,image/gif' %>
|
||||
<p style="font-size: 0.9em;">
|
||||
<%= t do %>
|
||||
Use a <a href="http://realfavicongenerator.net/" target="_blank">single 16x16, 32x32, 48x48 ico file</a>.
|
||||
<% end %>
|
||||
</p>
|
||||
<% if @account.settings[:favicon] %>
|
||||
<p><%= t("Current:") %></p>
|
||||
<p><img src="<%= @account.settings[:favicon] %>" width="16" height="16" /></p>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if @account.settings[:favicon] %>
|
||||
<%= settings.check_box :favicon_remove, "class" => "branding_section_toggler" %>
|
||||
<%= settings.label :favicon_remove, :en => "Revert to default" %>
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<%= settings.label :apple_touch_icon, t('Apple Touch Icon') %>
|
||||
</td>
|
||||
<td>
|
||||
<div class="branding_section">
|
||||
<%= settings.file_field :apple_touch_icon, accept: 'image/png,image/svg' %>
|
||||
<p style="font-size: 0.9em;">
|
||||
<%= t("The shortcut icon for iOS/Android devices. 180x180 png or svg") %>
|
||||
</p>
|
||||
<% if @account.settings[:apple_touch_icon] %>
|
||||
<p><%= t("Current:") %></p>
|
||||
<p><img src="<%= @account.settings[:apple_touch_icon] %>" width="57" height="57" /></p>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if @account.settings[:apple_touch_icon] %>
|
||||
<%= settings.check_box :apple_touch_icon_remove, "class" => "branding_section_toggler" %>
|
||||
<%= settings.label :apple_touch_icon_remove, :en => "Revert to default" %>
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><%= settings.label :msapplication_tile_square, t('Windows Square Tile') %></td>
|
||||
<td>
|
||||
<div class="branding_section">
|
||||
<%= settings.file_field :msapplication_tile_square, accept: 'image/png,image/gif,image/jpeg' %>
|
||||
<p style="font-size: 0.9em;">
|
||||
<%= t("558x558 png (1.8x the standard tile size, so it can be scaled up or down
|
||||
as needed)") %>
|
||||
</p>
|
||||
<% if @account.settings[:msapplication_tile_square] %>
|
||||
<p><%= t("Current:") %></p>
|
||||
<p><img src="<%= @account.settings[:msapplication_tile_square] %>" width="150" height="150" /></p>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if @account.settings[:msapplication_tile_square] %>
|
||||
<%= settings.check_box :msapplication_tile_square_remove, "class" => "branding_section_toggler" %>
|
||||
<%= settings.label :msapplication_tile_square_remove, :en => "Revert to default" %>
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><%= settings.label :msapplication_tile_wide, t('Windows Wide Tile') %></td>
|
||||
<td>
|
||||
<div class="branding_section">
|
||||
<%= settings.file_field :msapplication_tile_wide, accept: 'image/png,image/gif,image/jpeg' %>
|
||||
<p style="font-size: 0.9em;">
|
||||
<%= t("558x270 png") %>
|
||||
</p>
|
||||
<% if @account.settings[:msapplication_tile_wide] %>
|
||||
<p><%= t("Current:") %></p>
|
||||
<p><img src="<%= @account.settings[:msapplication_tile_wide] %>" width="150" height="150" /></p>
|
||||
<% end %>
|
||||
</div>
|
||||
<% if @account.settings[:msapplication_tile_wide] %>
|
||||
<%= settings.check_box :msapplication_tile_wide_remove, "class" => "branding_section_toggler" %>
|
||||
<%= settings.label :msapplication_tile_wide_remove, :en => "Revert to default" %>
|
||||
<% end %>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<%= settings.label :msapplication_tile_color, t('Windows Tile Color') %>
|
||||
</td>
|
||||
<td>
|
||||
<%= settings.text_field :msapplication_tile_color, value: @account.settings[:msapplication_tile_color], placeholder: '#0099e0'
|
||||
# #0099e0 is $canvas-primary from sass. If you change this, make sure to change it in app/views/info/browserconfig.xml.builder too.
|
||||
%>
|
||||
<p style="font-size:0.9em">
|
||||
<a href="http://msdn.microsoft.com/en-us/library/ie/dn455106.aspx" target="_blank">
|
||||
<%= t("Read more about windows tiles") %>
|
||||
</a>
|
||||
</p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</fieldset>
|
||||
<% end %>
|
||||
<% end %>
|
||||
<fieldset>
|
||||
<legend><%= t(:quiz_ip_filters_title, "Quiz IP Address Filters")%>
|
||||
<%= link_to("<i class='icon-question standalone-icon'></i>".html_safe, '#', :class => 'ip_help_link no-hover', :title => t(:quiz_ip_filters_help_tooltip, "What are Quiz IP Filters?")) %></legend>
|
||||
|
|
|
@ -1,13 +1,13 @@
|
|||
# Please read: http://msdn.microsoft.com/en-us/library/ie/dn455106.aspx
|
||||
# read: http://msdn.microsoft.com/en-us/library/ie/dn455106.aspx to learn more about browserconfig.xml
|
||||
xml.instruct!
|
||||
xml.browserconfig do
|
||||
xml.msapplication do
|
||||
xml.tile do
|
||||
xml.square70x70logo src: @domain_root_account.settings[:msapplication_tile_square].presence || "/windows-tile.png"
|
||||
xml.square150x150logo src: @domain_root_account.settings[:msapplication_tile_square].presence || "/windows-tile.png"
|
||||
xml.wide310x150logo src: @domain_root_account.settings[:msapplication_tile_wide].presence || "/windows-tile-wide.png"
|
||||
xml.square310x310logo src: @domain_root_account.settings[:msapplication_tile_square].presence || "/windows-tile.png"
|
||||
xml.TileColor @domain_root_account.settings[:msapplication_tile_color].presence || '#009900' # This is $canvas-primary from sass. If you change this, make sure to change it in the placeholder in app/views/accounts/settings.html.erb too.
|
||||
xml.square70x70logo src: brand_variable('ic-brand-msapplication-tile-square')
|
||||
xml.square150x150logo src: brand_variable('ic-brand-msapplication-tile-square')
|
||||
xml.wide310x150logo src: brand_variable('ic-brand-msapplication-tile-wide')
|
||||
xml.square310x310logo src: brand_variable('ic-brand-msapplication-tile-square')
|
||||
xml.TileColor brand_variable('ic-brand-msapplication-tile-color')
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -5,6 +5,17 @@
|
|||
// TODO: move this out when we have a single require call
|
||||
require = {
|
||||
translate: <%= include_js_translations? %>,
|
||||
<%-
|
||||
# Purposely not using action_controller.asset_host here, since
|
||||
# any scripts require.js loads dynamically don't know about the
|
||||
# md5 fingerprints to use. Since they don't have a fingerprint
|
||||
# it would be unsafe to rely on serving them from an s3 bucket--
|
||||
# deploys would overwrite what's there and you'd run into a case
|
||||
# where a user talking to a server running old code gets a newer
|
||||
# js file than they were expecting.
|
||||
# By serving it from the same hostname they loaded the page from, it will
|
||||
# have the same version of static assets as the code that host is running.
|
||||
%>
|
||||
baseUrl: '<%= js_base_url %>',
|
||||
paths: <%= raw Canvas::RequireJs.paths(true) %>,
|
||||
packages : <%= raw Canvas::RequireJs.packages %>,
|
||||
|
@ -15,7 +26,8 @@
|
|||
</script>
|
||||
|
||||
<%= javascript_include_tag "#{js_base_url}/vendor/require.js" %>
|
||||
<%= javascript_include_tag "#{js_base_url}/compiled/bundles/common" if include_common_bundle %>
|
||||
<% js_bundles.unshift([:common]) if include_common_bundle %>
|
||||
|
||||
<%= include_js_bundles %>
|
||||
<%= include_account_js %>
|
||||
<%= render_js_blocks %>
|
||||
|
|
|
@ -12,14 +12,8 @@
|
|||
<title><%= (yield :page_title).presence || @page_title || t('default_page_title', "Canvas LMS") %></title>
|
||||
<!--[if lte IE 8]> <meta http-equiv=refresh content="0; URL=/ie-8-is-not-supported.html" /> <![endif]-->
|
||||
|
||||
<link rel="icon" type="image/x-icon" href="<%=
|
||||
@domain_root_account.settings[:favicon].presence ||
|
||||
(Rails.env.development? && "/favicon-green.ico") ||
|
||||
(Rails.env.test? && "/favicon-green.ico") ||
|
||||
"/favicon.ico" %>"
|
||||
/>
|
||||
<link rel="apple-touch-icon" href="<%= @domain_root_account.settings[:apple_touch_icon].presence || "/apple-touch-icon.png" %>">
|
||||
|
||||
<%= favicon_link_tag(favicon) %>
|
||||
<%= favicon_link_tag(brand_variable('ic-brand-apple-touch-icon'), rel: 'apple-touch-icon', type: nil) %>
|
||||
|
||||
<%= yield :auto_discovery %>
|
||||
<%= yield :head %>
|
||||
|
@ -48,4 +42,3 @@
|
|||
document.addEventListener('click', _earlyClick);
|
||||
</script>
|
||||
</head>
|
||||
|
||||
|
|
|
@ -36,15 +36,10 @@
|
|||
<%= render :partial => "layouts/head" %>
|
||||
<body class="<%= (@body_classes).uniq.join(" ") %>">
|
||||
<%# Flash messages must be outside of #application or they won't work in screenreaders with modals open. %>
|
||||
<%= render :partial => 'shared/static_notices' %>
|
||||
<%= render :partial => 'shared/flash_notices' %>
|
||||
<div id="application" <% if use_new_styles? %>class="ic-app"<% end %>>
|
||||
<noscript><div role="alert" class="ic-flash-static ic-flash-error"><h1><%= t :javascript_required, "You need to have JavaScript enabled in order to access this site." %></h1></div></noscript>
|
||||
|
||||
<% if @current_user.try(:show_bouncing_channel_message?) %>
|
||||
<div role="alert" class="ic-flash-static ic-flash-warning">
|
||||
<%= t(:bouncing_communication_channels, "There appears to be a problem with one of your contact methods. Please check your *Settings Page*.", wrapper: link_to('\1', settings_profile_path)) %>
|
||||
</div>
|
||||
<% end %>
|
||||
<%# Begin alternate markup for K-12 Canvas header %>
|
||||
<% if k12? %>
|
||||
<a href="#content" id="skip_navigation_link"><%= t 'links.skip_to_content', "Skip To Content" %></a>
|
||||
|
|
|
@ -1,53 +1,13 @@
|
|||
<%-
|
||||
|
||||
content_for :head, include_common_stylesheets
|
||||
<%= render :partial => "layouts/head" %>
|
||||
<body class="Sg-only">
|
||||
<%= render :partial => 'shared/static_notices' %>
|
||||
<%= render :partial => 'shared/flash_notices' %>
|
||||
|
||||
-%>
|
||||
<%-
|
||||
@locale = raw I18n.locale.to_json
|
||||
@body_classes ||= []
|
||||
@body_classes << "context-#{@context.asset_string}" if @context
|
||||
yield :pre_html
|
||||
|
||||
-%><!DOCTYPE html>
|
||||
<!--[if gte IE 9 ]><html class="Sg-only ie ie9 scripts-not-loaded" lang=<%= @locale %>> <![endif]-->
|
||||
<!--[if !(IE)]><!--> <html class="Sg-only not-ie scripts-not-loaded" lang=<%= @locale %>> <!--<![endif]-->
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title><%= (yield :page_title).presence || @page_title || t('default_page_title', "Canvas LMS") %></title>
|
||||
<!--[if lte IE 8]> <meta http-equiv=refresh content="0; URL=/ie-8-is-not-supported.html" /> <![endif]-->
|
||||
|
||||
<%= yield :auto_discovery %>
|
||||
<%= yield :head %>
|
||||
<%= yield :meta_tags %>
|
||||
<%= include_custom_meta_tags %>
|
||||
<%= include_css_bundles %>
|
||||
<%= yield :stylesheets %>
|
||||
<%= include_account_css %>
|
||||
<script>
|
||||
// listen for any clicks on links that have href="#" and queue them to be fired on dom ready.
|
||||
function _earlyClick(e){
|
||||
var cur = e.target || e.srcElement;
|
||||
while ( cur && cur.ownerDocument ) {
|
||||
if ( cur.getAttribute('href') == '#' ) {
|
||||
e.preventDefault();
|
||||
_earlyClick.clicks = _earlyClick.clicks || [];
|
||||
_earlyClick.clicks.push(cur);
|
||||
break;
|
||||
}
|
||||
cur = cur.parentNode;
|
||||
}
|
||||
}
|
||||
document.addEventListener('click', _earlyClick);
|
||||
</script>
|
||||
</head>
|
||||
<body class="Sg-only">
|
||||
|
||||
<div id="application">
|
||||
<div id="main" class="Sg-only">
|
||||
<%= yield %>
|
||||
</div>s
|
||||
<%= render :partial => 'layouts/foot', :locals => { :include_common_bundle => true } %>
|
||||
</div> <!-- #application -- this is important to keep since it kicks off some of our js examples -->
|
||||
</body>
|
||||
</html>
|
||||
<div id="application">
|
||||
<div id="main" class="Sg-only">
|
||||
<%= yield %>
|
||||
</div>s
|
||||
<%= render :partial => 'layouts/foot', :locals => { :include_common_bundle => true } %>
|
||||
</div> <!-- #application -- this is important to keep since it kicks off some of our js examples -->
|
||||
</body>
|
||||
</html>
|
|
@ -9,3 +9,9 @@
|
|||
<% end -%>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if @account == @domain_root_account && use_new_styles? && can_do(@account, @current_user, :manage_site_settings) %>
|
||||
<div class="rs-margin-lr">
|
||||
<%= link_to t("Open Theme Editor"), brand_configs_new_path, :class => 'btn button-sidebar-wide' %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
|
|
@ -1,8 +1,4 @@
|
|||
<%
|
||||
notices = flash_notices()
|
||||
js_env(:notices => notices)
|
||||
%>
|
||||
<% js_env notices: flash_notices() %>
|
||||
|
||||
<ul role="alert" aria-live="assertive" id="flash_message_holder"></ul>
|
||||
<div role="alert" aria-live="assertive" id="flash_screenreader_holder" class="screenreader-only"></div>
|
||||
|
||||
|
|
|
@ -0,0 +1,23 @@
|
|||
<noscript>
|
||||
<div role="alert" class="ic-flash-static ic-flash-error">
|
||||
<h1><%= t :javascript_required, "You need to have JavaScript enabled in order to access this site." %></h1>
|
||||
</div>
|
||||
</noscript>
|
||||
|
||||
<% if session.has_key? :brand_config_md5 %>
|
||||
<div role="alert" class="ic-flash-static ic-flash-info ic-flash--Theme-Editor">
|
||||
<%= t("You're editing your Canvas Theme! The changes have not been applied yet.") %>
|
||||
<% unless params[:editing_brand_config] %>
|
||||
<%= link_to t("Apply Changes"), brand_configs_new_path, :class => "Button Button--success Button--flash" %>
|
||||
or
|
||||
<%= link_to t("Open Theme Editor"), brand_configs_new_path, :class => '' %>
|
||||
|
||||
<% end %>
|
||||
</div>
|
||||
<% end %>
|
||||
|
||||
<% if @current_user.try(:show_bouncing_channel_message?) %>
|
||||
<div role="alert" class="ic-flash-static ic-flash-warning">
|
||||
<%= t(:bouncing_communication_channels, "There appears to be a problem with one of your contact methods. Please check your *Settings Page*.", wrapper: link_to('\1', settings_profile_path)) %>
|
||||
</div>
|
||||
<% end %>
|
|
@ -220,5 +220,9 @@ module CanvasRails
|
|||
end
|
||||
|
||||
config.exceptions_app = ExceptionsApp.new
|
||||
|
||||
config.before_initialize do
|
||||
config.action_controller.asset_host = Canvas::CDN.config.host if Canvas::CDN.config.host
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
|
@ -1,86 +0,0 @@
|
|||
<%=
|
||||
=begin
|
||||
|
||||
WHAT HAPPEND TO assets.yml? AND WHAT ARE THE $use_new_styles, $is-k12 AND $use_high_contrast SASS VARIABLES?
|
||||
-----------------------------------------------------------------------------
|
||||
|
||||
TL,DR: don't add things to this file. Put css bundles in 'assets_real.yml'
|
||||
|
||||
In order to allow accounts to opt-in to a feature flag to use the new styles
|
||||
that UI/UX is working on, and in order to provide a k12-specific look-and-feel
|
||||
and in order to allow individual users to turn on a
|
||||
feature flag for high-contrast styles, we do some trickery in our sass.
|
||||
|
||||
### What you, the developer need to know.
|
||||
In any sass file, you can do things like:
|
||||
|
||||
@include 'environment'; // or 'variables' or 'variant_variables' if you need to be more fine grained
|
||||
|
||||
@if $use_new_styles {
|
||||
background-color: red;
|
||||
} @else {
|
||||
background-color: blue;
|
||||
}
|
||||
|
||||
@if not $use_high_contrast{
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
...and those styles will show up for users depending on their own
|
||||
:use_high_contrast feature flag and the account's :use_new_styles feature flag.
|
||||
|
||||
Make sure, if you see stuff about $use_high_contrast, $is-k12 or $use_new_styles in a
|
||||
sass file you are working on, that you test it with the feature flag both on and
|
||||
off, so your changes don't mess up the other. At least having them in the same
|
||||
file will make it easier than keeping track of some orverrides.css file floating
|
||||
around somewhere that might be overriding or depending on the rules you're
|
||||
changing though eh?
|
||||
|
||||
Common Gotcha With Variables:
|
||||
variables in sass are block scoped, and @if is a block so you can't do:
|
||||
@if $use_new_styles { $textColor: black } @else { $textColor: grey}
|
||||
because $textColor wont be available anywere ouside that @if.
|
||||
to get around that do:
|
||||
$textColor: null;
|
||||
@if $use_new_styles { $textColor: black } @else { $textColor: grey}
|
||||
You *could* do:
|
||||
$textColor: grey;
|
||||
@if $use_new_styles { $textColor: black }
|
||||
but DON'T, the explicit semantics of either being in the @if or the @else will help
|
||||
us when we delete things after everyone has $use_new_styles.
|
||||
|
||||
A key goal is that, as we work toward the new styles, put anything that we won't
|
||||
need anymore inside an '@if not $use_new_styles' block, SO WE CAN DELETE STUFF
|
||||
SOMEDAY!! YAY!
|
||||
|
||||
for more info on the @if and @else syntax see:
|
||||
http://sass-lang.com/documentation/file.SASS_REFERENCE.html#_8
|
||||
|
||||
|
||||
The Nitty Gritty:
|
||||
We can do this because Canvas::MultiVariantCompassCompiler#compile_all
|
||||
(which is the thing that we use to compile all of our sass) compiles each sass
|
||||
file 6 times, each time adding the path 'app/stylesheets/variants/<variant>'
|
||||
to the load path so when we @import 'variant_variables', it is actually
|
||||
grabbing, for example:
|
||||
app/stylesheets/variants/new_styles_high_contrast/_variant_variables.scss
|
||||
which sets the variables $use_new_styles and $use_high_contrast correspondingly.
|
||||
|
||||
It also outputs all the css files for each variant to a folder with the same
|
||||
name. Here in assets.yml we manipulate the actual yml in assets_real.yml to make 4 bundles
|
||||
out of each original bundle, and rename the file paths in each to point to the generated
|
||||
css directory for that variant.
|
||||
And finally we do stuff in ApplicationController#include_css_bundles, so when you say
|
||||
`jammit_css :speedgrader`, it will look at the user's :use_high_contrast feature flag
|
||||
and the account's :use_new_styles flag and (assuming they were both set to true) it will
|
||||
tell jammit it wants the 'speedgrader_new_styles_high_contrast' bundle which will include
|
||||
things like 'public/stylesheets_compiled/new_styles_high_contrast/speed_grader.css',
|
||||
which was compiled with the $use_new_styles and $use_high_contrast variables set correctly.
|
||||
|
||||
Note:
|
||||
If $is-k12 is turned on, then $use_new_styles will be true as well.
|
||||
|
||||
=end
|
||||
require File.join(Jammit::ASSET_ROOT, 'lib', 'multi_variant_compass_compiler')
|
||||
MultiVariantCompassCompiler.make_variants_from_real_assets_yml
|
||||
%>
|
|
@ -1,175 +0,0 @@
|
|||
embed_assets: off
|
||||
gzip_assets: off
|
||||
|
||||
# libsass does this for us
|
||||
compress_assets: off
|
||||
|
||||
# if you want use IE in dev mode and want to get around the max of 30 stylesheets
|
||||
# problem, uncomment the following lines and make sure you
|
||||
# rm -rf public/assets after you make any changes to css
|
||||
# package_assets: always
|
||||
# compress_assets: off
|
||||
|
||||
<% require File.expand_path(Jammit::ASSET_ROOT + '/lib/canvas/plugins/plugin_assets') %>
|
||||
<% plugin_assets = PluginAssets.new %>
|
||||
<%= plugin_assets.bundle_yml %>
|
||||
|
||||
|
||||
stylesheets:
|
||||
saml_fields:
|
||||
- public/stylesheets/compiled/bundles/saml_fields.css
|
||||
course_wizard:
|
||||
- public/stylesheets/compiled/bundles/course_wizard.css
|
||||
react_files:
|
||||
- public/stylesheets/compiled/bundles/react_files.css
|
||||
instructure_eportfolio:
|
||||
- public/stylesheets/compiled/bundles/instructure_eportfolio.css
|
||||
course_show:
|
||||
- public/stylesheets/compiled/bundles/course_show.css
|
||||
course_list:
|
||||
- public/stylesheets/compiled/bundles/course_list.css
|
||||
content_migrations:
|
||||
- public/stylesheets/compiled/bundles/content_migrations.css
|
||||
vendor:
|
||||
- public/stylesheets/compiled/bundles/vendor.css
|
||||
common:
|
||||
- public/stylesheets/compiled/bundles/common.css
|
||||
context_modules:
|
||||
- public/stylesheets/compiled/bundles/context_modules.css
|
||||
context_modules2:
|
||||
- public/stylesheets/compiled/bundles/context_modules2.css
|
||||
content_next:
|
||||
- public/stylesheets/compiled/bundles/content_next.css
|
||||
modules_next:
|
||||
- public/stylesheets/compiled/bundles/modules_next.css
|
||||
context_module_progressions:
|
||||
- public/stylesheets/compiled/bundles/context_module_progressions.css
|
||||
dashboard:
|
||||
- public/stylesheets/compiled/bundles/dashboard.css
|
||||
registration:
|
||||
- public/stylesheets/compiled/bundles/registration.css
|
||||
profile_show:
|
||||
- public/stylesheets/compiled/bundles/profile_show.css
|
||||
profile_edit:
|
||||
- public/stylesheets/compiled/bundles/profile_edit.css
|
||||
facebook:
|
||||
- public/stylesheets/compiled/bundles/facebook.css
|
||||
speed_grader:
|
||||
- public/stylesheets/compiled/bundles/speed_grader.css
|
||||
conferences:
|
||||
- public/stylesheets/compiled/bundles/conferences.css
|
||||
gradebook_uploads:
|
||||
- public/stylesheets/compiled/bundles/gradebook_uploads.css
|
||||
calendar2:
|
||||
- public/stylesheets/compiled/bundles/calendar2.css
|
||||
agenda_view:
|
||||
- public/stylesheets/compiled/bundles/agenda_view.css
|
||||
course_settings:
|
||||
- public/stylesheets/compiled/bundles/course_settings.css
|
||||
discussions:
|
||||
- public/stylesheets/compiled/bundles/discussions.css
|
||||
discussions_list:
|
||||
- public/stylesheets/compiled/bundles/discussions_list.css
|
||||
full_files:
|
||||
- public/stylesheets/compiled/bundles/full_files.css
|
||||
datagrid:
|
||||
- public/stylesheets/compiled/bundles/datagrid.css
|
||||
gradebook_history:
|
||||
- public/stylesheets/compiled/bundles/gradebook_history.css
|
||||
gradebook2:
|
||||
- public/stylesheets/compiled/bundles/gradebook2.css
|
||||
screenreader_gradebook:
|
||||
- public/stylesheets/compiled/bundles/screenreader_gradebook.css
|
||||
attendance:
|
||||
- public/stylesheets/compiled/bundles/attendance.css
|
||||
quizzes:
|
||||
- public/stylesheets/compiled/bundles/quizzes.css
|
||||
quizzes_ember:
|
||||
- public/stylesheets/compiled/bundles/quizzes_ember.css
|
||||
moderate_quiz:
|
||||
- public/stylesheets/compiled/bundles/moderate_quiz.css
|
||||
assignments:
|
||||
- public/stylesheets/compiled/bundles/assignments.css
|
||||
assignments_edit:
|
||||
- public/stylesheets/compiled/bundles/assignments_edit.css
|
||||
new_assignments:
|
||||
- public/stylesheets/compiled/bundles/new_assignments.css
|
||||
grading_standards:
|
||||
- public/stylesheets/compiled/bundles/grading_standards.css
|
||||
grading_periods:
|
||||
- public/stylesheets/compiled/bundles/grading_periods.css
|
||||
login:
|
||||
- public/stylesheets/compiled/bundles/login.css
|
||||
otp_login:
|
||||
- public/stylesheets/compiled/bundles/otp_login.css
|
||||
roster:
|
||||
- public/stylesheets/compiled/bundles/roster.css
|
||||
roster_user:
|
||||
- public/stylesheets/compiled/bundles/roster_user.css
|
||||
learning_outcomes:
|
||||
- public/stylesheets/compiled/bundles/learning_outcomes.css
|
||||
grade_summary:
|
||||
- public/stylesheets/compiled/bundles/grade_summary.css
|
||||
context_list:
|
||||
- public/stylesheets/compiled/bundles/context_list.css
|
||||
page_views:
|
||||
- public/stylesheets/compiled/bundles/page_views.css
|
||||
prior_users:
|
||||
- public/stylesheets/compiled/bundles/prior_users.css
|
||||
reports:
|
||||
- public/stylesheets/compiled/bundles/reports.css
|
||||
statistics:
|
||||
- public/stylesheets/compiled/bundles/statistics.css
|
||||
slickgrid:
|
||||
- public/stylesheets/compiled/bundles/slickgrid.css
|
||||
sub_accounts:
|
||||
- public/stylesheets/compiled/bundles/sub_accounts.css
|
||||
user_grades:
|
||||
- public/stylesheets/compiled/bundles/user_grades.css
|
||||
user_logins:
|
||||
- public/stylesheets/compiled/bundles/user_logins.css
|
||||
account_settings:
|
||||
- public/stylesheets/compiled/bundles/account_settings.css
|
||||
account_admin_tools:
|
||||
- public/stylesheets/compiled/bundles/account_admin_tools.css
|
||||
select_content_dialog:
|
||||
- public/stylesheets/compiled/bundles/select_content_dialog.css
|
||||
conversations_new:
|
||||
- public/stylesheets/compiled/bundles/conversations_new.css
|
||||
alerts:
|
||||
- public/stylesheets/compiled/bundles/alerts.css
|
||||
developer_keys:
|
||||
- public/stylesheets/compiled/bundles/developer_keys.css
|
||||
edit_calendar_event_full:
|
||||
- public/stylesheets/compiled/bundles/edit_calendar_event_full.css
|
||||
notification_preferences:
|
||||
- public/stylesheets/compiled/bundles/notification_preferences.css
|
||||
tinymce:
|
||||
- public/stylesheets/compiled/bundles/tinymce.css
|
||||
messages:
|
||||
- public/stylesheets/compiled/bundles/messages.css
|
||||
user_notes:
|
||||
- public/stylesheets/compiled/bundles/user_notes.css
|
||||
imports:
|
||||
- public/stylesheets/compiled/bundles/imports.css
|
||||
styleguide:
|
||||
- public/stylesheets/compiled/bundles/styleguide.css
|
||||
locale:
|
||||
- public/stylesheets/compiled/bundles/locale.css
|
||||
mobile_auth:
|
||||
- public/stylesheets/compiled/bundles/mobile_auth.css
|
||||
all_courses:
|
||||
- public/stylesheets/compiled/bundles/all_courses.css
|
||||
external_tool_full_width:
|
||||
- public/stylesheets/compiled/bundles/external_tool_full_width.css
|
||||
dashboard_card:
|
||||
- public/stylesheets/compiled/bundles/dashboard_card.css
|
||||
terms:
|
||||
- public/stylesheets/compiled/bundles/terms.css
|
||||
wiki_page:
|
||||
- public/stylesheets/compiled/bundles/wiki_page.css
|
||||
# Client Apps:
|
||||
canvas_quizzes:
|
||||
- public/stylesheets/compiled/bundles/canvas_quizzes.css
|
||||
|
||||
<%= plugin_assets.anchors_yml %>
|
|
@ -1,40 +0,0 @@
|
|||
/*
|
||||
This is an expirimental feature to preview how branding canvas with your own colors will work.
|
||||
It's not ready yet, and should only be used on dev machines.
|
||||
|
||||
To use, copy this file to `config/brand_variables.scss` (remove the `.example`).
|
||||
You can set values to whatever you want here and they will be used in the
|
||||
'new_styles_normal_contrast' and 'k12_normal_contrast' variants when they are compiled on
|
||||
your computer.
|
||||
remember, anything that you want to be able to set in this file, needs to have a "!default"
|
||||
next to it where it is declared in the real sass sheet.
|
||||
|
||||
As we decide upon new variables that we want to be able to set, remember to add them to
|
||||
both "config/brand_variables.scss.example" as well as your "config/brand_variables.cass" locally.
|
||||
|
||||
*/
|
||||
|
||||
//// Canvas Primary Palette
|
||||
////////////////////////////////////////////////
|
||||
// $ic-brand-primary
|
||||
// $ic-brand-secondary
|
||||
// $linkColor
|
||||
|
||||
//// Instructure Canvas (IC) Global Variables
|
||||
////////////////////////////////////////////////
|
||||
|
||||
// $ic-body-background-color
|
||||
// $ic-content-background-color
|
||||
// $ic-font-color-light
|
||||
// $ic-font-color-dark
|
||||
// $ic-font-color--subdued
|
||||
// $ic-border-color
|
||||
// $ic-list-item-background--hover
|
||||
// $ic-list-item-background--selected
|
||||
|
||||
//// Buttons
|
||||
////////////////////////////
|
||||
// $ic-brand-button--primary-bgd
|
||||
// $ic-brand-button--primary-text
|
||||
// $ic-brand-button--secondary-bgd
|
||||
// $ic-brand-button--secondary-text
|
|
@ -0,0 +1,23 @@
|
|||
paths:
|
||||
public_dir: public
|
||||
sass_dir: app/stylesheets
|
||||
all_sass_bundles: 'app/stylesheets/{,plugins/*/}**/[^_]*.s[ac]ss'
|
||||
all_sass_files: 'app/stylesheets/{,plugins/*/}**/*.s[ac]ss'
|
||||
brandable_variables_json: app/stylesheets/brandable_variables.json
|
||||
brandable_variables_defaults_scss: app/stylesheets/_brandable_variables_defaults_autogenerated.scss
|
||||
branded_scss_folder: app/stylesheets/brandable_css_brands
|
||||
bundles_with_deps: public/dist/brandable_css/brandable_css_bundles_with_deps.json
|
||||
file_checksums: public/dist/brandable_css/brandable_css_file_checksums.json
|
||||
output_dir: public/dist/brandable_css
|
||||
browsers_yml: config/browsers.yml
|
||||
|
||||
manifest_key_seperator: $$$$$$$$$$$
|
||||
|
||||
variants:
|
||||
legacy_normal_contrast: {}
|
||||
legacy_high_contrast: {}
|
||||
new_styles_normal_contrast:
|
||||
brandable: true
|
||||
new_styles_high_contrast: {}
|
||||
k12_normal_contrast: {}
|
||||
k12_high_contrast: {}
|
|
@ -0,0 +1,16 @@
|
|||
defaults: &defaults
|
||||
# host: 'the hostname to use for static asset cdn, eg: https://mydistribution.cloudfront.net'
|
||||
# bucket: "name of the s3 bucket to push things to"
|
||||
# aws_access_key_id: "secret aws_access_key_id that has write access to that bucket"
|
||||
# aws_secret_access_key: "secret_access_key for that access_key_id"
|
||||
# enabled: true #set false to not push anything to s3 and serve assets from same hostname as rails
|
||||
|
||||
development:
|
||||
<<: *defaults
|
||||
|
||||
test:
|
||||
<<: *defaults
|
||||
enabled: false
|
||||
|
||||
production:
|
||||
<<: *defaults
|
|
@ -1,34 +1,31 @@
|
|||
require 'fileutils'
|
||||
require 'pathname'
|
||||
|
||||
# stolen and adapted from ./plugin_symlinks.rb
|
||||
def maintain_client_app_symlinks
|
||||
# remove bad symlinks first
|
||||
Dir.glob("public/javascripts/client_apps/*").each do |app_symlink|
|
||||
if File.symlink?(app_symlink) && !File.exist?(app_symlink)
|
||||
File.unlink(app_symlink)
|
||||
end
|
||||
end
|
||||
output_dir = Pathname.new "public/javascripts/client_apps"
|
||||
# remove anything that was there first
|
||||
output_dir.rmtree if output_dir.exist?
|
||||
|
||||
# create new ones
|
||||
Dir.glob("client_apps/*").select { |f| File.directory?(f) }.each do |app_dir|
|
||||
unless File.exist?("public/javascripts/client_apps")
|
||||
FileUtils.makedirs("public/javascripts/client_apps")
|
||||
Pathname.glob("client_apps/*").select(&:directory?).each do |app_dir|
|
||||
app = app_dir.basename
|
||||
dist = app_dir.join('dist')
|
||||
next unless dist.exist?
|
||||
files = Dir.chdir(dist) do
|
||||
[Pathname.new("#{app}.js")] + Pathname.glob("#{app}/**/*").reject(&:directory?)
|
||||
end
|
||||
|
||||
app = File.basename(app_dir)
|
||||
|
||||
[ "#{app}", "#{app}.js" ].each do |asset|
|
||||
source = "public/javascripts/client_apps/#{asset}"
|
||||
target = "../../../#{app_dir}/dist/#{asset}"
|
||||
|
||||
unless File.symlink?(source) && File.readlink(source) == target
|
||||
File.unlink(source) if File.exist?(source)
|
||||
File.symlink(target, source)
|
||||
end
|
||||
files.each do |asset|
|
||||
original = dist.join(asset)
|
||||
target = output_dir.join(asset)
|
||||
FileUtils.mkdir_p(target.dirname)
|
||||
File.symlink(original.relative_path_from(target.dirname), target)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
File.open(__FILE__) do |f|
|
||||
f.flock(File::LOCK_EX)
|
||||
|
||||
maintain_client_app_symlinks
|
||||
end
|
||||
|
|
|
@ -0,0 +1,67 @@
|
|||
module RevManifest
|
||||
def self.manifest
|
||||
# don't look this up every request in prduction
|
||||
return @manifest if ActionController::Base.perform_caching && defined? @manifest
|
||||
file = Rails.root.join('public', 'dist', 'rev-manifest.json')
|
||||
if file.exist?
|
||||
Rails.logger.debug "reading rev-manifest.json"
|
||||
@manifest = JSON.parse(file.read).freeze
|
||||
elsif Rails.env.production?
|
||||
raise "you need to run `gulp rev` first"
|
||||
else
|
||||
@manifest = {}.freeze
|
||||
end
|
||||
end
|
||||
|
||||
def self.url_for(source)
|
||||
# remove the leading slash if there is one
|
||||
source = source.sub(/^\//, '')
|
||||
fingerprinted = manifest[source]
|
||||
"/dist/#{fingerprinted}" if fingerprinted
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
# This is where we monkeypatch rails to look at the rev-manifest.json file we make in `gulp rev`
|
||||
# instead of doing it's normal cache busting stuff on the url.
|
||||
# eg: instead of '/images/whatever.png?12345', we want '/dist/images/whatever-<md5 of file>.png'
|
||||
# There is a different method that needs to be monkeypatched for rails 3 vs rails 4
|
||||
if CANVAS_RAILS3
|
||||
module ActionView
|
||||
module Helpers
|
||||
module AssetTagHelper
|
||||
class AssetPaths
|
||||
private
|
||||
|
||||
# Rails 3 expects us to override 'rewrite_asset_path' if we want to do something other than the
|
||||
# default "/images/whatever.png?12345".
|
||||
def rewrite_asset_path_with_gulp_assets(source, dir, options = nil)
|
||||
# our brandable_css stylesheets are already fingerprinted, we don't need to do anything to them
|
||||
return source if source =~ /^\/dist\/brandable_css/
|
||||
|
||||
key = (source[0] == '/') ? source : "#{dir}/#{source}"
|
||||
RevManifest.url_for(key) || rewrite_asset_path_without_gulp_assets(source, dir, options)
|
||||
end
|
||||
alias_method_chain :rewrite_asset_path, :gulp_assets
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
require 'action_view/helpers/asset_url_helper'
|
||||
module ActionView
|
||||
module Helpers
|
||||
module AssetUrlHelper
|
||||
|
||||
# Rails 4 leaves us 'compute_asset_path' to override instead.
|
||||
def compute_asset_path_with_gulp_assets(source, options = {})
|
||||
original_path = compute_asset_path_without_gulp_assets(source, options)
|
||||
RevManifest.url_for(original_path) || original_path
|
||||
end
|
||||
alias_method_chain :compute_asset_path, :gulp_assets
|
||||
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -808,6 +808,14 @@ CanvasRails::Application.routes.draw do
|
|||
concerns :files
|
||||
end
|
||||
|
||||
|
||||
scope(controller: :brand_configs) do
|
||||
get 'brand_configs/new', action: :new
|
||||
post 'brand_configs', action: :create
|
||||
delete 'brand_configs', action: :destroy
|
||||
post 'brand_configs/save_to_account', action: :save_to_account
|
||||
end
|
||||
|
||||
get 'courses/:course_id/outcome_rollups' => 'outcome_results#rollups', as: 'course_outcome_rollups'
|
||||
|
||||
get 'terms_of_use' => 'legal_information#terms_of_use', as: 'terms_of_use_redirect'
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
class CreateBrandConfigs < ActiveRecord::Migration
|
||||
tag :predeploy
|
||||
disable_ddl_transaction!
|
||||
|
||||
LENGTH_OF_AN_MD5_HASH = 32
|
||||
|
||||
def up
|
||||
create_table :brand_configs, id: false do |t|
|
||||
t.string :md5, limit: LENGTH_OF_AN_MD5_HASH, null: false, unique: true
|
||||
t.column :variables, :text, null: false
|
||||
t.boolean :share, default: false, null: false
|
||||
t.string :name
|
||||
t.datetime :created_at, null: false
|
||||
end
|
||||
# because we didn't use the rails default `id` int primary key, we have to add it manually
|
||||
execute %{ ALTER TABLE brand_configs ADD PRIMARY KEY (md5); }
|
||||
add_index :brand_configs, :share
|
||||
|
||||
add_column :accounts, :brand_config_md5, :string, limit: LENGTH_OF_AN_MD5_HASH
|
||||
add_foreign_key :accounts, :brand_configs, column: 'brand_config_md5', primary_key: 'md5'
|
||||
add_index :accounts, :brand_config_md5, where: 'brand_config_md5 IS NOT NULL', algorithm: :concurrently
|
||||
end
|
||||
|
||||
def down
|
||||
remove_column :accounts, :brand_config_md5
|
||||
drop_table :brand_configs
|
||||
end
|
||||
|
||||
end
|
|
@ -65,10 +65,17 @@ module HandlebarsTasks
|
|||
|
||||
dependencies = ['compiled/handlebars_helpers']
|
||||
|
||||
if css = get_css(id)
|
||||
dependencies << "compiled/util/registerTemplateCss"
|
||||
# arguments[1] will be the registerTemplateCss function
|
||||
css_registration = "\narguments[1]('#{id}', #{MultiJson.dump css});\n"
|
||||
# if a scss file named exactly like this exists, load it when this is loaded
|
||||
if Dir.glob("app/stylesheets/jst/#{id}.s[ac]ss").first
|
||||
bundle = "jst/#{id}"
|
||||
require 'lib/brandable_css'
|
||||
fingerprints = MultiJson.dump BrandableCSS.all_fingerprints_for(bundle)
|
||||
dependencies << "compiled/util/brandableCss"
|
||||
# arguments[1] will be brandableCss
|
||||
css_registration = "
|
||||
var fingerprint = #{fingerprints}[arguments[1].getCssVariant()];
|
||||
arguments[1].loadStylesheet('#{bundle}-' + fingerprint);
|
||||
"
|
||||
end
|
||||
|
||||
# take care of `require`ing partials
|
||||
|
@ -88,23 +95,12 @@ define('#{plugin ? plugin + "/" : ""}jst/#{id}', #{MultiJson.dump dependencies},
|
|||
var template = Handlebars.template, templates = Handlebars.templates = Handlebars.templates || {};
|
||||
templates['#{id}'] = template(#{data["template"]});
|
||||
#{partial_registration}
|
||||
#{css_registration}
|
||||
#{css_registration}
|
||||
return templates['#{id}'];
|
||||
});
|
||||
JS
|
||||
end
|
||||
|
||||
def get_css(file_path)
|
||||
if sass_file = Dir.glob("app/stylesheets/jst/#{file_path}.s[ac]ss").first
|
||||
# renders the sass file to disk, then returns the css it wrote
|
||||
# note: for now, all jst stylesheets will be just in 'legacy_normal_contrast'
|
||||
system({"CANVAS_SASS_STYLE" => "compressed"}, "node script/compile-sass.js #{sass_file}")
|
||||
File.read sass_file
|
||||
.sub(/^app\/stylesheets/, 'public/stylesheets_compiled/legacy_normal_contrast')
|
||||
.sub(/.s[ac]ss$/, '.css')
|
||||
end
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
def find_partial_deps(template)
|
||||
|
|
|
@ -0,0 +1,15 @@
|
|||
require 'guard'
|
||||
require 'guard/guard'
|
||||
|
||||
module Guard
|
||||
class BrandableCSS < Guard
|
||||
|
||||
def start
|
||||
@pid = spawn("./node_modules/.bin/brandable_css --watch")
|
||||
end
|
||||
|
||||
def stop
|
||||
Process.kill(:INT, @pid)
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,25 +0,0 @@
|
|||
require 'guard'
|
||||
require 'guard/guard'
|
||||
|
||||
module Guard
|
||||
class Compass < Guard
|
||||
|
||||
def initialize(watchers=[], options={})
|
||||
super([::Guard::Watcher.new(/(app\/stylesheets.*)/)], {})
|
||||
end
|
||||
|
||||
def run_on_change(paths)
|
||||
# for now just recompile everything, we'll do this more optimized when we
|
||||
# fix the TODO below
|
||||
run_all
|
||||
end
|
||||
|
||||
def run_all
|
||||
::Guard::UI.info "Forcing recompilation of all SASS files"
|
||||
# TODO: get rid of this guard and watch sass in our JS based frontend watcher.
|
||||
# whatever that ends up being (gulp, broccoli, etc)
|
||||
`npm run compile-sass`
|
||||
end
|
||||
|
||||
end
|
||||
end
|
|
@ -0,0 +1,20 @@
|
|||
const gulp = require('gulp')
|
||||
const gulpPlugins = require('gulp-load-plugins')()
|
||||
|
||||
const DIST = 'public/dist'
|
||||
const PUBLIC_NOT_DIST = ['public/**/*', '!' + DIST + '/**/*']
|
||||
|
||||
gulp.task('rev', function() {
|
||||
return gulp.src(PUBLIC_NOT_DIST)
|
||||
.pipe(gulp.dest(DIST))
|
||||
.pipe(gulpPlugins.rev())
|
||||
.pipe(gulp.dest(DIST))
|
||||
.pipe(gulpPlugins.rev.manifest())
|
||||
.pipe(gulp.dest(DIST))
|
||||
})
|
||||
|
||||
gulp.task('watch', function (){
|
||||
gulp.watch(PUBLIC_NOT_DIST, ['rev'])
|
||||
})
|
||||
|
||||
gulp.task('default', ['rev', 'watch'])
|
|
@ -0,0 +1,96 @@
|
|||
module BrandableCSS
|
||||
require 'rails'
|
||||
CONFIG = YAML.load_file(Rails.root.join('config/brandable_css.yml')).freeze
|
||||
BRANDABLE_VARIABLES = JSON.parse(File.read(Rails.root.join(CONFIG['paths']['brandable_variables_json']))).freeze
|
||||
SASS_STYLE = ENV['SASS_STYLE'] || (Rails.env.production? ? 'compressed' : 'nested')
|
||||
|
||||
|
||||
class << self
|
||||
def variables_map
|
||||
@variables_map ||= BRANDABLE_VARIABLES.each_with_object({}) do |variable_group, memo|
|
||||
variable_group['variables'].each { |variable| memo[variable['variable_name']] = variable }
|
||||
end.freeze
|
||||
end
|
||||
|
||||
# gets the *effective* value for a brandable variable
|
||||
def brand_variable_value(variable_name, active_brand_config=nil)
|
||||
explicit_value = active_brand_config && active_brand_config.get_value(variable_name).presence
|
||||
return explicit_value if explicit_value
|
||||
config = variables_map[variable_name]
|
||||
default = config['default']
|
||||
return brand_variable_value(default[1..-1], active_brand_config) if default && default.starts_with?('$')
|
||||
|
||||
# while in our sass, we want `url(/images/foo.png)`,
|
||||
# the Rails Asset Helpers expect us to not have the '/images/', eg: <%= image_tag('foo.png') %>
|
||||
default.sub!(/^\/images\//, '') if config['type'] == 'image'
|
||||
default
|
||||
end
|
||||
|
||||
def variants
|
||||
@variants ||= CONFIG['variants'].map{|(k)| k }.freeze
|
||||
end
|
||||
|
||||
def brandable_variants
|
||||
@brandable_variants ||= CONFIG['variants'].select{|_, v| v['brandable']}.map{ |k,_| k }.freeze
|
||||
end
|
||||
|
||||
def combined_checksums
|
||||
return @combined_checksums if ActionController::Base.perform_caching && defined? @combined_checksums
|
||||
file = Rails.root.join(CONFIG['paths']['bundles_with_deps'] + SASS_STYLE)
|
||||
if file.exist?
|
||||
@combined_checksums = JSON.parse(file.read).each_with_object({}) do |(k, v), memo|
|
||||
memo[k] = v['combinedChecksum']
|
||||
end.freeze
|
||||
elsif Rails.env.production?
|
||||
raise "you need to run #{cli} before you can serve css."
|
||||
else
|
||||
# for dev/test there might be cases where you don't want it to raise an exception
|
||||
# if you haven't ran `brandable_css` and the manifest file doesn't exist yet.
|
||||
# eg: you want to test a controller action and you don't care that it links
|
||||
# to a css file that hasn't been created yet.
|
||||
default_value = "Error: unknown css checksum. you need to run #{cli}"
|
||||
@combined_checksums = Hash.new(default_value).freeze
|
||||
end
|
||||
end
|
||||
|
||||
# bundle path should be something like "bundles/speedgrader" or "plugins/analytics/something"
|
||||
def fingerprint_for(bundle_path, variant)
|
||||
key = ["#{bundle_path}.scss", variant].join(CONFIG['manifest_key_seperator'])
|
||||
fingerprint = combined_checksums[key]
|
||||
raise "Fingerprint not found. #{bundle_path} #{variant}" unless fingerprint
|
||||
fingerprint
|
||||
end
|
||||
|
||||
def all_fingerprints_for(bundle_path)
|
||||
variants.each_with_object({}) do |variant, object|
|
||||
object[variant] = fingerprint_for(bundle_path, variant)
|
||||
end
|
||||
end
|
||||
|
||||
def cli
|
||||
'./node_modules/.bin/brandable_css'
|
||||
end
|
||||
|
||||
def compile_all!
|
||||
run_cli!
|
||||
end
|
||||
|
||||
def compile_brand!(brand_id)
|
||||
run_cli!('--brand-id', brand_id)
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def run_cli!(*args)
|
||||
# this makes sure the symlinks to app/stylesheets/plugins/analytics, etc exist
|
||||
# so their scss files can be picked up and compiled with everything else
|
||||
require 'config/initializers/plugin_symlinks'
|
||||
|
||||
command = [cli].push(*args).shelljoin + ' 2>&1'
|
||||
msg = "running BrandableCSS CLI: #{command}"
|
||||
Rails.logger.try(:debug, msg)
|
||||
raise "Error: #{msg}" unless system(command)
|
||||
end
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,25 @@
|
|||
module Canvas
|
||||
module CDN
|
||||
class << self
|
||||
def config
|
||||
@config ||= begin
|
||||
config = ActiveSupport::OrderedOptions.new
|
||||
config.enabled = false
|
||||
yml = ConfigFile.load('canvas_cdn')
|
||||
config.merge!(yml.symbolize_keys) if yml
|
||||
config
|
||||
end
|
||||
end
|
||||
|
||||
def enabled?
|
||||
config.enabled
|
||||
end
|
||||
|
||||
def push_to_s3!(*args)
|
||||
return unless enabled?
|
||||
uploader = Canvas::CDN::S3Uploader.new(*args)
|
||||
uploader.upload!
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,74 @@
|
|||
module Canvas
|
||||
module CDN
|
||||
class S3Uploader
|
||||
|
||||
attr_accessor :bucket, :config
|
||||
|
||||
def initialize(folder='dist')
|
||||
require 'aws-sdk'
|
||||
@folder = folder
|
||||
@config = Canvas::CDN.config
|
||||
@s3 = AWS::S3.new(access_key_id: config.aws_access_key_id,
|
||||
secret_access_key: config.aws_secret_access_key)
|
||||
@bucket = @s3.buckets[config.bucket]
|
||||
end
|
||||
|
||||
def local_files
|
||||
Dir.chdir(Rails.public_path) { Dir["#{@folder}/**/**"]}
|
||||
end
|
||||
|
||||
def upload!
|
||||
Parallel.each(local_files, :in_threads=>8) do |file|
|
||||
upload_file(file)
|
||||
end
|
||||
end
|
||||
|
||||
def fingerprint?(path)
|
||||
/-[0-9a-fA-F]{32}$/.match(path.basename(path.extname).to_s)
|
||||
end
|
||||
|
||||
def font?(path)
|
||||
%w{.ttf .ttc .otf .eot .woff .woff2}.include?(path.extname)
|
||||
end
|
||||
|
||||
def mime_for(path)
|
||||
Mime::Type.lookup_by_extension(path.extname[1..-1])
|
||||
end
|
||||
|
||||
def options_for(path)
|
||||
options = {acl: :public_read, content_type: mime_for(path)}
|
||||
if fingerprint?(path)
|
||||
options.merge!({
|
||||
cache_control: "public, max-age=#{1.year}",
|
||||
expires: 1.year.from_now.httpdate
|
||||
})
|
||||
end
|
||||
|
||||
# Set headers so font's work cross-orign. While you can also set a
|
||||
# CORSConfig when you set up your s3 bucket to do the same thing, this
|
||||
# will make sure it is always set for fonts.
|
||||
options['Access-Control-Allow-Origin'] = '*' if font?(path)
|
||||
options
|
||||
end
|
||||
|
||||
def upload_file(remote_path)
|
||||
local_path = Pathname.new("#{Rails.public_path}/#{remote_path}")
|
||||
return if (local_path.extname == '.gz') || local_path.directory?
|
||||
s3_object = bucket.objects[remote_path]
|
||||
return log("skipping already existing #{remote_path}") if s3_object.exists?
|
||||
options = options_for(local_path)
|
||||
gzipped = Pathname.new("#{local_path}.gz")
|
||||
if gzipped.exist?
|
||||
local_path = gzipped
|
||||
options[:content_encoding] = 'gzip'
|
||||
end
|
||||
log "uploading #{remote_path} #{options[:content_encoding]}"
|
||||
s3_object.write(local_path, options)
|
||||
end
|
||||
|
||||
def log(msg)
|
||||
Rails.logger.debug "#{self.class} - #{msg}"
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,132 +0,0 @@
|
|||
# pull in the bundles from the various plugins' config/assets.yml extension
|
||||
# files and combine them under a plugins.<plugin> dictionary. so e.g. the
|
||||
# stylesheets bundles from {gems,vendor}/plugins/myplugin/config/assets.yml
|
||||
# will be added under
|
||||
#
|
||||
# plugins:
|
||||
# myplugin:
|
||||
# stylesheets:
|
||||
# ...
|
||||
#
|
||||
# in the output. additionally, rescope bundle elements defined under public/
|
||||
# (in the context of the plugin) as follows:
|
||||
#
|
||||
# * public/stylesheets/compiled -> public/stylesheets/compiled/plugins/<plugin>
|
||||
# * public/* -> public/plugins/<plugin>/*
|
||||
#
|
||||
# i.e. public/x becomes public/plugins/myplugin/x with the exception of
|
||||
# compiled stylesheets, where compass throws them in the more specific
|
||||
# public/stylesheets/compiled/plugins/myplugin/
|
||||
#
|
||||
# to prevent this translation on a bundle element -- to request an element
|
||||
# from canvas-lms in your bundle, for example -- prefix it with "~:". this
|
||||
# prefix will be removed but no other changes made.
|
||||
#
|
||||
# similarly, a prefix of "otherplugin:" just as without a prefix, but the
|
||||
# rescoping will target "otherplugin" rather than "myplugin". this is useful
|
||||
# if myplugin can rely on otherplugin being installed and wishes to reuse
|
||||
# some of the assets from otherplugin.
|
||||
|
||||
|
||||
class PluginAssets
|
||||
attr_reader :anchors, :asset_matcher, :plugin_matcher
|
||||
|
||||
def initialize( options = {} )
|
||||
@anchors = { 'stylesheets' => {} }
|
||||
@asset_matcher = options[:asset_matcher] || '{gems,vendor}/plugins/*/config/assets.yml'
|
||||
@plugin_matcher = options[:plugin_matcher] || %r{^(?:gems|vendor)/plugins/(.*)/config/assets\.yml$}
|
||||
end
|
||||
|
||||
# this is the yaml that can be dropped into the top of assets.yml
|
||||
# to output the different plugin bundle definitions
|
||||
def bundle_yml
|
||||
|
||||
subdoc = YAML.dump('plugins' => plugin_assets).gsub(/^---\s?\n/, '')
|
||||
|
||||
# add anchors to the various bundles in the imported plugin asset definitions.
|
||||
# these bundles will be included in the known bundle types below with a
|
||||
# namespaced bundle name. for instance, the bar stylesheet bundle in the foo
|
||||
# plugin will be referred under stylesheets below as "plugins/foo/bar" and
|
||||
# use the corresponding anchor.
|
||||
#
|
||||
# I'd add the anchors programmatically instead of through post-processing of
|
||||
# the serialized document, but I couldn't figure out how
|
||||
subdoc.gsub!(%r{^( {6})plugins/([^/]*)/([^/]*)/([^:]*): ?$}) do |match|
|
||||
indent, plugin, type, bundle = $1, $2, $3, $4
|
||||
namespaced_bundle = "plugins_#{plugin}_#{bundle}"
|
||||
anchor = "#{type}_#{namespaced_bundle}"
|
||||
anchors[type][namespaced_bundle] = anchor if anchors[type]
|
||||
"#{indent}#{bundle}: &#{anchor}"
|
||||
end
|
||||
|
||||
# for some reason the serialized document outputs as
|
||||
#
|
||||
# plugins:
|
||||
# foo:
|
||||
# stylesheets:
|
||||
# bar: &anchor
|
||||
# - value
|
||||
#
|
||||
# instead of
|
||||
#
|
||||
# plugins:
|
||||
# foo:
|
||||
# stylesheets:
|
||||
# bar: &anchor
|
||||
# - value
|
||||
#
|
||||
# both are equivalent without an anchor on bar, but with an anchor on bar,
|
||||
# the first for adds value to plugins.foo.stylesheets.bar but *not* to
|
||||
# *anchor. the second form adds value to both.
|
||||
subdoc.gsub!(%r{(^ {6})- (.*)$}, '\\1 - \\2')
|
||||
|
||||
subdoc
|
||||
|
||||
end
|
||||
|
||||
def anchors_yml(options = {})
|
||||
indent_depth = options[:indent_depth] || 2
|
||||
indent_token = Array.new(indent_depth, ' ').join('')
|
||||
bundle_yml if anchors['stylesheets'].empty?
|
||||
anchors['stylesheets'].map { |(bundle, anchor)| "#{bundle}: *#{anchor}" }.join( "\n#{indent_token}" )
|
||||
end
|
||||
|
||||
def plugin_assets
|
||||
return @plugin_assets if @plugin_assets
|
||||
|
||||
@plugin_assets = {}
|
||||
for_each_plugin do |name, assets|
|
||||
assets.each do |type,bundles|
|
||||
bundles.keys.each do |bundle,entries|
|
||||
# the bundle is temporarily renamed to a fully namespaced bundle so
|
||||
# that we can detect and translate that namespace into an anchor in
|
||||
# post-processing.
|
||||
bundles["plugins/#{name}/#{type}/#{bundle}"] = bundles.
|
||||
delete(bundle).map { |entry| format_bundle_entry( entry, name ) }
|
||||
end
|
||||
end
|
||||
@plugin_assets[name] = assets
|
||||
end
|
||||
@plugin_assets
|
||||
end
|
||||
|
||||
def for_each_plugin
|
||||
Dir.glob( asset_matcher ).sort.each do |asset_file|
|
||||
yield plugin_name_for(asset_file), YAML.load(File.read(asset_file))
|
||||
end
|
||||
end
|
||||
|
||||
def format_bundle_entry(entry, plugin)
|
||||
entry.gsub(%r{^public/stylesheets/compiled/}, "~:public/stylesheets/compiled/plugins/#{plugin}/").
|
||||
gsub(%r{^public/}, "~:public/plugins/#{plugin}/").
|
||||
gsub(%r{^(\w+):public/stylesheets/compiled/}, "~:public/stylesheets/compiled/plugins/\\1/").
|
||||
gsub(%r{^(\w+):public/}, "~:public/plugins/\\1/").
|
||||
gsub(%r{^~:}, '')
|
||||
end
|
||||
|
||||
def plugin_name_for(path)
|
||||
match = plugin_matcher.match(path)
|
||||
raise ArgumentError, 'must provide a valid plugin asset.yml path' unless match
|
||||
match[1]
|
||||
end
|
||||
end
|
|
@ -1,33 +0,0 @@
|
|||
##########################################################################
|
||||
# See the docs at the top of assets.yml for more info on what this is for.
|
||||
##########################################################################
|
||||
|
||||
module MultiVariantCompassCompiler
|
||||
|
||||
VARIANTS = %w{legacy_normal_contrast legacy_high_contrast new_styles_normal_contrast new_styles_high_contrast k12_normal_contrast k12_high_contrast}.freeze
|
||||
|
||||
def all_sass_files
|
||||
require 'config/initializers/plugin_symlinks'
|
||||
# build the list of files ourselves so that we get it to follow symlinks
|
||||
sass_path = File.expand_path('app/stylesheets')
|
||||
sass_files = Dir.glob("#{sass_path}/{,plugins/*/}**/[^_]*.s[ac]ss")
|
||||
end
|
||||
|
||||
def self.make_variants_from_real_assets_yml
|
||||
require 'erb'
|
||||
require 'yaml'
|
||||
require 'jammit'
|
||||
original_yml = ERB.new(File.read(File.expand_path('../../config/assets_real.yml', __FILE__))).result(binding)
|
||||
parsed = YAML.load(original_yml)
|
||||
stylesheets_with_variants = {}
|
||||
VARIANTS.each do |variant|
|
||||
parsed['stylesheets'].each do |bundle_name, file_paths|
|
||||
paths = file_paths.map{ |p| p.gsub(%r{/compiled/}, '_compiled/' + variant + '/')}
|
||||
stylesheets_with_variants[bundle_name + '_' + variant] = paths
|
||||
end
|
||||
end
|
||||
parsed['stylesheets'] = stylesheets_with_variants
|
||||
YAML.dump(parsed)
|
||||
end
|
||||
|
||||
end
|
|
@ -0,0 +1,17 @@
|
|||
namespace :brand_configs do
|
||||
desc "Write _brand_variable.scss to disk so canvas_css can render stylesheets for that branding. " +
|
||||
"Set BRAND_CONFIG_MD5=<whatever> to save just that one, otherwise writes a file for each BrandConfig in db."
|
||||
task :write => :environment do
|
||||
if md5 = ENV['BRAND_CONFIG_MD5']
|
||||
BrandConfig.find(md5).save_file!
|
||||
else
|
||||
Rake::Task['brand_configs:clean'].invoke
|
||||
BrandConfig.find_each(&:save_file!)
|
||||
end
|
||||
end
|
||||
|
||||
desc "Remove all Brand Variable scss files"
|
||||
task :clean do
|
||||
rm_rf BrandConfig::CONFIG['paths']['branded_scss_folder']
|
||||
end
|
||||
end
|
|
@ -119,18 +119,12 @@ namespace :canvas do
|
|||
|
||||
tasks = Hash.new
|
||||
|
||||
if compile_css
|
||||
tasks["Compile sass and make jammit css bundles"] = -> {
|
||||
log_time('npm run compile-sass') do
|
||||
half_of_avilable_cores = (processes / 2).ceil.to_s
|
||||
raise unless system({"CANVAS_SASS_STYLE" => "compressed", "CANVAS_BUILD_CONCURRENCY" => half_of_avilable_cores}, "npm run compile-sass")
|
||||
end
|
||||
# "tmp/brandable_css_bundles_with_deps.json needs to exist before we run
|
||||
# handlebars stuff, so we have to do this first
|
||||
require 'lib/brandable_css'
|
||||
log_time('compile css (including custom brands)') { BrandableCSS.compile_all! }
|
||||
|
||||
log_time("Jammit") do
|
||||
require 'jammit'
|
||||
Jammit.package!
|
||||
end
|
||||
}
|
||||
if compile_css
|
||||
tasks["css:styleguide"] = -> {
|
||||
Rake::Task['css:styleguide'].invoke
|
||||
}
|
||||
|
@ -170,6 +164,7 @@ namespace :canvas do
|
|||
end
|
||||
combined_time = times.reduce(:+)
|
||||
puts "Finished compiling assets in #{real_time}. parallelism saved #{combined_time - real_time} (#{real_time.to_f / combined_time.to_f * 100.0}%)"
|
||||
raise "Error reving files" unless system('node_modules/.bin/gulp rev')
|
||||
end
|
||||
|
||||
desc "Check static assets and generate api documentation."
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
namespace :canvas do
|
||||
namespace :cdn do
|
||||
desc 'Push static assets to s3'
|
||||
task :upload_to_s3 => :environment do
|
||||
Canvas::CDN.push_to_s3!
|
||||
end
|
||||
end
|
||||
end
|
|
@ -4,10 +4,4 @@ namespace :css do
|
|||
puts "--> creating styleguide"
|
||||
puts `dress_code config/styleguide.yml`
|
||||
end
|
||||
|
||||
desc "Compile css assets."
|
||||
task :generate do
|
||||
raise 'the new way to compile sass is with `npm run compile-sass`. FYI, it uses libsass and is much faster'
|
||||
end
|
||||
|
||||
end
|
||||
|
|
27
package.json
|
@ -2,21 +2,19 @@
|
|||
"name": "canvas-lms",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"autoprefixer": "^2.2.0",
|
||||
"coffee-script": "1.6.2",
|
||||
"compute-cluster": "0.0.9",
|
||||
"glob": "~3.2.9",
|
||||
"js-yaml": "^3.1.0",
|
||||
"lodash": "^2.4.1",
|
||||
"mkdirp": "^0.5.0",
|
||||
"node-sass": "0.9.3",
|
||||
"react-tools": "0.11.2",
|
||||
"requirejs": "~2.1.10",
|
||||
"uglify-js": "~2.4.12"
|
||||
"brandable_css": "0.0.30"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel": "^5.6.10",
|
||||
"coffee-script": "coffee-script is pinned to exact version matching 'coffee-script-source' bundler gem so all output matches",
|
||||
"coffee-script": "1.6.2",
|
||||
"compute-cluster": "0.0.9",
|
||||
"fleck": "~0.5.1",
|
||||
"gglobby": "0.0.2",
|
||||
"glob": "^5.0.6",
|
||||
"gulp": "^3.9",
|
||||
"gulp-load-plugins": "^0.10.0",
|
||||
"gulp-rev": "^5.0.1",
|
||||
"karma": "~0.10.9",
|
||||
"karma-coffee-preprocessor": "0.1.3",
|
||||
"karma-coverage": "~0.1.4",
|
||||
|
@ -27,7 +25,13 @@
|
|||
"karma-qunit": "~0.1.1",
|
||||
"karma-requirejs": "^0.2.2",
|
||||
"karma-safari-launcher": "~0.1.1",
|
||||
"lodash": "^2.4.1",
|
||||
"react-tools": "~0.11.2",
|
||||
"requirejs": "~2.1.10",
|
||||
"testem": "~0.7.1",
|
||||
"uglify-js": "~2.4.12",
|
||||
"vinyl-fs": "vinly-fs is not actually required by us but is required by glup. Pinning it to 0.3.7 here because 0.3.8 doesn't work with symlinks. We should really be using npm-shrinkwrap and when we do, remove this.",
|
||||
"vinyl-fs": "0.3.7",
|
||||
"xsslint": "0.1.0"
|
||||
},
|
||||
"repository": {
|
||||
|
@ -37,7 +41,6 @@
|
|||
"scripts": {
|
||||
"test": "./node_modules/karma/bin/karma start --browsers Chrome,Firefox,Safari --single-run",
|
||||
"compress": "node script/compress.js",
|
||||
"compile-sass": "node script/compile-sass.js",
|
||||
"preinstall": "script/gem_npm install",
|
||||
"preupdate": "script/gem_npm update"
|
||||
}
|
||||
|
|
BIN
public/find.png
Before Width: | Height: | Size: 666 B |
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
Before Width: | Height: | Size: 32 KiB After Width: | Height: | Size: 32 KiB |
|
@ -0,0 +1 @@
|
|||
../favicon.ico
|
Before Width: | Height: | Size: 19 KiB After Width: | Height: | Size: 19 KiB |
Before Width: | Height: | Size: 25 KiB After Width: | Height: | Size: 25 KiB |
|
@ -294,11 +294,6 @@ define([
|
|||
$('#self_registration_type_radios').toggle(this.checked);
|
||||
}).trigger('change');
|
||||
|
||||
|
||||
$('.branding_section_toggler').on('change', function(){
|
||||
$(this).prevAll('.branding_section').last().toggle(!this.checked)
|
||||
})
|
||||
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
This folder contains symlinks to every client app's built JS assets.
|
||||
|
||||
A client app is expected to store its built JS asset in:
|
||||
|
||||
/client_apps/NAME/dist/NAME.js
|
|
@ -1,45 +0,0 @@
|
|||
const computecluster = require('compute-cluster');
|
||||
const glob = require('glob');
|
||||
const path = require('path');
|
||||
const mkdirp = require('mkdirp');
|
||||
|
||||
var clusterOpts = {
|
||||
module: path.join(__dirname, "compile-sass_worker.js"),
|
||||
max_backlog: 10000
|
||||
}
|
||||
|
||||
// You can run this with with `node script/compile-sass.js app/stylesheets/jst/something.scss to compile a specific file.
|
||||
var sassFileToConvert = process.argv[2];
|
||||
var sassFiles = sassFileToConvert ? [sassFileToConvert] : glob.sync("app/stylesheets/{,plugins/*/}**/[^_]*.s[ac]ss");
|
||||
|
||||
|
||||
// by default, we'll create a cluster as big as the number of cores on your machine,
|
||||
// set the CANVAS_BUILD_CONCURRENCY environment variable if you want it to be something else
|
||||
if (process.env.CANVAS_BUILD_CONCURRENCY) clusterOpts.max_processes = parseInt(process.env.CANVAS_BUILD_CONCURRENCY);
|
||||
// if we're just doing one file, just spin up one worker
|
||||
if (sassFileToConvert) clusterOpts.max_processes = 1;
|
||||
var cc = new computecluster(clusterOpts);
|
||||
|
||||
cc.on('error', function(e) {
|
||||
cc.exit()
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
const VARIANTS = 'legacy_normal_contrast legacy_high_contrast new_styles_normal_contrast new_styles_high_contrast k12_normal_contrast k12_high_contrast'.split(' ')
|
||||
|
||||
var toRun = 0;
|
||||
VARIANTS.forEach(function(variant){
|
||||
sassFiles.forEach(function(sassFile){
|
||||
// TODO: figure out how to exclude app/stylesheets/jst from the glob when running everything.
|
||||
if (sassFile.match(/^app\/stylesheets\/jst/) && (!sassFileToConvert || variant !== 'legacy_normal_contrast')) return;
|
||||
|
||||
toRun++
|
||||
cc.enqueue([variant, sassFile], function(err,r){
|
||||
if (err) return// console.log("an error occured compiling sass:", err);
|
||||
if (--toRun === 0) {
|
||||
cc.exit();
|
||||
}
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
const fs = require('fs')
|
||||
const mkdirp = require('mkdirp')
|
||||
const sass = require('node-sass')
|
||||
const path = require('path')
|
||||
const yaml = require('js-yaml')
|
||||
const _ = require('lodash')
|
||||
|
||||
|
||||
const browserSupport = _.map(yaml.safeLoad(fs.readFileSync('config/browsers.yml')).minimums, function(version, browserName) {
|
||||
return browserName.replace('Internet Explorer', 'Explorer') + ' >= ' + version
|
||||
})
|
||||
const autoprefixer = require('autoprefixer')(browserSupport)
|
||||
|
||||
// if you want compressed output (eg: in production), set the environment variable CANVAS_SASS_STYLE=compressed
|
||||
const outputStyle = process.env.CANVAS_SASS_STYLE || 'nested'
|
||||
|
||||
process.on('message', function(variantAndSassFile){
|
||||
const variant = variantAndSassFile[0]
|
||||
const sassFile = variantAndSassFile[1]
|
||||
const cssFolder = path.dirname(sassFile).replace(/^app\/stylesheets/, 'public/stylesheets_compiled/' + variant)
|
||||
const cssFile = cssFolder + '/' + path.basename(sassFile).replace(/.s[ac]ss$/, '.css')
|
||||
var includePaths = ['app/stylesheets', 'app/stylesheets/variants/' + variant]
|
||||
|
||||
// pull in 'config/brand_variables.scss' if we should
|
||||
if ((variant === 'new_styles_normal_contrast' || variant === 'k12_normal_contrast') && fs.existsSync('config/brand_variables.scss')) {
|
||||
includePaths.unshift('config')
|
||||
}
|
||||
|
||||
// make sure the folder is there before we try to write the css file to it
|
||||
mkdirp.sync(cssFolder)
|
||||
|
||||
sass.render({
|
||||
file: sassFile,
|
||||
success: function(css){
|
||||
try {
|
||||
css = autoprefixer.process(css, {cascade: false})
|
||||
} catch (e) {
|
||||
console.log("FAILED on: " + sassFile, e.message)
|
||||
throw e
|
||||
}
|
||||
fs.writeFile(cssFile, css, function(err) {
|
||||
if (err) return console.log(err)
|
||||
process.send('complete')
|
||||
})
|
||||
},
|
||||
error: function(errorMsg){
|
||||
console.log('Error compiling sass:', errorMsg)
|
||||
throw new Error(errorMsg)
|
||||
},
|
||||
includePaths: includePaths,
|
||||
imagePath: '/images',
|
||||
outputStyle: outputStyle,
|
||||
sourceComments: (outputStyle === 'compressed' ? 'none' : 'normal'), // one of 'none', 'normal', 'map'
|
||||
sourceMap: false
|
||||
})
|
||||
})
|
|
@ -0,0 +1,38 @@
|
|||
define [
|
||||
'jquery'
|
||||
'compiled/util/brandableCss'
|
||||
], ($, brandableCss) ->
|
||||
|
||||
testBundleId = 'bundles/foo-asdf1234'
|
||||
|
||||
stubENV = ->
|
||||
window.ENV ||= {}
|
||||
window.ENV.active_brand_config = "brand_config_id"
|
||||
window.ENV.ASSET_HOST = 'http://cdn.example.com'
|
||||
window.ENV.use_new_styles = true
|
||||
window.ENV.use_high_contrast = true
|
||||
|
||||
module 'brandableCss.loadStylesheet'
|
||||
test 'should load correctly', ->
|
||||
brandableCss.loadStylesheet(testBundleId)
|
||||
ok $('head link[rel="stylesheet"]:last').attr('href').match(testBundleId)
|
||||
|
||||
module 'brandableCss.getCssVariant'
|
||||
test 'should be legacy_normal_contrast by default', ->
|
||||
equal brandableCss.getCssVariant(), 'legacy_normal_contrast'
|
||||
|
||||
test 'should pick up ENV settings', ->
|
||||
stubENV()
|
||||
equal brandableCss.getCssVariant(), 'new_styles_high_contrast'
|
||||
|
||||
module 'brandableCss.urlFor'
|
||||
test 'should have right default', ->
|
||||
window.ENV = {}
|
||||
expected = "/dist/brandable_css/legacy_normal_contrast/#{testBundleId}.css"
|
||||
equal brandableCss.urlFor(testBundleId), expected
|
||||
|
||||
test 'should pick up ENV settings', ->
|
||||
stubENV()
|
||||
window.ENV.use_high_contrast = false
|
||||
expected = "http://cdn.example.com/dist/brandable_css/#{window.ENV.active_brand_config}/new_styles_normal_contrast/#{testBundleId}.css"
|
||||
equal brandableCss.urlFor(testBundleId), expected
|
|
@ -1,23 +0,0 @@
|
|||
define [
|
||||
'jquery'
|
||||
'compiled/util/registerTemplateCss'
|
||||
], ($, registerTemplateCss) ->
|
||||
|
||||
testColor = 'rgb(255, 0, 0)'
|
||||
testRule = "#fixtures {color:#{testColor};}"
|
||||
testTemplateId = templateId = 'test_template_id'
|
||||
|
||||
module 'registerTemplateCss'
|
||||
test 'should render correctly', ->
|
||||
registerTemplateCss testTemplateId, testRule
|
||||
equal $('#fixtures').css('color'), testColor
|
||||
|
||||
test 'should append <style> node to bottom of <head>', ->
|
||||
registerTemplateCss testTemplateId, testRule
|
||||
ok $('head style:last').text().indexOf("/* From: #{testTemplateId} */\n#{testRule}") >= 0
|
||||
|
||||
test 'should remove all styles when you call clear()', ->
|
||||
registerTemplateCss testTemplateId, testRule
|
||||
registerTemplateCss.clear()
|
||||
equal $('head style:last').text(), ''
|
||||
ok $('#fixtures').css('color') != testColor
|
|
@ -1,155 +0,0 @@
|
|||
require File.expand_path(File.dirname(__FILE__) + '/../../../../lib/canvas/plugins/plugin_assets')
|
||||
|
||||
describe PluginAssets do
|
||||
|
||||
let(:plugin_assets) { PluginAssets.new }
|
||||
let(:fixture_assets) { PluginAssets.new( :asset_matcher => 'spec/fixtures/asset_files/*.yml', :plugin_matcher => %r{(\w+)\.yml$} ) }
|
||||
|
||||
describe '#initialize' do
|
||||
|
||||
describe 'with no options' do
|
||||
subject { plugin_assets }
|
||||
|
||||
describe '#anchors' do
|
||||
subject { super().anchors }
|
||||
it { is_expected.to eq({ 'stylesheets' => {} }) }
|
||||
end
|
||||
|
||||
describe '#asset_matcher' do
|
||||
subject { super().asset_matcher }
|
||||
it { is_expected.to eq '{gems,vendor}/plugins/*/config/assets.yml' }
|
||||
end
|
||||
|
||||
describe '#plugin_matcher' do
|
||||
subject { super().plugin_matcher }
|
||||
it { is_expected.to eq %r{^(?:gems|vendor)/plugins/(.*)/config/assets\.yml$} }
|
||||
end
|
||||
end
|
||||
|
||||
describe 'with an options hash' do
|
||||
let(:options) { {:asset_matcher => 'test/*/file.yml', :plugin_matcher => %r{(\w+)} } }
|
||||
subject { PluginAssets.new options }
|
||||
|
||||
describe '#asset_matcher' do
|
||||
subject { super().asset_matcher }
|
||||
it { is_expected.to eq options[:asset_matcher] }
|
||||
end
|
||||
|
||||
describe '#plugin_matcher' do
|
||||
subject { super().plugin_matcher }
|
||||
it { is_expected.to eq options[:plugin_matcher] }
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe '#bundle_yml' do
|
||||
subject { fixture_assets.bundle_yml }
|
||||
|
||||
it { is_expected.to match %r{plugin_assets_1:\s+stylesheets:} }
|
||||
it { is_expected.to match %r{plugin_assets_2:\s+stylesheets:} }
|
||||
it { is_expected.to match %r{plugins/plugin_assets_1/first_plugin\.css} }
|
||||
it { is_expected.to match %r{plugins/plugin_assets_1/first_plugin_alt\.css} }
|
||||
it { is_expected.to match %r{plugins/plugin_assets_2/second_plugin\.css} }
|
||||
it { is_expected.to match %r{plugins/plugin_assets_2/second_plugin_alt\.css} }
|
||||
end
|
||||
|
||||
describe '#anchors_yml' do
|
||||
subject { fixture_assets.anchors_yml }
|
||||
|
||||
it { is_expected.to match %r{plugins_plugin_assets_1_first_plugin: \*stylesheets_plugins_plugin_assets_1_first_plugin} }
|
||||
it { is_expected.to match %r{plugins_plugin_assets_1_first_plugin_alt: \*stylesheets_plugins_plugin_assets_1_first_plugin_alt} }
|
||||
it { is_expected.to match %r{plugins_plugin_assets_2_second_plugin: \*stylesheets_plugins_plugin_assets_2_second_plugin} }
|
||||
it { is_expected.to match %r{plugins_plugin_assets_2_second_plugin_alt: \*stylesheets_plugins_plugin_assets_2_second_plugin_alt} }
|
||||
|
||||
#check indent depth
|
||||
it { is_expected.to match %r{^ plugins} }
|
||||
it { is_expected.not_to match %r{^ plugins} }
|
||||
|
||||
describe 'with overriden indent' do
|
||||
subject { fixture_assets.anchors_yml(:indent_depth => 4) }
|
||||
|
||||
it { is_expected.to match %r{plugins_plugin_assets_1_first_plugin: \*stylesheets_plugins_plugin_assets_1_first_plugin} }
|
||||
it { is_expected.to match %r{^ plugins} }
|
||||
it { is_expected.not_to match %r{^ plugins} }
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
describe '#for_each_plugin' do
|
||||
|
||||
before do
|
||||
@yield_values = []
|
||||
fixture_assets.for_each_plugin { |name, yaml| @yield_values << [name, yaml] }
|
||||
end
|
||||
|
||||
it 'pulls the plugin names correctly' do
|
||||
expect(@yield_values[0][0]).to eq 'plugin_assets_1'
|
||||
expect(@yield_values[1][0]).to eq 'plugin_assets_2'
|
||||
end
|
||||
|
||||
it 'parses the yaml files' do
|
||||
expect(@yield_values[0][1]["stylesheets"].keys.sort).to eq ['first_plugin', 'first_plugin_alt']
|
||||
expect(@yield_values[1][1]["stylesheets"].keys.sort).to eq ['second_plugin', 'second_plugin_alt']
|
||||
end
|
||||
end
|
||||
|
||||
describe '#format_bundle_entry' do
|
||||
let(:plugin) { "plugin_name" }
|
||||
|
||||
def formatted(path)
|
||||
fixture_assets.format_bundle_entry( path, plugin )
|
||||
end
|
||||
|
||||
it 'tacks a plugin path onto a compiled css path' do
|
||||
expect(formatted('public/stylesheets/compiled/something.css')).to eq "public/stylesheets/compiled/plugins/#{plugin}/something.css"
|
||||
end
|
||||
|
||||
it 'expands a simple path with the plugin name' do
|
||||
expect(formatted( 'public/jellyfish/sting.txt')).to eq "public/plugins/#{plugin}/jellyfish/sting.txt"
|
||||
end
|
||||
|
||||
it 'moves plugin_name from before the path into the middle of the path for a compiled css path' do
|
||||
new_path = formatted( 'other_plugin_name:public/stylesheets/compiled/something.css' )
|
||||
expect(new_path).to eq "public/stylesheets/compiled/plugins/other_plugin_name/something.css"
|
||||
end
|
||||
|
||||
it 'moves the plugin_name forward on a simiple path' do
|
||||
expect(formatted( 'other_plugin:public/simple/path.css' )).to eq "public/plugins/other_plugin/simple/path.css"
|
||||
end
|
||||
end
|
||||
|
||||
describe '#plugin_name_for' do
|
||||
it 'pulls the plugin name out of well formed vendor/plugin paths' do
|
||||
expect(plugin_assets.plugin_name_for('vendor/plugins/analytics/config/assets.yml')).to eq 'analytics'
|
||||
end
|
||||
|
||||
it 'pulls the plugin name out of well formed gems/plugin paths' do
|
||||
expect(plugin_assets.plugin_name_for('gems/plugins/analytics/config/assets.yml')).to eq 'analytics'
|
||||
end
|
||||
|
||||
it 'errors on badly formed paths' do
|
||||
expect { plugin_assets.plugin_name_for('bogus/path/blargh.yml') }.to raise_error(ArgumentError, 'must provide a valid plugin asset.yml path')
|
||||
end
|
||||
end
|
||||
|
||||
describe '#plugin_assets' do
|
||||
it 'builds a hash for dumping to yaml' do
|
||||
expect(fixture_assets.plugin_assets).to eq({
|
||||
"plugin_assets_2" => {
|
||||
"stylesheets" => {
|
||||
"plugins/plugin_assets_2/stylesheets/second_plugin" => ["public/stylesheets/compiled/plugins/plugin_assets_2/second_plugin.css"],
|
||||
"plugins/plugin_assets_2/stylesheets/second_plugin_alt" => ["public/stylesheets/compiled/plugins/plugin_assets_2/second_plugin_alt.css"]
|
||||
}
|
||||
},
|
||||
"plugin_assets_1" => {
|
||||
"stylesheets" => {
|
||||
"plugins/plugin_assets_1/stylesheets/first_plugin_alt" => ["public/stylesheets/compiled/plugins/plugin_assets_1/first_plugin_alt.css"],
|
||||
"plugins/plugin_assets_1/stylesheets/first_plugin" => ["public/stylesheets/compiled/plugins/plugin_assets_1/first_plugin.css"]
|
||||
}
|
||||
}
|
||||
})
|
||||
end
|
||||
end
|
||||
|
||||
end
|