From db028887761d1093c719f92da1214a1853818e3d Mon Sep 17 00:00:00 2001 From: Dan Elbert Date: Sun, 9 Sep 2018 16:37:25 -0500 Subject: [PATCH] icons --- app/javascript/components/AppIcon.vue | 194 ++++++++++++++++++++++---- config/webpack/environment.js | 6 +- config/webpack/loaders/svg.js | 16 ++- config/webpack/loaders/svg_loader.js | 27 ++++ lib/tasks/dev.rake | 47 +++++-- 5 files changed, 244 insertions(+), 46 deletions(-) create mode 100644 config/webpack/loaders/svg_loader.js diff --git a/app/javascript/components/AppIcon.vue b/app/javascript/components/AppIcon.vue index 1c22293..bc115db 100644 --- a/app/javascript/components/AppIcon.vue +++ b/app/javascript/components/AppIcon.vue @@ -1,6 +1,6 @@ @@ -19,16 +19,102 @@ import Star from "../iconic/svg/smart/star"; import StarEmpty from "../iconic/svg/smart/star-empty"; import X from "../iconic/svg/smart/x"; - - const iconicInstance = IconicJs({ - autoInjectSelector: null - }); + + const SVG_CACHE = {}; + let REMAP_COUNT = 0; class IconData { - constructor(url, dataAttributes) { - this.url = url; + constructor(svgData, dataAttributes) { + this.svgData = svgData; this.dataAttributes = dataAttributes || {}; - this.dataAttributes['src'] = url; + this.svgTemplate = null; + } + + getSvg() { + if (this.svgTemplate === null) { + const iconicName = this.svgData.attributes.class; + + this.svgTemplate = SVG_CACHE[iconicName] || null; + + if (this.svgTemplate === null) { + this.svgTemplate = document.createElementNS("http://www.w3.org/2000/svg", "svg"); + SVG_CACHE[iconicName] = this.svgTemplate; + + for (let attr in this.svgData.attributes) { + this.svgTemplate.setAttribute(attr, this.svgData.attributes[attr]); + } + this.svgTemplate.innerHTML = this.svgData.content; + + + // Find then prune the scripts + const scripts = [...this.svgTemplate.querySelectorAll('script')]; + + scripts.filter(s => { + let scriptType = s.getAttribute("type"); + return !scriptType || scriptType === 'application/ecmascript' || scriptType === 'application/javascript' + }).forEach(s => { + new Function(s.innerText || s.textContent)(window); + this.svgTemplate.removeChild(s); + }); + } + } + + const clone = this.svgTemplate.cloneNode(true); + this.remapIds(clone); + this.attachIconicApi(clone); + for (let attr in this.dataAttributes) { + clone.setAttribute(`data-${attr}`, this.dataAttributes[attr]); + } + return clone; + } + + remapIds(svg) { + const iriElementsAndProperties = { + 'clipPath': ['clip-path'], + 'color-profile': ['color-profile'], + 'cursor': ['cursor'], + 'filter': ['filter'], + 'linearGradient': ['fill', 'stroke'], + 'marker': ['marker', 'marker-start', 'marker-mid', 'marker-end'], + 'mask': ['mask'], + 'pattern': ['fill', 'stroke'], + 'radialGradient': ['fill', 'stroke'] + }; + + REMAP_COUNT += 1; + + let element, elementDefs, properties, currentId, newId; + Object.keys(iriElementsAndProperties).forEach(function (key) { + element = key; + properties = iriElementsAndProperties[key]; + + elementDefs = svg.querySelectorAll('defs ' + element + '[id]'); + for (var i = 0, elementsLen = elementDefs.length; i < elementsLen; i++) { + currentId = elementDefs[i].id; + newId = currentId + '-' + REMAP_COUNT; + + // All of the properties that can reference this element type + var referencingElements; + properties.forEach(function (property) { + // :NOTE: using a substring match attr selector here to deal with IE "adding extra quotes in url() attrs" + referencingElements = svg.querySelectorAll('[' + property + '*="' + currentId + '"]'); + for (var j = 0, referencingElementLen = referencingElements.length; j < referencingElementLen; j++) { + referencingElements[j].setAttribute(property, 'url(#' + newId + ')'); + } + }); + + elementDefs[i].id = newId; + } + }); + } + + attachIconicApi(svg) { + const key = svg.getAttribute("data-icon"); + const apis = window.iconicSmartIconApis; + if (key && apis[key]) { + const iconApi = apis[key](svg); + for (let func in iconApi) svg[func] = iconApi[func] + } } } @@ -38,6 +124,43 @@ this.defaultPadding = defaultPadding || null; } } + + class IconicQueue { + constructor(iconicInstance) { + this.iconic = iconicInstance; + this.updateQueue = []; + this.isTimerRunning = false; + } + + processQueues() { + if (this.updateQueue.length > 0) { + console.log(`updating ${this.updateQueue.length}`); + this.iconic.update(this.updateQueue); + this.updateQueue.forEach(s => (s.style.visibility = null)); + this.updateQueue = []; + } + + this.isTimerRunning = false; + } + + ensureTimer() { + if (!this.isTimerRunning) { + this.isTimerRunning = true; + setTimeout(() => this.processQueues()); + } + } + + updateSvg(svgElem) { + this.updateQueue.push(svgElem); + this.ensureTimer(); + } + } + + const iconicInstance = IconicJs({ + autoInjectSelector: "#notreal-noinjection" + }); + + const iconicQueue = new IconicQueue(iconicInstance); const iconMap = { 'caret-bottom': new IconData(Caret, {direction: 'bottom'}), @@ -97,15 +220,6 @@ return sizeMap[this.size]; }, - extraIconAttributes() { - const attrs = {}; - - for (let attr in this.iconData.dataAttributes) { - attrs[`data-${attr}`] = this.iconData.dataAttributes[attr]; - } - return attrs; - }, - sizeClass() { return this.sizeData.bulmaIconClass; }, @@ -115,22 +229,42 @@ } }, - mounted() { - const self = this; - setTimeout(() => { - iconicInstance.inject(this.$refs.img, { - each: function(svg) { self.injectedSvg = svg; } + methods: { + insertSvg() { + const parent = this.$refs.container; + + while (parent.firstChild) { + parent.removeChild(parent.firstChild); + } + + const svg = this.iconData.getSvg(); + this.injectedSvg = svg; + + svg.style.padding = this.svgPadding; + svg.style.visibility = "hidden"; + svg.classList.add("iconic-fluid"); + parent.appendChild(svg); + + + setTimeout(() => { + //svg.style.visibility = "visible"; + //iconicInstance.update(svg); + iconicQueue.updateSvg(svg); }); - }); + } }, - updated() { - if (this.injectedSvg) { - for(let attr in this.extraIconAttributes) { - this.injectedSvg.setAttribute(attr, this.extraIconAttributes[attr]); + mounted() { + let self = this; + this.$watch( + function() { return this.icon + this.size + this.padding }, + function() { + this.insertSvg(); + }, + { + immediate: true } - iconicInstance.update(this.injectedSvg); - } + ) } } diff --git a/config/webpack/environment.js b/config/webpack/environment.js index 3b91fd3..dc70989 100644 --- a/config/webpack/environment.js +++ b/config/webpack/environment.js @@ -3,9 +3,9 @@ const vue = require('./loaders/vue'); const svg = require('./loaders/svg'); environment.loaders.append('vue', vue); -//environment.loaders.prepend('svg', svg); +environment.loaders.prepend('svg', svg); -//const fileLoader = environment.loaders.get('file'); -//fileLoader.exclude = /\.(svg)$/i; +const fileLoader = environment.loaders.get('file'); +fileLoader.exclude = /\.(svg)$/i; module.exports = environment; diff --git a/config/webpack/loaders/svg.js b/config/webpack/loaders/svg.js index 7b00dcc..b1da427 100644 --- a/config/webpack/loaders/svg.js +++ b/config/webpack/loaders/svg.js @@ -1,10 +1,18 @@ +const path = require("path"); module.exports = { test: /\.svg$/, use: [{ - loader: 'url-loader', - options: { - limit: 10000 - } + loader: path.join(__dirname, 'svg_loader.js') }] }; + +// module.exports = { +// test: /\.svg$/, +// use: [{ +// loader: 'url-loader', +// options: { +// limit: 10000 +// } +// }] +// }; diff --git a/config/webpack/loaders/svg_loader.js b/config/webpack/loaders/svg_loader.js new file mode 100644 index 0000000..619b321 --- /dev/null +++ b/config/webpack/loaders/svg_loader.js @@ -0,0 +1,27 @@ +module.exports = function(content) { + this.cacheable && this.cacheable(); + + var match = content.match(/]+)+>([\s\S]+)<\/svg>/i); + var attrs = {}; + + if (match) { + attrs = match[1]; + if (attrs) { + attrs = attrs.match(/([\w-:]+)(=)?("[^<>"]*"|'[^<>']*'|[\w-:]+)/g) + .reduce(function(obj, attr){ + var split = attr.split('='); + var name = split[0]; + var value = true; + if (split && split[1]) { + value = split[1].replace(/['"]/g, ''); + } + obj[name] = value; + return obj; + }, {}) + } + + content = match[2] || ''; + }; + + return "module.exports = " + JSON.stringify({attributes: attrs, content: content}); +}; \ No newline at end of file diff --git a/lib/tasks/dev.rake b/lib/tasks/dev.rake index ec5b4de..77ab93f 100644 --- a/lib/tasks/dev.rake +++ b/lib/tasks/dev.rake @@ -3,21 +3,50 @@ namespace :dev do desc 'Run both rails server and webpack dev server' task :run do - pids = [] - - pids << fork do + rails_pid = fork do exec 'rails s' end - pids << fork do + webpack_pid = fork do exec 'bin/webpack-dev-server' end - begin - Process.wait - rescue SignalException - puts 'shutting down...' - pids.each { |pid| Process.kill("SIGINT", pid) } + running = true + shutdown = false + shutdown_start = nil + + Signal.trap('SIGINT') do + shutdown = true + end + + while running + rails_check ||= Process.waitpid(rails_pid, Process::WNOHANG) + webpack_check ||= Process.waitpid(webpack_pid, Process::WNOHANG) + running = rails_check.nil? || webpack_check.nil? + + if shutdown + if shutdown_start.nil? + puts "Shutting down..." + shutdown_start = Time.now + #Process.kill("SIGINT", rails_pid) rescue Errno::ESRCH + #Process.kill("SIGINT", webpack_pid) rescue Errno::ESRCH + end + + if (Time.now - shutdown_start) > 5 + if rails_check.nil? + puts "Force killing rails..." + Process.kill("KILL", rails_pid) + end + + if webpack_check.nil? + puts "Force killing webpack..." + Process.kill("KILL", webpack_pid) + end + end + + end + + sleep 0.25 end end