diff --git a/dev/sass/controls.scss b/dev/sass/controls.scss index fbe37955..ab066a22 100644 --- a/dev/sass/controls.scss +++ b/dev/sass/controls.scss @@ -4,6 +4,22 @@ border-radius: $control-radius; } +.copy-button { + margin-top: 10px; + padding: 8px 12px; + background-color: #4CAF50; + color: #fff; + border: none; + border-radius: 4px; + cursor: pointer; + font-size: 14px; +} + +.copy-button:hover { + background-color: #45a049; +} + + input, textarea { @extend %control; diff --git a/dev/src/views/Expression.js b/dev/src/views/Expression.js index c9d45717..ad8708b4 100644 --- a/dev/src/views/Expression.js +++ b/dev/src/views/Expression.js @@ -31,51 +31,51 @@ import EventDispatcher from "../events/EventDispatcher" import app from "../app"; export default class Expression extends EventDispatcher { - constructor (el) { + constructor(el) { super(); this.el = el; this.delim = "/"; this.lexer = new ExpressionLexer(); - + this._initUI(el); - app.flavor.on("change", ()=> this._onFlavorChange()); + app.flavor.on("change", () => this._onFlavorChange()); this._onFlavorChange(); } - + set value(expression) { let regex = Utils.decomposeRegEx(expression || Expression.DEFAULT_EXPRESSION, this.delim); this.pattern = regex.source; this.flags = regex.flags; } - + get value() { return this.editor.getValue(); } - + set pattern(pattern) { let index = this.editor.getValue().lastIndexOf(this.delim); - this.editor.replaceRange(pattern, {line: 0, ch: 1}, {line: 0, ch: index}); + this.editor.replaceRange(pattern, { line: 0, ch: 1 }, { line: 0, ch: index }); this._deferUpdate(); } - + get pattern() { return Utils.decomposeRegEx(this.editor.getValue(), this.delim).source; } - + set flags(flags) { flags = app.flavor.validateFlagsStr(flags); let str = this.editor.getValue(), index = str.lastIndexOf(this.delim); - this.editor.replaceRange(flags, {line: 0, ch: index + 1}, {line: 0, ch: str.length }); // this doesn't work if readOnly is false. + this.editor.replaceRange(flags, { line: 0, ch: index + 1 }, { line: 0, ch: str.length }); // this doesn't work if readOnly is false. } - + get flags() { return Utils.decomposeRegEx(this.editor.getValue(), this.delim).flags; } - + get token() { return this.lexer.token; } - + showFlags() { this.flagsList.selected = this.flags.split(""); app.tooltip.toggle.toggleOn("flags", this.flagsEl, this.flagsBtn, true, -2); @@ -83,13 +83,13 @@ export default class Expression extends EventDispatcher { toggleFlag(s) { let flags = this.flags, i = flags.indexOf(s); - this.flags = i>=0 ? flags.replace(s, "") : flags+s; + this.flags = i >= 0 ? flags.replace(s, "") : flags + s; } - + showFlavors() { app.tooltip.toggle.toggleOn("flavor", this.flavorEl, this.flavorBtn, true, -2) } - + insert(str) { this.editor.replaceSelection(str, "end"); } @@ -97,8 +97,8 @@ export default class Expression extends EventDispatcher { selectAll() { CMUtils.selectAll(this.editor); } - -// private methods: + + // private methods: _initUI(el) { this.editorEl = $.query("> .editor", el); let editor = this.editor = CMUtils.create(this.editorEl, { @@ -106,31 +106,48 @@ export default class Expression extends EventDispatcher { maxLength: 2500, singleLine: true }, "100%", "100%"); - - editor.on("mousedown", (cm, evt)=> this._onEditorMouseDown(cm, evt)); - editor.on("change", (cm, evt)=> this._onEditorChange(cm, evt)); - editor.on("keydown", (cm, evt)=> this._onEditorKeyDown(cm, evt)); - // hacky method to disable overwrite mode on expressions to avoid overwriting flags: - editor.toggleOverwrite = ()=>{}; - + + // Add Copy Button + const copyButton = document.createElement("button"); + copyButton.textContent = "Copy"; + copyButton.className = "copy-button"; + copyButton.addEventListener("click", () => { + const patternText = this.pattern || ""; + navigator.clipboard.writeText(patternText).then(() => { + alert("Pattern copied!"); + }).catch(err => { + console.error("Failed to copy pattern:", err); + }); + }); + + // Append the button to the parent container + this.editorEl.parentElement.appendChild(copyButton); + + editor.on("mousedown", (cm, evt) => this._onEditorMouseDown(cm, evt)); + editor.on("change", (cm, evt) => this._onEditorChange(cm, evt)); + editor.on("keydown", (cm, evt) => this._onEditorKeyDown(cm, evt)); + + editor.toggleOverwrite = () => { }; // Disable overwrite mode for flags + this.errorEl = $.query(".icon.alert", this.editorEl); - this.errorEl.addEventListener("mouseenter", (evt)=>this._onMouseError(evt)); - this.errorEl.addEventListener("mouseleave", (evt)=>this._onMouseError(evt)); - + this.errorEl.addEventListener("mouseenter", (evt) => this._onMouseError(evt)); + this.errorEl.addEventListener("mouseleave", (evt) => this._onMouseError(evt)); + this.highlighter = new ExpressionHighlighter(editor); this.hover = new ExpressionHover(editor, this.highlighter); - + this._setInitialExpression(); this._initTooltips(el); this.value = Expression.DEFAULT_EXPRESSION; } - + + _setInitialExpression() { let editor = this.editor; editor.setValue("/./g"); - + // leading / - editor.getDoc().markText({line: 0, ch: 0}, { + editor.getDoc().markText({ line: 0, ch: 0 }, { line: 0, ch: 1 }, { @@ -139,9 +156,9 @@ export default class Expression extends EventDispatcher { atomic: true, inclusiveLeft: true }); - + // trailing /g - editor.getDoc().markText({line: 0, ch: 2}, { + editor.getDoc().markText({ line: 0, ch: 2 }, { line: 0, ch: 4 }, { @@ -152,11 +169,11 @@ export default class Expression extends EventDispatcher { }); this._deferUpdate(); } - + _deferUpdate() { - Utils.defer(()=>this._update(), "Expression._update"); + Utils.defer(() => this._update(), "Expression._update"); } - + _update() { let expr = this.editor.getValue(); this.lexer.profile = app.flavor.profile; @@ -166,50 +183,50 @@ export default class Expression extends EventDispatcher { this.highlighter.draw(token); this.dispatchEvent("change"); } - + _initTooltips(el) { const template = $.template` ${"label"}`; - let flavorData = app.flavor.profiles.map((o)=>({id:o.id, label:o.label+" ("+(o.browser?"Browser":"Server")+")"})); - + let flavorData = app.flavor.profiles.map((o) => ({ id: o.id, label: o.label + " (" + (o.browser ? "Browser" : "Server") + ")" })); + this.flavorBtn = $.query("section.expression .button.flavor", el); this.flavorEl = $.query("#library #tooltip-flavor"); - this.flavorList = new List($.query("ul.list", this.flavorEl), {data:flavorData, template}); - this.flavorList.on("change", ()=>this._onFlavorListChange()); + this.flavorList = new List($.query("ul.list", this.flavorEl), { data: flavorData, template }); + this.flavorList.on("change", () => this._onFlavorListChange()); this.flavorBtn.addEventListener("click", (evt) => this.showFlavors()); - $.query(".icon.help", this.flavorEl).addEventListener("click", ()=> app.sidebar.goto("engine")); - + $.query(".icon.help", this.flavorEl).addEventListener("click", () => app.sidebar.goto("engine")); + this.flagsBtn = $.query("section.expression .button.flags", el); this.flagsEl = $.query("#library #tooltip-flags"); - this.flagsList = new List($.query("ul.list", this.flagsEl), {data:[], multi:true, template}); - this.flagsList.on("change", ()=> this._onFlagListChange()); + this.flagsList = new List($.query("ul.list", this.flagsEl), { data: [], multi: true, template }); + this.flagsList.on("change", () => this._onFlagListChange()); this.flagsBtn.addEventListener("click", (evt) => this.showFlags()); - $.query(".icon.help", this.flagsEl).addEventListener("click", ()=> app.sidebar.goto("flags")); + $.query(".icon.help", this.flagsEl).addEventListener("click", () => app.sidebar.goto("flags")); } -// event handlers: + // event handlers: _onFlavorListChange() { app.tooltip.toggle.hide("flavor"); app.flavor.value = this.flavorList.selected; - Track.page("flavor/"+this.flavorList.selected); + Track.page("flavor/" + this.flavorList.selected); } - + _onFlagListChange() { let sel = this.flagsList.selected; this.flags = sel ? sel.join("") : ""; Track.event("set_flags", "engagement", this.flags); } - + _onFlavorChange() { let flavor = app.flavor, profile = flavor.profile; this.flavorList.selected = profile.id; $.query("> .label", this.flavorBtn).innerText = profile.label; - - let supported = Expression.FLAGS.split("").filter((n)=>!!profile.flags[n]); + + let supported = Expression.FLAGS.split("").filter((n) => !!profile.flags[n]); let labels = Expression.FLAG_LABELS; - this.flagsList.data = supported.map((n)=>({id:n, label:labels[n]})); - this.flags = this.flags.split("").filter((n)=>!!profile.flags[n]).join(""); + this.flagsList.data = supported.map((n) => ({ id: n, label: labels[n] })); + this.flags = this.flags.split("").filter((n) => !!profile.flags[n]).join(""); } - + _onEditorMouseDown(cm, evt) { // offset by half a character to make accidental clicks less likely: let index = CMUtils.getCharIndexAt(cm, evt.clientX - cm.defaultCharWidth() * 0.6, evt.clientY); @@ -217,8 +234,8 @@ export default class Expression extends EventDispatcher { this.showFlags(); } } - - + + _onEditorChange(cm, evt) { // catches pasting full expressions in. // TODO: will need to be updated to work with other delimeters @@ -230,7 +247,7 @@ export default class Expression extends EventDispatcher { } this.value = str; } - + _onEditorKeyDown(cm, evt) { // Ctrl or Command + D by default, will delete the expression and the flags field, Re: https://github.com/gskinner/regexr/issues/74 // So we just manually reset to nothing here. @@ -239,7 +256,7 @@ export default class Expression extends EventDispatcher { this.pattern = ""; } } - + _onMouseError(evt) { let tt = app.tooltip.hover, errs = this.lexer.errors; if (evt.type === "mouseleave") { return tt.hide("error"); } @@ -247,9 +264,9 @@ export default class Expression extends EventDispatcher { let err = errs.length === 1 && errs[0].error; let str = err ? app.reference.getError(err, errs[0]) : "Problems in the Expression are underlined in red. Roll over them for details."; let label = err && err.warning ? "WARNING" : "PARSE ERROR"; - tt.showOn("error", ""+label+": "+str, this.errorEl); + tt.showOn("error", "" + label + ": " + str, this.errorEl); } - + } Expression.DEFAULT_EXPRESSION = "/([A-Z])\\w+/g";